dataface 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- d3_format/__init__.py +14 -0
- d3_format/errors.py +19 -0
- d3_format/format.py +551 -0
- d3_format/spec.py +159 -0
- dataface/DATAFACE_SYNTAX.md +1135 -0
- dataface/__init__.py +93 -0
- dataface/_docs_site.py +20 -0
- dataface/_install_hint.py +26 -0
- dataface/agent_api/__init__.py +79 -0
- dataface/agent_api/_init_templates/__init__.py +0 -0
- dataface/agent_api/_init_templates/agents_dft_snippet.md +26 -0
- dataface/agent_api/_init_templates/dataface.yml +15 -0
- dataface/agent_api/_init_templates/faces-dataface.yml +144 -0
- dataface/agent_api/_init_templates/index.md +24 -0
- dataface/agent_api/_paths.py +118 -0
- dataface/agent_api/_project_agents_md.py +43 -0
- dataface/agent_api/_session_store.py +486 -0
- dataface/agent_api/_state.py +28 -0
- dataface/agent_api/chat.py +221 -0
- dataface/agent_api/dashboards.py +257 -0
- dataface/agent_api/describe.py +366 -0
- dataface/agent_api/describe_query.py +120 -0
- dataface/agent_api/docs/__init__.py +25 -0
- dataface/agent_api/docs/_loader.py +292 -0
- dataface/agent_api/docs/yaml-reference.md +2757 -0
- dataface/agent_api/file_refs.py +118 -0
- dataface/agent_api/init.py +126 -0
- dataface/agent_api/inspect.py +128 -0
- dataface/agent_api/mcp_install.py +170 -0
- dataface/agent_api/query.py +274 -0
- dataface/agent_api/schema.py +658 -0
- dataface/agent_api/schema_search.py +284 -0
- dataface/agent_api/search.py +270 -0
- dataface/agent_api/skill_install.py +141 -0
- dataface/agent_api/skill_render.py +90 -0
- dataface/agent_api/skills.py +293 -0
- dataface/agent_api/surface_aliases.yaml +128 -0
- dataface/agent_api/validate.py +175 -0
- dataface/agent_api/validate_query.py +84 -0
- dataface/ai/__init__.py +39 -0
- dataface/ai/agent.py +139 -0
- dataface/ai/context.py +45 -0
- dataface/ai/events.py +62 -0
- dataface/ai/external_mcp.py +610 -0
- dataface/ai/generate_sql.py +96 -0
- dataface/ai/llm.py +403 -0
- dataface/ai/mcp/__init__.py +51 -0
- dataface/ai/mcp/server.py +289 -0
- dataface/ai/memories.py +85 -0
- dataface/ai/prompts.py +177 -0
- dataface/ai/schema_context.py +138 -0
- dataface/ai/skills/before-after-comparison/SKILL.md +102 -0
- dataface/ai/skills/before-after-comparison/examples/before-after-comparison.yml +24 -0
- dataface/ai/skills/dashboard-build/SKILL.md +212 -0
- dataface/ai/skills/dashboard-build/examples/_smoke.yml +15 -0
- dataface/ai/skills/dashboard-design/SKILL.md +182 -0
- dataface/ai/skills/dashboard-review/SKILL.md +113 -0
- dataface/ai/skills/dashboard-structural-review/SKILL.md +173 -0
- dataface/ai/skills/dashboard-visual-review/SKILL.md +139 -0
- dataface/ai/skills/dataface-mcp-setup/SKILL.md +177 -0
- dataface/ai/skills/dataface-troubleshooting/SKILL.md +225 -0
- dataface/ai/skills/drill-down-link/SKILL.md +112 -0
- dataface/ai/skills/drill-down-link/examples/drill-down-link.yml +27 -0
- dataface/ai/skills/faceted-small-multiples/SKILL.md +116 -0
- dataface/ai/skills/faceted-small-multiples/examples/faceted-small-multiples.yml +33 -0
- dataface/ai/skills/filter-bar-with-variables/SKILL.md +105 -0
- dataface/ai/skills/filter-bar-with-variables/examples/filter-bar-with-variables.yml +49 -0
- dataface/ai/skills/kpi-row/SKILL.md +101 -0
- dataface/ai/skills/kpi-row/examples/kpi-row.yml +55 -0
- dataface/ai/skills/report-design/SKILL.md +184 -0
- dataface/ai/skills/single-metric-bignum/SKILL.md +90 -0
- dataface/ai/skills/single-metric-bignum/examples/single-metric-bignum.yml +27 -0
- dataface/ai/skills/table-heavy-ops-dashboard/SKILL.md +114 -0
- dataface/ai/skills/table-heavy-ops-dashboard/examples/table-heavy-ops-dashboard.yml +48 -0
- dataface/ai/skills/time-series-trend/SKILL.md +93 -0
- dataface/ai/skills/time-series-trend/examples/time-series-trend.yml +26 -0
- dataface/ai/skills/top-n-with-detail/SKILL.md +98 -0
- dataface/ai/skills/top-n-with-detail/examples/top-n-with-detail.yml +45 -0
- dataface/ai/skills/two-by-two-grid-overview/SKILL.md +78 -0
- dataface/ai/skills/two-by-two-grid-overview/examples/two-by-two-grid-overview.yml +64 -0
- dataface/ai/tool_schemas.py +132 -0
- dataface/ai/tools/__init__.py +312 -0
- dataface/ai/yaml_utils.py +57 -0
- dataface/cli/__init__.py +3 -0
- dataface/cli/_console.py +48 -0
- dataface/cli/_error_format.py +83 -0
- dataface/cli/_extras.py +190 -0
- dataface/cli/_json_output.py +8 -0
- dataface/cli/_parsing.py +17 -0
- dataface/cli/_version_info.py +56 -0
- dataface/cli/commands/__init__.py +3 -0
- dataface/cli/commands/_agent_input.py +205 -0
- dataface/cli/commands/_agent_server.py +115 -0
- dataface/cli/commands/chat.py +645 -0
- dataface/cli/commands/describe.py +107 -0
- dataface/cli/commands/docs.py +131 -0
- dataface/cli/commands/extension.py +179 -0
- dataface/cli/commands/init.py +240 -0
- dataface/cli/commands/inspect.py +94 -0
- dataface/cli/commands/mcp_init.py +167 -0
- dataface/cli/commands/query.py +386 -0
- dataface/cli/commands/render.py +291 -0
- dataface/cli/commands/schema.py +411 -0
- dataface/cli/commands/search.py +49 -0
- dataface/cli/commands/serve.py +114 -0
- dataface/cli/commands/skills.py +133 -0
- dataface/cli/commands/skills_init.py +161 -0
- dataface/cli/commands/validate.py +63 -0
- dataface/cli/main.py +1501 -0
- dataface/core/__init__.py +75 -0
- dataface/core/compile/__init__.py +244 -0
- dataface/core/compile/_jinja_helpers.py +78 -0
- dataface/core/compile/channel.py +222 -0
- dataface/core/compile/chart_focus.py +101 -0
- dataface/core/compile/chart_resolved.py +169 -0
- dataface/core/compile/chart_type_detection.py +489 -0
- dataface/core/compile/chart_update.py +261 -0
- dataface/core/compile/colors.py +64 -0
- dataface/core/compile/compiler.py +904 -0
- dataface/core/compile/config.py +823 -0
- dataface/core/compile/custom_chart_types.py +208 -0
- dataface/core/compile/data_table_attachment.py +1287 -0
- dataface/core/compile/detect.py +110 -0
- dataface/core/compile/errors.py +302 -0
- dataface/core/compile/filter_injection.py +319 -0
- dataface/core/compile/introspection.py +527 -0
- dataface/core/compile/jinja.py +511 -0
- dataface/core/compile/labels_env.py +52 -0
- dataface/core/compile/markdown.py +154 -0
- dataface/core/compile/meta.py +388 -0
- dataface/core/compile/models/__init__.py +0 -0
- dataface/core/compile/models/chart/__init__.py +0 -0
- dataface/core/compile/models/chart/authored.py +2137 -0
- dataface/core/compile/models/chart/compiled.py +398 -0
- dataface/core/compile/models/config.py +347 -0
- dataface/core/compile/models/face/__init__.py +0 -0
- dataface/core/compile/models/face/authored.py +659 -0
- dataface/core/compile/models/face/compiled.py +522 -0
- dataface/core/compile/models/factories.py +201 -0
- dataface/core/compile/models/markers.py +40 -0
- dataface/core/compile/models/palette.py +36 -0
- dataface/core/compile/models/primitives.py +415 -0
- dataface/core/compile/models/query/__init__.py +0 -0
- dataface/core/compile/models/query/authored.py +246 -0
- dataface/core/compile/models/query/compiled.py +710 -0
- dataface/core/compile/models/refs.py +137 -0
- dataface/core/compile/models/source.py +611 -0
- dataface/core/compile/models/style/__init__.py +0 -0
- dataface/core/compile/models/style/authored.py +481 -0
- dataface/core/compile/models/style/compiled.py +3399 -0
- dataface/core/compile/models/style/merged.py +1682 -0
- dataface/core/compile/models/theme.py +362 -0
- dataface/core/compile/models/variable/__init__.py +0 -0
- dataface/core/compile/models/variable/authored.py +254 -0
- dataface/core/compile/models/vega_lite/__init__.py +0 -0
- dataface/core/compile/models/vega_lite/config.py +510 -0
- dataface/core/compile/models/vega_lite/contracts.py +171 -0
- dataface/core/compile/normalize_charts.py +494 -0
- dataface/core/compile/normalize_layout.py +1000 -0
- dataface/core/compile/normalize_queries.py +297 -0
- dataface/core/compile/normalize_variables.py +489 -0
- dataface/core/compile/normalizer.py +543 -0
- dataface/core/compile/palette.py +1100 -0
- dataface/core/compile/parameterized.py +658 -0
- dataface/core/compile/parser.py +228 -0
- dataface/core/compile/schema.py +20 -0
- dataface/core/compile/schema_renderers/__init__.py +0 -0
- dataface/core/compile/schema_renderers/json_schema.py +163 -0
- dataface/core/compile/schema_renderers/prompt.py +152 -0
- dataface/core/compile/schema_renderers/vscode_schema.py +301 -0
- dataface/core/compile/sizing.py +2126 -0
- dataface/core/compile/sources.py +518 -0
- dataface/core/compile/sql_authoring_lint.py +56 -0
- dataface/core/compile/style_cascade.py +471 -0
- dataface/core/compile/typography.py +299 -0
- dataface/core/compile/validator.py +301 -0
- dataface/core/compile/variables.py +53 -0
- dataface/core/compile/vega_config.py +98 -0
- dataface/core/compile/vega_lite/__init__.py +6 -0
- dataface/core/compile/vega_lite/validation.py +95 -0
- dataface/core/compile/yaml_error_formatter.py +838 -0
- dataface/core/connections.py +38 -0
- dataface/core/dashboard.py +358 -0
- dataface/core/defaults/default_config.yml +101 -0
- dataface/core/defaults/palettes/categorical/category-10-dark.yml +32 -0
- dataface/core/defaults/palettes/categorical/category-10-light.yml +43 -0
- dataface/core/defaults/palettes/categorical/category-10.yml +31 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-blue.yml +22 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-brown.yml +29 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-green.yml +20 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-orange.yml +21 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-purple.yml +20 -0
- dataface/core/defaults/palettes/categorical/editorial-10-dark.yml +32 -0
- dataface/core/defaults/palettes/categorical/editorial-10.yml +40 -0
- dataface/core/defaults/palettes/categorical/hero-6.yml +17 -0
- dataface/core/defaults/palettes/categorical/single-blue.yml +11 -0
- dataface/core/defaults/palettes/categorical/tableau.yml +20 -0
- dataface/core/defaults/palettes/data/xkcd_colors.json +3803 -0
- dataface/core/defaults/palettes/diverging/blue-red.yml +25 -0
- dataface/core/defaults/palettes/diverging/coolwarm.yml +24 -0
- dataface/core/defaults/palettes/diverging/crimson-green.yml +23 -0
- dataface/core/defaults/palettes/diverging/orange-teal.yml +23 -0
- dataface/core/defaults/palettes/diverging/sunset.yml +24 -0
- dataface/core/defaults/palettes/scaffold/dft-creams.yml +38 -0
- dataface/core/defaults/palettes/scaffold/dft-grays.yml +53 -0
- dataface/core/defaults/palettes/sequential/amber.yml +22 -0
- dataface/core/defaults/palettes/sequential/blue.yml +22 -0
- dataface/core/defaults/palettes/sequential/brown.yml +22 -0
- dataface/core/defaults/palettes/sequential/gray.yml +22 -0
- dataface/core/defaults/palettes/sequential/green.yml +22 -0
- dataface/core/defaults/palettes/sequential/purple.yml +22 -0
- dataface/core/defaults/palettes/sequential/rust.yml +22 -0
- dataface/core/defaults/palettes/sequential/teal.yml +22 -0
- dataface/core/defaults/palettes/tone/negative.yml +32 -0
- dataface/core/defaults/palettes/tone/positive.yml +22 -0
- dataface/core/defaults/palettes/tone/warning.yml +22 -0
- dataface/core/defaults/themes/_base.yaml +786 -0
- dataface/core/defaults/themes/bi.yaml +16 -0
- dataface/core/defaults/themes/carbong100.yaml +41 -0
- dataface/core/defaults/themes/cream.yaml +122 -0
- dataface/core/defaults/themes/dark.yaml +40 -0
- dataface/core/defaults/themes/diagnostics-title-angle-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-baseline-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-baseline.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-center.yaml +8 -0
- dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +25 -0
- dataface/core/defaults/themes/diagnostics-title-left.yaml +8 -0
- dataface/core/defaults/themes/diagnostics-title-offset-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +24 -0
- dataface/core/defaults/themes/editorial.yaml +147 -0
- dataface/core/defaults/themes/light.yaml +30 -0
- dataface/core/defaults/themes/looker.yaml +17 -0
- dataface/core/defaults/themes/stark.yaml +134 -0
- dataface/core/errors/__init__.py +67 -0
- dataface/core/errors/codes_compile.py +56 -0
- dataface/core/errors/codes_execute.py +177 -0
- dataface/core/errors/codes_render.py +106 -0
- dataface/core/errors/codes_unknown.py +15 -0
- dataface/core/errors/hints.py +74 -0
- dataface/core/errors/registry.py +42 -0
- dataface/core/errors/structured.py +92 -0
- dataface/core/execute/__init__.py +91 -0
- dataface/core/execute/adapters/__init__.py +49 -0
- dataface/core/execute/adapters/adapter_registry.py +400 -0
- dataface/core/execute/adapters/base.py +245 -0
- dataface/core/execute/adapters/csv_adapter.py +239 -0
- dataface/core/execute/adapters/dbt_adapter.py +283 -0
- dataface/core/execute/adapters/dbt_adapter_factory.py +212 -0
- dataface/core/execute/adapters/dbt_macro_loader.py +95 -0
- dataface/core/execute/adapters/dbt_utils.py +150 -0
- dataface/core/execute/adapters/http_adapter.py +224 -0
- dataface/core/execute/adapters/metricflow_adapter.py +94 -0
- dataface/core/execute/adapters/schema_resolver_adapter.py +144 -0
- dataface/core/execute/adapters/sql_adapter.py +710 -0
- dataface/core/execute/adapters/values_adapter.py +58 -0
- dataface/core/execute/batch.py +744 -0
- dataface/core/execute/cache_backend.py +135 -0
- dataface/core/execute/cache_keys.py +66 -0
- dataface/core/execute/dbt_jinja.py +21 -0
- dataface/core/execute/dialects/__init__.py +121 -0
- dataface/core/execute/dialects/athena.py +75 -0
- dataface/core/execute/dialects/base.py +302 -0
- dataface/core/execute/dialects/bigquery.py +38 -0
- dataface/core/execute/dialects/databricks.py +68 -0
- dataface/core/execute/dialects/duckdb.py +35 -0
- dataface/core/execute/dialects/mysql.py +68 -0
- dataface/core/execute/dialects/postgres.py +39 -0
- dataface/core/execute/dialects/redshift.py +12 -0
- dataface/core/execute/dialects/snowflake.py +51 -0
- dataface/core/execute/dialects/sqlserver.py +92 -0
- dataface/core/execute/duckdb_cache.py +712 -0
- dataface/core/execute/duckdb_config.py +26 -0
- dataface/core/execute/errors.py +213 -0
- dataface/core/execute/executor.py +1249 -0
- dataface/core/execute/parallel.py +162 -0
- dataface/core/execute/setup_sql.py +58 -0
- dataface/core/execute/source_registry.py +72 -0
- dataface/core/execute/source_resolver.py +255 -0
- dataface/core/execute/sql_guard.py +387 -0
- dataface/core/execute/sql_literals.py +199 -0
- dataface/core/fonts.py +52 -0
- dataface/core/inspect/__init__.py +32 -0
- dataface/core/inspect/cache_factory.py +98 -0
- dataface/core/inspect/db_types.py +162 -0
- dataface/core/inspect/dbt_schema.py +96 -0
- dataface/core/inspect/defaults.yml +37 -0
- dataface/core/inspect/fanout_risk.py +109 -0
- dataface/core/inspect/manifest_utils.py +77 -0
- dataface/core/inspect/partials/categorical.yml +40 -0
- dataface/core/inspect/partials/date.yml +40 -0
- dataface/core/inspect/partials/numeric.yml +55 -0
- dataface/core/inspect/partition_types.py +38 -0
- dataface/core/inspect/query_validator.py +975 -0
- dataface/core/inspect/renderer.py +354 -0
- dataface/core/inspect/resolver.py +808 -0
- dataface/core/inspect/search.py +461 -0
- dataface/core/inspect/sources/__init__.py +32 -0
- dataface/core/inspect/sources/dbt.py +738 -0
- dataface/core/inspect/sources/duckdb_utils.py +66 -0
- dataface/core/inspect/templates/__init__.py +1 -0
- dataface/core/inspect/templates/categorical_column.yml +196 -0
- dataface/core/inspect/templates/charts.yml +109 -0
- dataface/core/inspect/templates/date_column.yml +248 -0
- dataface/core/inspect/templates/model.yml +138 -0
- dataface/core/inspect/templates/numeric_column.yml +261 -0
- dataface/core/inspect/templates/quality.yml +80 -0
- dataface/core/inspect/templates/string_column.yml +263 -0
- dataface/core/project_roots.py +165 -0
- dataface/core/render/__init__.py +87 -0
- dataface/core/render/board_links.py +176 -0
- dataface/core/render/chart/__init__.py +27 -0
- dataface/core/render/chart/arc_attached_table.py +251 -0
- dataface/core/render/chart/artifacts.py +16 -0
- dataface/core/render/chart/callout.py +225 -0
- dataface/core/render/chart/decisions.py +358 -0
- dataface/core/render/chart/geo.py +700 -0
- dataface/core/render/chart/kpi.py +916 -0
- dataface/core/render/chart/labels.py +76 -0
- dataface/core/render/chart/pipeline.py +818 -0
- dataface/core/render/chart/presentation.py +36 -0
- dataface/core/render/chart/profile.py +3438 -0
- dataface/core/render/chart/render_single.py +347 -0
- dataface/core/render/chart/renderers.py +193 -0
- dataface/core/render/chart/rendering.py +565 -0
- dataface/core/render/chart/serialization.py +90 -0
- dataface/core/render/chart/spark.py +496 -0
- dataface/core/render/chart/spark_bar.py +370 -0
- dataface/core/render/chart/spec_builders.py +154 -0
- dataface/core/render/chart/standard_renderer.py +2645 -0
- dataface/core/render/chart/table.py +2957 -0
- dataface/core/render/chart/table_support.py +1452 -0
- dataface/core/render/chart/tick_values.py +66 -0
- dataface/core/render/chart/time_unit_detect.py +809 -0
- dataface/core/render/chart/title_overflow.py +157 -0
- dataface/core/render/chart/type_inference.py +122 -0
- dataface/core/render/chart/validation.py +99 -0
- dataface/core/render/chart/vega_lite.py +125 -0
- dataface/core/render/chart/vega_lite_types.py +268 -0
- dataface/core/render/chart/vl_field_maps.py +346 -0
- dataface/core/render/chart_interactivity.py +24 -0
- dataface/core/render/control_registry.py +287 -0
- dataface/core/render/converters/__init__.py +24 -0
- dataface/core/render/converters/chart.py +276 -0
- dataface/core/render/converters/html.py +98 -0
- dataface/core/render/converters/pdf.py +40 -0
- dataface/core/render/converters/png.py +41 -0
- dataface/core/render/errors.py +144 -0
- dataface/core/render/face_api.py +160 -0
- dataface/core/render/faces.py +1194 -0
- dataface/core/render/font_measurement.py +48 -0
- dataface/core/render/font_support.py +197 -0
- dataface/core/render/fonts/DFTSansTabular-Regular.ttf +0 -0
- dataface/core/render/fonts/DFTSansTabular-Regular.woff2 +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.ttf +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.ttf +0 -0
- dataface/core/render/fonts/InterVariable.ttf +0 -0
- dataface/core/render/fonts/InterVariable.woff2 +0 -0
- dataface/core/render/fonts/NOTO_COLOR_EMOJI_LICENSE.txt +93 -0
- dataface/core/render/fonts/NOTO_EMOJI_LICENSE.txt +93 -0
- dataface/core/render/fonts/NotoColorEmoji-Regular.ttf +0 -0
- dataface/core/render/fonts/NotoColorEmoji-Regular.woff2 +0 -0
- dataface/core/render/fonts/NotoEmoji-Regular.ttf +0 -0
- dataface/core/render/fonts/NotoEmoji-Regular.woff2 +0 -0
- dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -0
- dataface/core/render/fonts/SOURCE_SERIF_4_LICENSE.txt +98 -0
- dataface/core/render/fonts/SourceCodePro-Regular.ttf +0 -0
- dataface/core/render/fonts/SourceSerif4-Regular.ttf +0 -0
- dataface/core/render/fonts/_emoji_font_face.css +43 -0
- dataface/core/render/fonts/source-serif-4-variable-latin.woff2 +0 -0
- dataface/core/render/format_utils.py +329 -0
- dataface/core/render/geo_defaults.yml +28 -0
- dataface/core/render/json_format.py +146 -0
- dataface/core/render/layout_sizing.py +865 -0
- dataface/core/render/layouts.py +541 -0
- dataface/core/render/markdown_defaults.yml +16 -0
- dataface/core/render/missing_vars_prompt.py +79 -0
- dataface/core/render/placeholder.py +389 -0
- dataface/core/render/render_result.py +14 -0
- dataface/core/render/renderer.py +467 -0
- dataface/core/render/script_embedding.py +16 -0
- dataface/core/render/svg_utils.py +212 -0
- dataface/core/render/template_loader.py +69 -0
- dataface/core/render/templates/controls/_styles.css +606 -0
- dataface/core/render/templates/controls/checkbox.html +16 -0
- dataface/core/render/templates/controls/date.html +16 -0
- dataface/core/render/templates/controls/number.html +19 -0
- dataface/core/render/templates/controls/readonly.html +9 -0
- dataface/core/render/templates/controls/select.html +21 -0
- dataface/core/render/templates/controls/slider.html +22 -0
- dataface/core/render/templates/controls/text.html +16 -0
- dataface/core/render/templates/scripts/chart_interactivity.js +191 -0
- dataface/core/render/templates/scripts/variables.js +976 -0
- dataface/core/render/templates/svg/grid_pattern.svg +3 -0
- dataface/core/render/templates/svg/styles.css +51 -0
- dataface/core/render/terminal.py +311 -0
- dataface/core/render/terminal_charts.py +563 -0
- dataface/core/render/terminal_defaults.yml +2 -0
- dataface/core/render/terminal_layouts.py +299 -0
- dataface/core/render/terminal_text.py +31 -0
- dataface/core/render/text/__init__.py +1 -0
- dataface/core/render/text/case.py +113 -0
- dataface/core/render/text_format.py +129 -0
- dataface/core/render/utils.py +106 -0
- dataface/core/render/variable_controls.py +946 -0
- dataface/core/render/variable_input_refinement.py +140 -0
- dataface/core/render/warnings/__init__.py +15 -0
- dataface/core/render/warnings/bar_color_1_to_1_with_x.py +80 -0
- dataface/core/render/warnings/base.py +44 -0
- dataface/core/render/warnings/fanout_risk.py +15 -0
- dataface/core/render/warnings/from_query_diagnostic.py +56 -0
- dataface/core/render/warnings/missing_join_predicate.py +13 -0
- dataface/core/render/warnings/query_parse_error.py +14 -0
- dataface/core/render/warnings/query_returned_zero_rows.py +42 -0
- dataface/core/render/warnings/reaggregation.py +14 -0
- dataface/core/render/warnings/registry.py +45 -0
- dataface/core/render/warnings/suppression.py +46 -0
- dataface/core/render/warnings/temporal_single_point.py +63 -0
- dataface/core/render/warnings/unreferenced_chart.py +15 -0
- dataface/core/render/warnings/y_encoding_mostly_null.py +76 -0
- dataface/core/render/yaml_format.py +167 -0
- dataface/core/resolve_face.py +195 -0
- dataface/core/schema/__init__.py +0 -0
- dataface/core/schema/guidance.py +151 -0
- dataface/core/scoped_paths.py +59 -0
- dataface/core/serve/__init__.py +14 -0
- dataface/core/serve/bootstrap.py +39 -0
- dataface/core/serve/embedded.py +57 -0
- dataface/core/serve/port.py +129 -0
- dataface/core/serve/server.py +938 -0
- dataface/core/serve/templates/__init__.py +0 -0
- dataface/core/serve/templates/directory.yml +6 -0
- dataface/core/serve/templates/error.html.j2 +217 -0
- dataface/core/utils.py +121 -0
- dataface/core/validate.py +64 -0
- dataface/integrations/__init__.py +0 -0
- dataface/integrations/highlighting.py +351 -0
- dataface/integrations/markdown.py +537 -0
- dataface/py.typed +0 -0
- dataface-0.1.2.dist-info/METADATA +375 -0
- dataface-0.1.2.dist-info/RECORD +455 -0
- dataface-0.1.2.dist-info/WHEEL +4 -0
- dataface-0.1.2.dist-info/entry_points.txt +2 -0
- dataface-0.1.2.dist-info/licenses/LICENSE +202 -0
- mdsvg/__init__.py +168 -0
- mdsvg/fonts.py +656 -0
- mdsvg/images.py +299 -0
- mdsvg/parser.py +629 -0
- mdsvg/playground.py +284 -0
- mdsvg/py.typed +2 -0
- mdsvg/renderer.py +1623 -0
- mdsvg/style.py +355 -0
- mdsvg/types.py +200 -0
- mdsvg/utils.py +86 -0
d3_format/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""d3-format — Python implementation of d3-format spec parsing and formatting.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
format(spec_str)(value) -> str
|
|
5
|
+
format(spec_str, value) -> str
|
|
6
|
+
parse(spec_str) -> FormatSpec
|
|
7
|
+
D3FormatError
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from d3_format.errors import D3FormatError
|
|
11
|
+
from d3_format.format import format
|
|
12
|
+
from d3_format.spec import FormatSpec, parse
|
|
13
|
+
|
|
14
|
+
__all__ = ["D3FormatError", "FormatSpec", "format", "parse"]
|
d3_format/errors.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""D3FormatError — raised for unsupported or invalid format specs."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class D3FormatError(ValueError):
|
|
5
|
+
"""Raised when a format spec is invalid or uses an unsupported feature.
|
|
6
|
+
|
|
7
|
+
Attributes:
|
|
8
|
+
spec: The full format spec string that failed.
|
|
9
|
+
position: 0-based position in spec where the problem was detected.
|
|
10
|
+
reason: Human-readable explanation of the problem.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, spec: str, position: int, reason: str) -> None:
|
|
14
|
+
self.spec = spec
|
|
15
|
+
self.position = position
|
|
16
|
+
self.reason = reason
|
|
17
|
+
super().__init__(
|
|
18
|
+
f"d3-format parse error at position {position} in {spec!r}: {reason}"
|
|
19
|
+
)
|
d3_format/format.py
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"""d3-format Python implementation.
|
|
2
|
+
|
|
3
|
+
Implements the d3-format spec with byte-for-byte parity to d3.js.
|
|
4
|
+
Key deviations from Python's standard number formatting:
|
|
5
|
+
- Negative sign is U+2212 MINUS SIGN, not U+002D HYPHEN-MINUS.
|
|
6
|
+
- Rounding uses round-half-up (JavaScript Math.round), not banker's rounding.
|
|
7
|
+
- SI type uses significant-digit precision (not decimal-place precision).
|
|
8
|
+
- `d` type rounds to nearest integer using round-half-up.
|
|
9
|
+
- `n` type is locale-aware `g` with grouping.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import math
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from decimal import ROUND_HALF_UP, Decimal
|
|
17
|
+
|
|
18
|
+
from d3_format.spec import FormatSpec, parse
|
|
19
|
+
|
|
20
|
+
# U+2212 MINUS SIGN — d3 uses this, not hyphen-minus.
|
|
21
|
+
_MINUS = "−"
|
|
22
|
+
# U+221E INFINITY SYMBOL — d3's d/,d use this for infinite values.
|
|
23
|
+
_INF_SYMBOL = "∞"
|
|
24
|
+
|
|
25
|
+
# SI prefixes: index 8 = '' (10^0), 9 = 'k' (10^3), etc.
|
|
26
|
+
_SI_PREFIXES = [
|
|
27
|
+
"y",
|
|
28
|
+
"z",
|
|
29
|
+
"a",
|
|
30
|
+
"f",
|
|
31
|
+
"p",
|
|
32
|
+
"n",
|
|
33
|
+
"µ",
|
|
34
|
+
"m",
|
|
35
|
+
"",
|
|
36
|
+
"k",
|
|
37
|
+
"M",
|
|
38
|
+
"G",
|
|
39
|
+
"T",
|
|
40
|
+
"P",
|
|
41
|
+
"E",
|
|
42
|
+
"Z",
|
|
43
|
+
"Y",
|
|
44
|
+
]
|
|
45
|
+
_SI_PREFIX_OFFSET = 8 # index of the '' (10^0) entry
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _round_half_up(x: float, decimals: int) -> float:
|
|
49
|
+
"""Round x to `decimals` decimal places using round-half-up (like JS Math.round).
|
|
50
|
+
|
|
51
|
+
Python's built-in round() uses banker's rounding; d3/JS always rounds 0.5 up.
|
|
52
|
+
We use Decimal for exact midpoint detection.
|
|
53
|
+
"""
|
|
54
|
+
if not math.isfinite(x) or math.isnan(x):
|
|
55
|
+
return x
|
|
56
|
+
quant = Decimal(
|
|
57
|
+
"1e-" + str(decimals)
|
|
58
|
+
) # works for any int, including 0 and negative
|
|
59
|
+
# Use Decimal(x) — the exact IEEE-754 binary value — so rounding matches d3/JS.
|
|
60
|
+
# Decimal(repr(x)) would round "2.55" up to "2.6", but d3 rounds it down to "2.5"
|
|
61
|
+
# because the binary value is 2.5499..., not 2.55.
|
|
62
|
+
return float(Decimal(x).quantize(quant, rounding=ROUND_HALF_UP))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _fmt_fixed(abs_val: float, decimals: int) -> str:
|
|
66
|
+
"""Format abs_val to `decimals` decimal places with round-half-up."""
|
|
67
|
+
rounded = _round_half_up(abs_val, decimals)
|
|
68
|
+
return f"{rounded:.{decimals}f}"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _format_group(digits: str, separator: str) -> str:
|
|
72
|
+
"""Apply thousands grouping (groups of 3) to a digit string."""
|
|
73
|
+
n = len(digits)
|
|
74
|
+
first = n % 3 or 3
|
|
75
|
+
parts = [digits[:first]]
|
|
76
|
+
i = first
|
|
77
|
+
while i < n:
|
|
78
|
+
parts.append(digits[i : i + 3])
|
|
79
|
+
i += 3
|
|
80
|
+
return separator.join(parts)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _apply_grouping(body: str, separator: str, decimal_pt: str) -> str:
|
|
84
|
+
"""Apply thousands grouping to the integer part of a formatted number.
|
|
85
|
+
|
|
86
|
+
Handles plain integers, decimals, and exponential notation.
|
|
87
|
+
"""
|
|
88
|
+
if "e" in body:
|
|
89
|
+
mantissa, exp_part = body.split("e", 1)
|
|
90
|
+
if decimal_pt in mantissa:
|
|
91
|
+
int_part, frac = mantissa.split(decimal_pt, 1)
|
|
92
|
+
return (
|
|
93
|
+
_format_group(int_part, separator) + decimal_pt + frac + "e" + exp_part
|
|
94
|
+
)
|
|
95
|
+
return _format_group(mantissa, separator) + "e" + exp_part
|
|
96
|
+
if decimal_pt in body:
|
|
97
|
+
int_part, frac = body.split(decimal_pt, 1)
|
|
98
|
+
return _format_group(int_part, separator) + decimal_pt + frac
|
|
99
|
+
return _format_group(body, separator)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _apply_trim(body: str) -> str:
|
|
103
|
+
"""Remove trailing zeros (and trailing decimal point) from a formatted number."""
|
|
104
|
+
if "e" in body:
|
|
105
|
+
mantissa, exp_part = body.split("e", 1)
|
|
106
|
+
if "." in mantissa:
|
|
107
|
+
mantissa = mantissa.rstrip("0").rstrip(".")
|
|
108
|
+
return mantissa + "e" + exp_part
|
|
109
|
+
if "." in body:
|
|
110
|
+
return body.rstrip("0").rstrip(".")
|
|
111
|
+
return body
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _format_decimal_parts(x: float, precision: int) -> tuple[str, int] | None:
|
|
115
|
+
"""Replicate d3's formatDecimalParts(x, p).
|
|
116
|
+
|
|
117
|
+
x must be positive and finite (non-zero).
|
|
118
|
+
Returns (coefficient_digits, exponent) where:
|
|
119
|
+
- coefficient_digits: the p significant-figure string (may be longer if carry)
|
|
120
|
+
- exponent: position of the leading digit (i.e. value ≈ 0.coefficient × 10^(exp+1))
|
|
121
|
+
|
|
122
|
+
e.g. formatDecimalParts(1.23, 3) → ("123", 0)
|
|
123
|
+
formatDecimalParts(1234.5, 3) → ("123", 3) (rounded to 3 sig figs)
|
|
124
|
+
|
|
125
|
+
Uses Decimal(x) — the exact IEEE-754 binary value — with ROUND_HALF_UP so that
|
|
126
|
+
rounding matches JavaScript's toExponential (d3 rounds the binary, not repr).
|
|
127
|
+
"""
|
|
128
|
+
if not math.isfinite(x) or x == 0:
|
|
129
|
+
return None
|
|
130
|
+
p = max(1, precision)
|
|
131
|
+
d = Decimal(x) # exact binary value — matches JS's toExponential rounding
|
|
132
|
+
adj = d.adjusted() # exponent of leading digit = floor(log10(|d|))
|
|
133
|
+
# Scale so rounding to nearest integer gives p significant figures.
|
|
134
|
+
scale_exp = p - 1 - adj
|
|
135
|
+
scaled = d * (Decimal(10) ** scale_exp)
|
|
136
|
+
rounded_int = scaled.quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
|
137
|
+
coefficient = str(abs(int(rounded_int)))
|
|
138
|
+
# If rounding caused a carry (e.g. 9.5 → 10 at p=1), exp_out increases.
|
|
139
|
+
exp_out = adj + (len(coefficient) - p)
|
|
140
|
+
# d3's toExponential(p-1) always returns exactly p significant-digit chars.
|
|
141
|
+
# When a carry adds a digit (e.g. '100' from p=2), truncate to match d3's shape.
|
|
142
|
+
if len(coefficient) > p:
|
|
143
|
+
coefficient = coefficient[:p]
|
|
144
|
+
return coefficient, exp_out
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _format_si(abs_val: float, precision: int) -> tuple[str, str]:
|
|
148
|
+
"""Format abs_val with SI prefix to `precision` significant digits.
|
|
149
|
+
|
|
150
|
+
Replicates d3's formatPrefixAuto(x, p) algorithm exactly:
|
|
151
|
+
1. Round to p significant digits using exponential notation.
|
|
152
|
+
2. Determine SI prefix exponent from the rounded exponent.
|
|
153
|
+
3. Build body string from coefficient positioned relative to prefix.
|
|
154
|
+
|
|
155
|
+
Returns (number_body, prefix_char).
|
|
156
|
+
"""
|
|
157
|
+
if abs_val == 0.0:
|
|
158
|
+
# d3 calls x.toPrecision(p) for zero (formatDecimalParts returns null).
|
|
159
|
+
# JS toPrecision(p) on 0 = "0." + "0"*(p-1) for p>=2, "0" for p<=1.
|
|
160
|
+
body = ("0." + "0" * (precision - 1)) if precision > 1 else "0"
|
|
161
|
+
return body, ""
|
|
162
|
+
|
|
163
|
+
coefficient, exponent = _format_decimal_parts(abs_val, precision) # type: ignore[misc]
|
|
164
|
+
|
|
165
|
+
# Clamp the level (not the product) to [-8, 8] matching d3-format formatPrefixAuto.js.
|
|
166
|
+
# Clamping the product (-24..24) instead of the level (-8..8) would allow exponents
|
|
167
|
+
# beyond ±24 that exceed the _SI_PREFIXES array bounds → IndexError.
|
|
168
|
+
prefix_exp = max(-8, min(8, math.floor(exponent / 3))) * 3
|
|
169
|
+
prefix = _SI_PREFIXES[prefix_exp // 3 + _SI_PREFIX_OFFSET]
|
|
170
|
+
|
|
171
|
+
# Position the decimal point in coefficient.
|
|
172
|
+
# i = exponent - prefix_exp + 1 = number of digits before the decimal point.
|
|
173
|
+
i = exponent - prefix_exp + 1
|
|
174
|
+
n = len(coefficient)
|
|
175
|
+
|
|
176
|
+
if i == n:
|
|
177
|
+
body = coefficient
|
|
178
|
+
elif i > n:
|
|
179
|
+
body = coefficient + "0" * (i - n)
|
|
180
|
+
elif i > 0:
|
|
181
|
+
body = coefficient[:i] + "." + coefficient[i:]
|
|
182
|
+
else:
|
|
183
|
+
# i <= 0: value is smaller than 1 in the chosen prefix unit.
|
|
184
|
+
# d3: "0." + new Array(1-i).join("0") + formatDecimalParts(x, max(0,p+i-1))[0]
|
|
185
|
+
# new Array(k).join("0") produces k-1 zeros, so (1-i)-1 = -i zeros.
|
|
186
|
+
new_prec = max(1, precision + i - 1)
|
|
187
|
+
d2 = _format_decimal_parts(abs_val, new_prec)
|
|
188
|
+
coeff2 = d2[0] if d2 else "0"
|
|
189
|
+
body = "0." + "0" * (-i) + coeff2
|
|
190
|
+
|
|
191
|
+
return body, prefix
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _js_to_precision(abs_val: float, precision: int) -> str:
|
|
195
|
+
"""Replicate JavaScript's Number.prototype.toPrecision(p).
|
|
196
|
+
|
|
197
|
+
Uses exponential notation when exponent >= precision or exponent < -6.
|
|
198
|
+
Otherwise uses fixed notation. Rounds using round-half-up.
|
|
199
|
+
|
|
200
|
+
This is the core of d3's 'g' and 'n' type formatting.
|
|
201
|
+
"""
|
|
202
|
+
# d3 treats toPrecision(0) as toPrecision(1) — g/n types use max(1, p).
|
|
203
|
+
p = max(1, precision)
|
|
204
|
+
if abs_val == 0.0:
|
|
205
|
+
# JS: (0).toPrecision(p) = "0." + "0"*(p-1) for p>=2, "0" for p=1
|
|
206
|
+
return ("0." + "0" * (p - 1)) if p > 1 else "0"
|
|
207
|
+
|
|
208
|
+
coefficient, exponent = _format_decimal_parts(abs_val, p) # type: ignore[misc]
|
|
209
|
+
|
|
210
|
+
# JS uses e notation if exponent >= p or exponent < -6.
|
|
211
|
+
if exponent >= p or exponent < -6:
|
|
212
|
+
# Exponential form: coefficient[0].coefficient[1:]e+exponent
|
|
213
|
+
if len(coefficient) > 1:
|
|
214
|
+
mantissa = coefficient[0] + "." + coefficient[1:]
|
|
215
|
+
else:
|
|
216
|
+
mantissa = coefficient[0]
|
|
217
|
+
exp_sign = "+" if exponent >= 0 else "-"
|
|
218
|
+
exp_str = str(abs(exponent))
|
|
219
|
+
return f"{mantissa}e{exp_sign}{exp_str}"
|
|
220
|
+
else:
|
|
221
|
+
# Fixed form: position decimal based on exponent.
|
|
222
|
+
i = exponent + 1 # digits before decimal point
|
|
223
|
+
n = len(coefficient)
|
|
224
|
+
if i >= n:
|
|
225
|
+
# All digits before decimal, may need trailing zeros.
|
|
226
|
+
return coefficient + "0" * (i - n)
|
|
227
|
+
elif i > 0:
|
|
228
|
+
return coefficient[:i] + "." + coefficient[i:]
|
|
229
|
+
else:
|
|
230
|
+
# 0 < abs_val < 1: prepend "0." and leading zeros.
|
|
231
|
+
return "0." + "0" * (-i) + coefficient
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _format_rounded(abs_val: float, precision: int) -> str:
|
|
235
|
+
"""Replicate d3's formatRounded(x, p) for 'r' type.
|
|
236
|
+
|
|
237
|
+
Always uses fixed notation (no e), rounds to p significant digits.
|
|
238
|
+
"""
|
|
239
|
+
if abs_val == 0.0:
|
|
240
|
+
return "0"
|
|
241
|
+
|
|
242
|
+
coefficient, exponent = _format_decimal_parts(abs_val, precision) # type: ignore[misc]
|
|
243
|
+
|
|
244
|
+
i = exponent + 1 # digits before decimal
|
|
245
|
+
n = len(coefficient)
|
|
246
|
+
if i <= 0:
|
|
247
|
+
return "0." + "0" * (-i) + coefficient
|
|
248
|
+
elif n > i:
|
|
249
|
+
return coefficient[:i] + "." + coefficient[i:]
|
|
250
|
+
else:
|
|
251
|
+
return coefficient + "0" * (i - n)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _format_general(abs_val: float, precision: int) -> str:
|
|
255
|
+
"""Format abs_val using d3's 'g' rule (x.toPrecision(p))."""
|
|
256
|
+
return _js_to_precision(abs_val, precision)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _body_is_zero(body: str) -> bool:
|
|
260
|
+
"""True if `body` contains no nonzero digit (e.g. '0', '0.0', '0.00e+0')."""
|
|
261
|
+
return not any(c.isdigit() and c != "0" for c in body)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _format_number(spec: FormatSpec, v: float) -> tuple[str, str, str]:
|
|
265
|
+
"""Format a finite, non-NaN float (v may be negative).
|
|
266
|
+
|
|
267
|
+
Returns (sign_char, body, suffix) where:
|
|
268
|
+
- sign_char: "−" | "+" | " " | ""
|
|
269
|
+
- body: formatted digit string
|
|
270
|
+
- suffix: SI prefix char, "%" or ""
|
|
271
|
+
"""
|
|
272
|
+
is_neg = math.copysign(1.0, v) < 0
|
|
273
|
+
abs_val = abs(v)
|
|
274
|
+
|
|
275
|
+
# d3: negative values get minus; but -0 loses its minus unless sign is "+".
|
|
276
|
+
# (equivalent to d3's "if valueNegative && +formattedValue === 0 && sign !== '+'" logic)
|
|
277
|
+
# We handle this after formatting for non-zero; for zero it's always suppressed unless sign="+".
|
|
278
|
+
is_neg_zero = is_neg and abs_val == 0.0
|
|
279
|
+
if is_neg_zero and spec.sign != "+":
|
|
280
|
+
is_neg = False
|
|
281
|
+
|
|
282
|
+
if is_neg:
|
|
283
|
+
sign_char = _MINUS
|
|
284
|
+
elif spec.sign == "+":
|
|
285
|
+
sign_char = "+"
|
|
286
|
+
elif spec.sign == " ":
|
|
287
|
+
sign_char = " "
|
|
288
|
+
else:
|
|
289
|
+
sign_char = ""
|
|
290
|
+
|
|
291
|
+
t = spec.type
|
|
292
|
+
precision = spec.precision
|
|
293
|
+
|
|
294
|
+
if t == "f":
|
|
295
|
+
prec = precision if precision is not None else 6
|
|
296
|
+
# JS Number.prototype.toFixed() falls back to toString() for |x| >= 10^21.
|
|
297
|
+
body = str(abs_val) if abs_val >= 1e21 else _fmt_fixed(abs_val, prec)
|
|
298
|
+
suffix = ""
|
|
299
|
+
|
|
300
|
+
elif t == "%":
|
|
301
|
+
prec = precision if precision is not None else 6
|
|
302
|
+
scaled = abs_val * 100.0
|
|
303
|
+
# toFixed fallback: |x * 100| >= 10^21 → toString(x * 100)
|
|
304
|
+
body = str(scaled) if scaled >= 1e21 else _fmt_fixed(scaled, prec)
|
|
305
|
+
suffix = "%"
|
|
306
|
+
|
|
307
|
+
elif t == "e":
|
|
308
|
+
prec = precision if precision is not None else 6
|
|
309
|
+
if abs_val == 0.0:
|
|
310
|
+
body = ("0." + "0" * prec if prec > 0 else "0") + "e+0"
|
|
311
|
+
else:
|
|
312
|
+
d = _format_decimal_parts(abs_val, prec + 1)
|
|
313
|
+
coefficient, exponent = d # type: ignore[misc]
|
|
314
|
+
# Pad to prec+1 digits in case trailing zeros were stripped
|
|
315
|
+
coefficient = coefficient.ljust(prec + 1, "0")
|
|
316
|
+
if prec > 0:
|
|
317
|
+
mantissa = coefficient[0] + "." + coefficient[1 : prec + 1]
|
|
318
|
+
else:
|
|
319
|
+
mantissa = coefficient[0]
|
|
320
|
+
exp_sign = "+" if exponent >= 0 else "-"
|
|
321
|
+
body = f"{mantissa}e{exp_sign}{abs(exponent)}"
|
|
322
|
+
suffix = ""
|
|
323
|
+
|
|
324
|
+
elif t in ("g", ""):
|
|
325
|
+
# d3's formatDefault (empty type) uses toPrecision(12) as the default precision.
|
|
326
|
+
# 'g' type uses the spec precision (default 6).
|
|
327
|
+
prec = precision if precision is not None else (12 if t == "" else 6)
|
|
328
|
+
body = _format_general(abs_val, prec)
|
|
329
|
+
suffix = ""
|
|
330
|
+
|
|
331
|
+
elif t == "r":
|
|
332
|
+
prec = precision if precision is not None else 6
|
|
333
|
+
body = _format_rounded(abs_val, prec)
|
|
334
|
+
suffix = ""
|
|
335
|
+
|
|
336
|
+
elif t == "s":
|
|
337
|
+
prec = precision if precision is not None else 6
|
|
338
|
+
body, si_suffix = _format_si(abs_val, prec)
|
|
339
|
+
suffix = si_suffix
|
|
340
|
+
|
|
341
|
+
elif t == "d":
|
|
342
|
+
if abs_val >= 1e21:
|
|
343
|
+
# JS formatDecimal uses x.toLocaleString("en") for |x| >= 1e21.
|
|
344
|
+
# str(x) gives the shortest round-trip repr; Decimal(str()) expands
|
|
345
|
+
# exponential form to the "clean" integer string matching JS.
|
|
346
|
+
s = str(abs_val)
|
|
347
|
+
body = str(int(Decimal(s))) if ("e" in s or "E" in s) else str(int(abs_val))
|
|
348
|
+
else:
|
|
349
|
+
body = str(int(_round_half_up(abs_val, 0)))
|
|
350
|
+
suffix = ""
|
|
351
|
+
|
|
352
|
+
else: # t == "n"
|
|
353
|
+
# n type: alias for ",g" — locale-aware g (grouping handled later).
|
|
354
|
+
prec = precision if precision is not None else 6
|
|
355
|
+
body = _js_to_precision(abs_val, prec)
|
|
356
|
+
suffix = ""
|
|
357
|
+
|
|
358
|
+
# d3: "if (valueNegative && +formattedValue === 0 && sign !== '+') valueNegative = false"
|
|
359
|
+
# Drop the minus sign when the formatted body rounds to zero.
|
|
360
|
+
if is_neg and spec.sign != "+" and _body_is_zero(body):
|
|
361
|
+
is_neg = False
|
|
362
|
+
sign_char = " " if spec.sign == " " else ""
|
|
363
|
+
|
|
364
|
+
return sign_char, body, suffix
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _assemble(
|
|
368
|
+
spec: FormatSpec,
|
|
369
|
+
sign_char: str,
|
|
370
|
+
body: str,
|
|
371
|
+
suffix: str,
|
|
372
|
+
) -> str:
|
|
373
|
+
"""Assemble sign + symbol + body + suffix, apply width/fill/align."""
|
|
374
|
+
currency = "$" if spec.symbol == "$" else ""
|
|
375
|
+
|
|
376
|
+
# Parens sign mode.
|
|
377
|
+
if spec.sign == "(" and sign_char == _MINUS:
|
|
378
|
+
paren_open = "("
|
|
379
|
+
paren_close = ")"
|
|
380
|
+
sign_char = ""
|
|
381
|
+
else:
|
|
382
|
+
paren_open = ""
|
|
383
|
+
paren_close = ""
|
|
384
|
+
|
|
385
|
+
# Assemble the non-padded string.
|
|
386
|
+
if paren_open:
|
|
387
|
+
full = paren_open + currency + body + suffix + paren_close
|
|
388
|
+
else:
|
|
389
|
+
full = sign_char + currency + body + suffix
|
|
390
|
+
|
|
391
|
+
if spec.width is None:
|
|
392
|
+
return full
|
|
393
|
+
|
|
394
|
+
w = spec.width
|
|
395
|
+
current = len(full)
|
|
396
|
+
if current >= w:
|
|
397
|
+
return full
|
|
398
|
+
needed = w - current
|
|
399
|
+
|
|
400
|
+
if spec.zero:
|
|
401
|
+
# Zero-pad: sign+currency, then zeros, then body+suffix.
|
|
402
|
+
# parens+zero is rejected at parse time, so paren_open is never set here.
|
|
403
|
+
prefix = sign_char + currency
|
|
404
|
+
rest = body + suffix
|
|
405
|
+
return prefix + "0" * needed + rest
|
|
406
|
+
|
|
407
|
+
if spec.align == "=":
|
|
408
|
+
prefix_str = (paren_open + currency) if paren_open else (sign_char + currency)
|
|
409
|
+
rest = body + suffix + paren_close
|
|
410
|
+
return prefix_str + spec.fill * needed + rest
|
|
411
|
+
|
|
412
|
+
if spec.align == "<":
|
|
413
|
+
return full + spec.fill * needed
|
|
414
|
+
elif spec.align == ">":
|
|
415
|
+
return spec.fill * needed + full
|
|
416
|
+
else: # "^"
|
|
417
|
+
left = needed // 2
|
|
418
|
+
right = needed - left
|
|
419
|
+
return spec.fill * left + full + spec.fill * right
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _assemble_special(
|
|
423
|
+
spec: FormatSpec,
|
|
424
|
+
text: str,
|
|
425
|
+
is_neg: bool,
|
|
426
|
+
) -> str:
|
|
427
|
+
"""Assemble NaN or Infinity string with sign/currency/width.
|
|
428
|
+
|
|
429
|
+
NaN: sign chars from spec apply (+ shows +, space shows space).
|
|
430
|
+
Infinity: actual sign from is_neg.
|
|
431
|
+
"""
|
|
432
|
+
if is_neg:
|
|
433
|
+
sign_char = _MINUS
|
|
434
|
+
elif spec.sign == "+":
|
|
435
|
+
sign_char = "+"
|
|
436
|
+
elif spec.sign == " ":
|
|
437
|
+
sign_char = " "
|
|
438
|
+
else:
|
|
439
|
+
sign_char = ""
|
|
440
|
+
|
|
441
|
+
currency = "$" if spec.symbol == "$" else ""
|
|
442
|
+
|
|
443
|
+
# Parens mode for negative infinity.
|
|
444
|
+
if spec.sign == "(" and is_neg:
|
|
445
|
+
full = "(" + currency + text + ")"
|
|
446
|
+
sign_char = ""
|
|
447
|
+
else:
|
|
448
|
+
full = sign_char + currency + text
|
|
449
|
+
|
|
450
|
+
if spec.width is None:
|
|
451
|
+
return full
|
|
452
|
+
|
|
453
|
+
w = spec.width
|
|
454
|
+
current = len(full)
|
|
455
|
+
if current >= w:
|
|
456
|
+
return full
|
|
457
|
+
needed = w - current
|
|
458
|
+
|
|
459
|
+
# For special values, zero-pad uses "0" as fill char.
|
|
460
|
+
if spec.zero:
|
|
461
|
+
prefix = sign_char + currency
|
|
462
|
+
return prefix + "0" * needed + text
|
|
463
|
+
|
|
464
|
+
if spec.align == "<":
|
|
465
|
+
return full + spec.fill * needed
|
|
466
|
+
elif spec.align in (">", "="):
|
|
467
|
+
return spec.fill * needed + full
|
|
468
|
+
else: # "^"
|
|
469
|
+
left = needed // 2
|
|
470
|
+
right = needed - left
|
|
471
|
+
return spec.fill * left + full + spec.fill * right
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _apply_spec(spec: FormatSpec, v: float | int | Decimal) -> str:
|
|
475
|
+
"""Apply a parsed FormatSpec to a numeric value."""
|
|
476
|
+
v = float(v) # coerce int, Decimal, etc.; preserves -0 sign via IEEE 754
|
|
477
|
+
|
|
478
|
+
# d and ,d type: use ∞ symbol for infinite values.
|
|
479
|
+
use_inf_symbol = spec.type in ("d",)
|
|
480
|
+
|
|
481
|
+
# The % type appends "%" even to NaN/Infinity.
|
|
482
|
+
type_suffix = "%" if spec.type == "%" else ""
|
|
483
|
+
|
|
484
|
+
# NaN.
|
|
485
|
+
if math.isnan(v):
|
|
486
|
+
return _assemble_special(spec, "NaN" + type_suffix, is_neg=False)
|
|
487
|
+
|
|
488
|
+
# Infinity.
|
|
489
|
+
if math.isinf(v):
|
|
490
|
+
is_neg = v < 0
|
|
491
|
+
text = (_INF_SYMBOL if use_inf_symbol else "Infinity") + type_suffix
|
|
492
|
+
return _assemble_special(spec, text, is_neg=is_neg)
|
|
493
|
+
|
|
494
|
+
# Normal number.
|
|
495
|
+
sign_char, body, suffix = _format_number(spec, v)
|
|
496
|
+
|
|
497
|
+
# Trim trailing zeros: explicit ~ flag, or unconditionally for empty type.
|
|
498
|
+
# d3's formatDefault (empty type) always strips trailing zeros regardless of ~.
|
|
499
|
+
if spec.trim or spec.type == "":
|
|
500
|
+
body = _apply_trim(body)
|
|
501
|
+
|
|
502
|
+
# Comma grouping: for 'n' type always group; for all others respect spec.comma.
|
|
503
|
+
# For 's' type, comma is applied to the mantissa (matters for very large SI bodies).
|
|
504
|
+
use_comma = spec.comma or spec.type == "n"
|
|
505
|
+
if use_comma:
|
|
506
|
+
if spec.zero and spec.width is not None:
|
|
507
|
+
# d3 applies comma grouping to the zero-padded digits.
|
|
508
|
+
# Pre-pad body to the largest digit count that fits the target width.
|
|
509
|
+
sign_len = len(sign_char) + (1 if spec.symbol == "$" else 0)
|
|
510
|
+
available = spec.width - sign_len - len(suffix)
|
|
511
|
+
d = len(body)
|
|
512
|
+
while True:
|
|
513
|
+
nd = d + 1
|
|
514
|
+
if nd + (nd - 1) // 3 > available:
|
|
515
|
+
break
|
|
516
|
+
d = nd
|
|
517
|
+
body = body.zfill(d)
|
|
518
|
+
body = _apply_grouping(body, ",", ".")
|
|
519
|
+
|
|
520
|
+
return _assemble(spec, sign_char, body, suffix)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def format( # noqa: A001
|
|
524
|
+
spec_str: str,
|
|
525
|
+
value: float | int | Decimal | None = None,
|
|
526
|
+
) -> Callable[[float | int | Decimal], str] | str:
|
|
527
|
+
"""Parse spec and return a formatter, or apply immediately.
|
|
528
|
+
|
|
529
|
+
Two calling conventions::
|
|
530
|
+
|
|
531
|
+
d3_format(",.2f")(1234.56) → "1,234.56"
|
|
532
|
+
d3_format(",.2f", 1234.56) → "1,234.56"
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
spec_str: d3-format spec string.
|
|
536
|
+
value: Optional value to format immediately.
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
A ``Callable[[float], str]`` if value is None, else the formatted string.
|
|
540
|
+
|
|
541
|
+
Raises:
|
|
542
|
+
D3FormatError: If spec_str is invalid or uses unsupported features.
|
|
543
|
+
"""
|
|
544
|
+
spec = parse(spec_str)
|
|
545
|
+
|
|
546
|
+
def _fmt(v: float | int | Decimal) -> str:
|
|
547
|
+
return _apply_spec(spec, v)
|
|
548
|
+
|
|
549
|
+
if value is None:
|
|
550
|
+
return _fmt
|
|
551
|
+
return _fmt(value)
|