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
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"""Chart normalization and inline chart handling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from dataface.core.compile.custom_chart_types import BUILTIN_CHART_TYPE_VALUES
|
|
12
|
+
from dataface.core.compile.errors import CompilationError, ReferenceError
|
|
13
|
+
from dataface.core.compile.jinja import extract_variable_dependencies
|
|
14
|
+
from dataface.core.compile.models.chart.authored import (
|
|
15
|
+
VALID_LAYER_TYPES,
|
|
16
|
+
Layer,
|
|
17
|
+
)
|
|
18
|
+
from dataface.core.compile.models.chart.compiled import (
|
|
19
|
+
Chart,
|
|
20
|
+
)
|
|
21
|
+
from dataface.core.compile.models.face.compiled import Layout
|
|
22
|
+
from dataface.core.compile.models.query.compiled import AnyQuery
|
|
23
|
+
from dataface.core.compile.models.style.authored import ChartStylePatch
|
|
24
|
+
from dataface.core.compile.normalize_queries import (
|
|
25
|
+
normalize_query,
|
|
26
|
+
resolve_external_query,
|
|
27
|
+
)
|
|
28
|
+
from dataface.core.compile.parser import looks_like_sql
|
|
29
|
+
from dataface.core.utils import slug_to_text as _slug_to_text
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from dataface.core.compile.custom_chart_types import CustomChartTypeRegistry
|
|
33
|
+
|
|
34
|
+
# Chart type aliases: rewrite alternate names to canonical forms.
|
|
35
|
+
# Applied before any other normalization so all downstream code sees only
|
|
36
|
+
# canonical names. donut → pie is handled separately in _normalize_chart()
|
|
37
|
+
# because it also sets style.inner_radius=0.6, not just a type rename.
|
|
38
|
+
CHART_TYPE_ALIASES: dict[str, str] = {
|
|
39
|
+
"point": "scatter",
|
|
40
|
+
"choropleth": "map",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Maps authored chart-type values to the sub-key on ChartStylePatch.
|
|
44
|
+
# Used both at normalize time (wrapping flat authored style into the monolithic
|
|
45
|
+
# shape) and at cascade/pipeline time (finding the family sub-patch for a
|
|
46
|
+
# given chart type).
|
|
47
|
+
_AUTHORED_FAMILY_TO_MONOLITHIC_KEY: dict[str, str] = {
|
|
48
|
+
"bar": "bar",
|
|
49
|
+
"histogram": "bar",
|
|
50
|
+
"line": "line",
|
|
51
|
+
"area": "area",
|
|
52
|
+
"scatter": "scatter",
|
|
53
|
+
"pie": "pie",
|
|
54
|
+
"donut": "pie",
|
|
55
|
+
"kpi": "kpi",
|
|
56
|
+
"table": "table",
|
|
57
|
+
"spark_bar": "spark_bar",
|
|
58
|
+
"heatmap": "heatmap",
|
|
59
|
+
"map": "geoshape",
|
|
60
|
+
"geoshape": "geoshape",
|
|
61
|
+
"point_map": "point_map",
|
|
62
|
+
"callout": "callout",
|
|
63
|
+
"layered": "layered",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _wrap_authored_style_for_compiled(
|
|
68
|
+
chart_type: str | None, authored_style: dict[str, Any]
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
"""Re-wrap a flat per-family authored style dict into the monolithic shape.
|
|
71
|
+
|
|
72
|
+
After the authored-side move to per-family styles, model_dump() produces
|
|
73
|
+
flat dicts (e.g. PieChartStylePatch → {"inner_radius": 0.3}). The compiled
|
|
74
|
+
Chart.style expects the monolithic ChartStylePatch shape with family sub-keys
|
|
75
|
+
(e.g. {"pie": {"inner_radius": 0.3}}). Layered charts use a flat
|
|
76
|
+
LayeredChartStyle that wraps to {"layered": {...}}.
|
|
77
|
+
Families without a monolithic slot (heatmap, geo) also pass through; any
|
|
78
|
+
family-specific fields they contain will be unknown to ChartStylePatch, but
|
|
79
|
+
those families have no authored chart-style extensions yet.
|
|
80
|
+
"""
|
|
81
|
+
key = _AUTHORED_FAMILY_TO_MONOLITHIC_KEY.get(chart_type or "")
|
|
82
|
+
if key is None:
|
|
83
|
+
return authored_style
|
|
84
|
+
return {key: authored_style}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _kpi_subtitle_error(chart_id: str | None) -> str:
|
|
88
|
+
where = f"KPI chart '{chart_id}' " if chart_id else "KPI charts "
|
|
89
|
+
return (
|
|
90
|
+
f"{where}uses `subtitle:` which is no longer part of the KPI "
|
|
91
|
+
"surface. Use the structured `support: {value, label}` block instead."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _collect_charts_from_layout(
|
|
96
|
+
layout: Layout,
|
|
97
|
+
charts: dict[str, Chart],
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Collect all inline charts from layout into the charts dict.
|
|
100
|
+
|
|
101
|
+
This normalizes the data model so ALL charts end up in face.charts,
|
|
102
|
+
making chart lookup a simple dict access (face.charts[chart_id])
|
|
103
|
+
instead of requiring layout traversal.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
layout: The layout to extract charts from
|
|
107
|
+
charts: Dict to add charts to (modified in place)
|
|
108
|
+
"""
|
|
109
|
+
for item in layout.items:
|
|
110
|
+
if item.type == "chart" and item.chart:
|
|
111
|
+
# Add inline chart to charts dict if not already there
|
|
112
|
+
chart_id = item.chart.id
|
|
113
|
+
if chart_id and chart_id not in charts:
|
|
114
|
+
charts[chart_id] = item.chart
|
|
115
|
+
|
|
116
|
+
elif item.type == "face" and item.face:
|
|
117
|
+
# Recurse into nested faces
|
|
118
|
+
_collect_charts_from_layout(item.face.layout, charts)
|
|
119
|
+
# Also collect charts from nested face's charts dict
|
|
120
|
+
if item.face.charts:
|
|
121
|
+
for chart_id, chart in item.face.charts.items():
|
|
122
|
+
if chart_id not in charts:
|
|
123
|
+
charts[chart_id] = chart
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _normalize_chart(
|
|
127
|
+
chart_id: str,
|
|
128
|
+
chart_def: Any,
|
|
129
|
+
query_registry: dict[str, AnyQuery],
|
|
130
|
+
base_path: Path | None = None,
|
|
131
|
+
default_source: str | None = None,
|
|
132
|
+
source_path: str | None = None,
|
|
133
|
+
custom_chart_types: CustomChartTypeRegistry | None = None,
|
|
134
|
+
) -> Chart:
|
|
135
|
+
"""Normalize a chart definition to Chart.
|
|
136
|
+
|
|
137
|
+
Supports external query references using the syntax:
|
|
138
|
+
`query: path/to/file.yml#query_name`
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
chart_id: Chart identifier
|
|
142
|
+
chart_def: Chart definition
|
|
143
|
+
query_registry: Query registry for resolution
|
|
144
|
+
base_path: Base path for resolving external file references
|
|
145
|
+
default_source: Default source to apply to inline queries
|
|
146
|
+
source_path: Path in YAML for edit-back support (e.g., "charts.revenue")
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Chart with resolved query
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
ReferenceError: If chart references unknown query
|
|
153
|
+
"""
|
|
154
|
+
if isinstance(chart_def, BaseModel):
|
|
155
|
+
# Covers all per-family *Patch classes (including CalloutChart which
|
|
156
|
+
# does not inherit _SharedChartFields but is still a BaseModel).
|
|
157
|
+
chart_dict = chart_def.model_dump(exclude_none=True)
|
|
158
|
+
elif isinstance(chart_def, dict):
|
|
159
|
+
chart_dict = dict(chart_def)
|
|
160
|
+
else:
|
|
161
|
+
raise CompilationError(f"Invalid chart definition: {chart_def}")
|
|
162
|
+
|
|
163
|
+
# Set ID
|
|
164
|
+
chart_dict["id"] = chart_id
|
|
165
|
+
|
|
166
|
+
# Auto-derive the chart-id slug into the appropriate slot. KPI charts
|
|
167
|
+
# use ``label`` (the text rendered above the headline value); every
|
|
168
|
+
# other chart type uses ``title`` (the section heading). Authoring an
|
|
169
|
+
# explicit empty value is honored so authors can opt out — only a
|
|
170
|
+
# missing key falls through to the slug.
|
|
171
|
+
raw_chart_type = chart_dict.get("type")
|
|
172
|
+
canonical_chart_type = (
|
|
173
|
+
CHART_TYPE_ALIASES.get(raw_chart_type, raw_chart_type)
|
|
174
|
+
if isinstance(raw_chart_type, str)
|
|
175
|
+
else raw_chart_type
|
|
176
|
+
)
|
|
177
|
+
if canonical_chart_type == "kpi":
|
|
178
|
+
if "label" not in chart_dict:
|
|
179
|
+
chart_dict["label"] = _slug_to_text(chart_id)
|
|
180
|
+
else:
|
|
181
|
+
if "title" not in chart_dict:
|
|
182
|
+
chart_dict["title"] = _slug_to_text(chart_id)
|
|
183
|
+
|
|
184
|
+
# Ensure description (might be None or missing)
|
|
185
|
+
if not chart_dict.get("description"):
|
|
186
|
+
chart_dict["description"] = ""
|
|
187
|
+
if not chart_dict.get("subtitle"):
|
|
188
|
+
chart_dict["subtitle"] = ""
|
|
189
|
+
|
|
190
|
+
# Rewrite chart type aliases to canonical forms
|
|
191
|
+
raw_type = chart_dict.get("type")
|
|
192
|
+
if raw_type in CHART_TYPE_ALIASES:
|
|
193
|
+
chart_dict["type"] = CHART_TYPE_ALIASES[raw_type]
|
|
194
|
+
|
|
195
|
+
# Set type to "auto" if not specified (enables auto-detection at render time)
|
|
196
|
+
if "type" not in chart_dict or chart_dict.get("type") is None:
|
|
197
|
+
chart_dict["type"] = "auto"
|
|
198
|
+
|
|
199
|
+
# Validate chart type against builtins + custom registry.
|
|
200
|
+
# Unknown types are always rejected. When a custom_chart_types
|
|
201
|
+
# registry is provided, registered custom types are also accepted.
|
|
202
|
+
_chart_type = chart_dict.get("type")
|
|
203
|
+
if _chart_type and _chart_type not in BUILTIN_CHART_TYPE_VALUES:
|
|
204
|
+
if not (custom_chart_types and custom_chart_types.is_registered(_chart_type)):
|
|
205
|
+
valid = sorted(
|
|
206
|
+
(BUILTIN_CHART_TYPE_VALUES | CHART_TYPE_ALIASES.keys()) - {"auto"}
|
|
207
|
+
)
|
|
208
|
+
msg = (
|
|
209
|
+
f"Chart '{chart_id}' has unknown type '{_chart_type}'. "
|
|
210
|
+
f"Valid built-in types: {valid}"
|
|
211
|
+
)
|
|
212
|
+
if custom_chart_types and (
|
|
213
|
+
custom := sorted(custom_chart_types.registered_names())
|
|
214
|
+
):
|
|
215
|
+
msg += f". Registered custom types: {custom}"
|
|
216
|
+
raise CompilationError(msg)
|
|
217
|
+
|
|
218
|
+
# Validate layered chart syntax
|
|
219
|
+
if _chart_type == "layered":
|
|
220
|
+
raw_layers = chart_dict.get("layers")
|
|
221
|
+
if not raw_layers:
|
|
222
|
+
raise CompilationError(
|
|
223
|
+
f"Chart '{chart_id}' has type 'layered' but no `layers` list. "
|
|
224
|
+
"Layered charts require at least one layer, each with a `type`."
|
|
225
|
+
)
|
|
226
|
+
validated_layers: list[Layer] = []
|
|
227
|
+
layer_aliases = {
|
|
228
|
+
k for k, v in CHART_TYPE_ALIASES.items() if v in VALID_LAYER_TYPES
|
|
229
|
+
}
|
|
230
|
+
for i, layer in enumerate(raw_layers):
|
|
231
|
+
if not isinstance(layer, dict):
|
|
232
|
+
raise CompilationError(
|
|
233
|
+
f"Chart '{chart_id}', layer {i}: expected a dict, got {type(layer).__name__}"
|
|
234
|
+
)
|
|
235
|
+
layer_type = layer.get("type")
|
|
236
|
+
if not layer_type:
|
|
237
|
+
raise CompilationError(
|
|
238
|
+
f"Chart '{chart_id}', layer {i}: each layer must specify a `type` "
|
|
239
|
+
f"(e.g. bar, line, area). Valid layer types: {sorted(VALID_LAYER_TYPES | layer_aliases)}"
|
|
240
|
+
)
|
|
241
|
+
# Apply chart type aliases to layers too (point→scatter, etc.)
|
|
242
|
+
if layer_type in CHART_TYPE_ALIASES:
|
|
243
|
+
layer_type = CHART_TYPE_ALIASES[layer_type]
|
|
244
|
+
layer["type"] = layer_type
|
|
245
|
+
if layer_type == "layered":
|
|
246
|
+
raise CompilationError(
|
|
247
|
+
f"Chart '{chart_id}', layer {i}: nested `type: layered` is not allowed. "
|
|
248
|
+
"Use primitive mark types (bar, line, area, etc.) inside layers."
|
|
249
|
+
)
|
|
250
|
+
if layer_type not in VALID_LAYER_TYPES:
|
|
251
|
+
raise CompilationError(
|
|
252
|
+
f"Chart '{chart_id}', layer {i}: invalid layer type '{layer_type}'. "
|
|
253
|
+
f"Valid layer types: {sorted(VALID_LAYER_TYPES | layer_aliases)}"
|
|
254
|
+
)
|
|
255
|
+
# Validate per-layer query reference exists in query registry
|
|
256
|
+
layer_query = layer.get("query")
|
|
257
|
+
if layer_query is not None:
|
|
258
|
+
if not isinstance(layer_query, str):
|
|
259
|
+
raise CompilationError(
|
|
260
|
+
f"Chart '{chart_id}', layer {i}: layer `query` must be a "
|
|
261
|
+
"string reference to a named query or inline SQL, got "
|
|
262
|
+
f"{type(layer_query).__name__}"
|
|
263
|
+
)
|
|
264
|
+
if layer_query not in query_registry:
|
|
265
|
+
if looks_like_sql(layer_query):
|
|
266
|
+
layer_inline_name = f"_inline_query_{chart_id}_layer{i}"
|
|
267
|
+
query_registry[layer_inline_name] = normalize_query(
|
|
268
|
+
layer_inline_name,
|
|
269
|
+
{"sql": layer_query},
|
|
270
|
+
default_source=default_source,
|
|
271
|
+
)
|
|
272
|
+
layer["query"] = layer_inline_name
|
|
273
|
+
else:
|
|
274
|
+
raise ReferenceError(
|
|
275
|
+
layer_query, f"chart '{chart_id}', layer {i}"
|
|
276
|
+
)
|
|
277
|
+
validated_layers.append(Layer(**layer))
|
|
278
|
+
chart_dict["layers"] = validated_layers
|
|
279
|
+
|
|
280
|
+
# Donut → pie alias: donut is just a pie with style.inner_radius > 0.
|
|
281
|
+
# After the per-family authored style move, PieChartStylePatch.inner_radius
|
|
282
|
+
# is a flat field (no "pie" sub-key). Inject directly into the style dict.
|
|
283
|
+
# 0.6 = ~64% ring area / ~36% hole area, putting the data ring as the
|
|
284
|
+
# dominant element while leaving room for a center total — the editorial-
|
|
285
|
+
# donut sweet spot. Authors override via explicit style.inner_radius.
|
|
286
|
+
if chart_dict.get("type") == "donut":
|
|
287
|
+
chart_dict["type"] = "pie"
|
|
288
|
+
style = chart_dict.setdefault("style", {})
|
|
289
|
+
if style.get("inner_radius") is None:
|
|
290
|
+
style["inner_radius"] = 0.6
|
|
291
|
+
|
|
292
|
+
# Promote sizing sentinels from per-chart style to chart root.
|
|
293
|
+
# aspect_ratio, min_height, max_height are authored in style: but stored as
|
|
294
|
+
# flat fields on compiled Chart for sizing.py to read directly (no style walk).
|
|
295
|
+
# For single-family charts the fields live in the per-family patch (e.g.
|
|
296
|
+
# BarChartStylePatch); for layered charts they live at the top of
|
|
297
|
+
# ChartStylePatch. In both cases the raw dict has them at style[key] at
|
|
298
|
+
# this point, so the pop loop is uniform.
|
|
299
|
+
# kpi and table patch models exclude these fields, so they can never reach
|
|
300
|
+
# this step for those types; the promotion runs unconditionally for all others.
|
|
301
|
+
if isinstance(chart_dict.get("style"), dict):
|
|
302
|
+
flat_style = chart_dict["style"]
|
|
303
|
+
for _sizing_key in ("aspect_ratio", "min_height", "max_height"):
|
|
304
|
+
if _sizing_key in flat_style:
|
|
305
|
+
chart_dict[_sizing_key] = flat_style.pop(_sizing_key)
|
|
306
|
+
|
|
307
|
+
# Resolve query reference
|
|
308
|
+
query_value = chart_dict.get("query")
|
|
309
|
+
query_is_inline = False
|
|
310
|
+
|
|
311
|
+
# Convert the per-family flat authored style dict to the monolithic
|
|
312
|
+
# ChartStylePatch shape expected by compiled Chart.style, then validate.
|
|
313
|
+
if chart_dict.get("style") and isinstance(chart_dict["style"], dict):
|
|
314
|
+
monolithic = _wrap_authored_style_for_compiled(
|
|
315
|
+
chart_dict.get("type"), chart_dict["style"]
|
|
316
|
+
)
|
|
317
|
+
chart_dict["style"] = ChartStylePatch.model_validate(monolithic)
|
|
318
|
+
|
|
319
|
+
# Handle inline query definitions (query is a dict, not a string reference)
|
|
320
|
+
if query_value is None:
|
|
321
|
+
# Blank chart with no query - this is valid for placeholder charts
|
|
322
|
+
chart_dict["query"] = None
|
|
323
|
+
chart_dict["query_name"] = None
|
|
324
|
+
chart_dict["source_path"] = source_path
|
|
325
|
+
chart_dict["query_is_inline"] = False
|
|
326
|
+
return Chart(**chart_dict)
|
|
327
|
+
elif isinstance(query_value, dict):
|
|
328
|
+
# Create an inline query in the registry
|
|
329
|
+
inline_query_name = f"_inline_query_{chart_id}"
|
|
330
|
+
normalized_query = normalize_query(
|
|
331
|
+
inline_query_name, query_value, default_source=default_source
|
|
332
|
+
)
|
|
333
|
+
query_registry[inline_query_name] = normalized_query
|
|
334
|
+
query_ref = inline_query_name
|
|
335
|
+
query_is_inline = True
|
|
336
|
+
elif isinstance(query_value, str):
|
|
337
|
+
# String reference to existing query
|
|
338
|
+
query_ref = query_value
|
|
339
|
+
if query_ref.startswith("queries."):
|
|
340
|
+
query_ref = query_ref[8:]
|
|
341
|
+
|
|
342
|
+
# Check for external file reference (file.yml#query_name)
|
|
343
|
+
if "#" in query_ref:
|
|
344
|
+
query_ref, query_registry = resolve_external_query(
|
|
345
|
+
query_ref, query_registry, base_path
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
if query_ref not in query_registry:
|
|
349
|
+
if looks_like_sql(query_ref):
|
|
350
|
+
# Promote bare SQL string to an inline query (same as query: {sql: ...})
|
|
351
|
+
inline_query_name = f"_inline_query_{chart_id}"
|
|
352
|
+
normalized_query = normalize_query(
|
|
353
|
+
inline_query_name, {"sql": query_ref}, default_source=default_source
|
|
354
|
+
)
|
|
355
|
+
query_registry[inline_query_name] = normalized_query
|
|
356
|
+
query_ref = inline_query_name
|
|
357
|
+
query_is_inline = True
|
|
358
|
+
else:
|
|
359
|
+
raise ReferenceError(query_ref, f"chart '{chart_id}'")
|
|
360
|
+
else:
|
|
361
|
+
raise CompilationError(
|
|
362
|
+
f"Chart '{chart_id}' has invalid query field. Expected string reference or dict, got {type(query_value)}"
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Set query fields
|
|
366
|
+
resolved_query = query_registry[query_ref]
|
|
367
|
+
chart_dict["query"] = resolved_query
|
|
368
|
+
chart_dict["query_name"] = query_ref
|
|
369
|
+
|
|
370
|
+
# Compute variable dependencies (union of own deps + query deps)
|
|
371
|
+
chart_deps: set[str] = set()
|
|
372
|
+
|
|
373
|
+
# Own deps from title/subtitle
|
|
374
|
+
if chart_dict.get("title"):
|
|
375
|
+
chart_deps |= extract_variable_dependencies(chart_dict["title"])
|
|
376
|
+
if chart_dict.get("subtitle"):
|
|
377
|
+
chart_deps |= extract_variable_dependencies(chart_dict["subtitle"])
|
|
378
|
+
|
|
379
|
+
# Inherit query deps
|
|
380
|
+
if resolved_query.variable_dependencies:
|
|
381
|
+
chart_deps |= resolved_query.variable_dependencies
|
|
382
|
+
|
|
383
|
+
chart_dict["variable_dependencies"] = chart_deps
|
|
384
|
+
|
|
385
|
+
# Track source location and query type for edit-back support
|
|
386
|
+
chart_dict["source_path"] = source_path
|
|
387
|
+
chart_dict["query_is_inline"] = query_is_inline
|
|
388
|
+
|
|
389
|
+
chart = Chart(**chart_dict)
|
|
390
|
+
|
|
391
|
+
# Extract filter deps from typed FilterDef instances — single source of shape truth.
|
|
392
|
+
# Done post-validation so we read .var/.template directly without re-parsing raw forms.
|
|
393
|
+
if chart.filters:
|
|
394
|
+
filter_deps: set[str] = set()
|
|
395
|
+
for fd in chart.filters.values():
|
|
396
|
+
if fd.var is not None:
|
|
397
|
+
filter_deps.add(fd.var)
|
|
398
|
+
elif fd.template is not None:
|
|
399
|
+
filter_deps |= extract_variable_dependencies(fd.template)
|
|
400
|
+
if filter_deps:
|
|
401
|
+
chart = chart.model_copy(
|
|
402
|
+
update={"variable_dependencies": chart_deps | filter_deps}
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# Resolve palette tokens in conditional_formatting color fields.
|
|
406
|
+
# ConditionalRule.background and .font.color are authored color literals
|
|
407
|
+
# that live outside the style tree, so they don't go through resolve_style().
|
|
408
|
+
# Walk them here, at compile time, so the renderer receives hex values.
|
|
409
|
+
if chart.conditional_formatting:
|
|
410
|
+
from dataface.core.compile.models.style.merged import _resolve_color_tokens
|
|
411
|
+
|
|
412
|
+
resolved_cf = {
|
|
413
|
+
col: _resolve_color_tokens(entry)
|
|
414
|
+
for col, entry in chart.conditional_formatting.items()
|
|
415
|
+
}
|
|
416
|
+
chart = chart.model_copy(update={"conditional_formatting": resolved_cf})
|
|
417
|
+
|
|
418
|
+
return chart
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _generate_inline_chart_id(chart_def: Any, fallback_id: str, used_ids: set) -> str:
|
|
422
|
+
"""Generate a unique chart ID for an inline chart definition.
|
|
423
|
+
|
|
424
|
+
Uses the chart's authored display text (``label`` for KPI, ``title``
|
|
425
|
+
otherwise) if available; falls back to the provided ID. Handles
|
|
426
|
+
duplicates by appending a number.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
chart_def: The chart definition dict
|
|
430
|
+
fallback_id: Fallback ID to use if no title (e.g., "row0")
|
|
431
|
+
used_ids: Set of already-used chart IDs (will be mutated)
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
A unique chart ID
|
|
435
|
+
"""
|
|
436
|
+
|
|
437
|
+
# KPI charts carry their authored display text in ``label`` (the slot
|
|
438
|
+
# rendered above the headline value); every other chart type uses
|
|
439
|
+
# ``title``. The auto-derive in _normalize_chart runs *after* this
|
|
440
|
+
# helper picks the inline ID, so we must read the right slot here too.
|
|
441
|
+
if isinstance(chart_def, dict):
|
|
442
|
+
raw_type = chart_def.get("type")
|
|
443
|
+
chart_type = (
|
|
444
|
+
CHART_TYPE_ALIASES.get(raw_type, raw_type)
|
|
445
|
+
if isinstance(raw_type, str)
|
|
446
|
+
else raw_type
|
|
447
|
+
)
|
|
448
|
+
title = (
|
|
449
|
+
chart_def.get("label") if chart_type == "kpi" else chart_def.get("title")
|
|
450
|
+
)
|
|
451
|
+
else:
|
|
452
|
+
chart_type = getattr(chart_def, "type", None)
|
|
453
|
+
if chart_type == "kpi":
|
|
454
|
+
title = getattr(chart_def, "label", None)
|
|
455
|
+
else:
|
|
456
|
+
title = getattr(chart_def, "title", None)
|
|
457
|
+
|
|
458
|
+
# Generate base slug
|
|
459
|
+
base_slug = _title_to_slug(title) if title else fallback_id
|
|
460
|
+
|
|
461
|
+
# Ensure uniqueness
|
|
462
|
+
if base_slug not in used_ids:
|
|
463
|
+
used_ids.add(base_slug)
|
|
464
|
+
return base_slug
|
|
465
|
+
|
|
466
|
+
# Append number to make unique
|
|
467
|
+
counter = 2
|
|
468
|
+
while f"{base_slug}-{counter}" in used_ids:
|
|
469
|
+
counter += 1
|
|
470
|
+
unique_id = f"{base_slug}-{counter}"
|
|
471
|
+
used_ids.add(unique_id)
|
|
472
|
+
return unique_id
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _title_to_slug(title: str) -> str:
|
|
476
|
+
"""Convert a title to a URL-friendly slug.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
title: Display title (e.g., "My First Chart")
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Slug string (e.g., "my-first-chart")
|
|
483
|
+
"""
|
|
484
|
+
# Convert to lowercase
|
|
485
|
+
slug = title.lower()
|
|
486
|
+
# Replace spaces and underscores with hyphens
|
|
487
|
+
slug = slug.replace(" ", "-").replace("_", "-")
|
|
488
|
+
# Remove any characters that aren't alphanumeric or hyphens
|
|
489
|
+
slug = re.sub(r"[^a-z0-9-]", "", slug)
|
|
490
|
+
# Collapse multiple hyphens
|
|
491
|
+
slug = re.sub(r"-+", "-", slug)
|
|
492
|
+
# Strip leading/trailing hyphens
|
|
493
|
+
slug = slug.strip("-")
|
|
494
|
+
return slug or "chart"
|