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,268 @@
|
|
|
1
|
+
"""Standard chart type Vega-Lite spec generators.
|
|
2
|
+
|
|
3
|
+
Each generator receives a ``MappedChart`` from ``profile.py`` and performs
|
|
4
|
+
mechanical Vega-Lite spec assembly — dimensions, title, tooltips. No
|
|
5
|
+
Dataface/VL encoding divergence logic lives here; that belongs in profile.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from dataface.core.compile.chart_resolved import ResolvedChart
|
|
13
|
+
from dataface.core.render.chart.profile import MappedChart
|
|
14
|
+
from dataface.core.render.chart.spec_builders import (
|
|
15
|
+
new_chart_spec,
|
|
16
|
+
set_chart_dimensions,
|
|
17
|
+
set_chart_title,
|
|
18
|
+
tooltip_entry,
|
|
19
|
+
)
|
|
20
|
+
from dataface.core.render.format_utils import resolve_format
|
|
21
|
+
from dataface.core.render.text.case import format_display_text
|
|
22
|
+
from dataface.core.render.utils import normalize_data_types
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _generate_layered_spec(
|
|
26
|
+
mapped: MappedChart,
|
|
27
|
+
chart: ResolvedChart,
|
|
28
|
+
data: list[dict[str, Any]],
|
|
29
|
+
width: float | None = None,
|
|
30
|
+
height: float | None = None,
|
|
31
|
+
min_height: float | None = None,
|
|
32
|
+
) -> dict[str, Any]:
|
|
33
|
+
"""Assemble layered chart spec from the profile-mapped layered contract."""
|
|
34
|
+
if mapped.data_override is not None:
|
|
35
|
+
data = mapped.data_override
|
|
36
|
+
data = normalize_data_types(data)
|
|
37
|
+
tooltip_number_format = resolve_format(
|
|
38
|
+
chart.resolved_style.tooltip.format, chart.resolved_style.formats
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
has_datasets = mapped.datasets is not None
|
|
42
|
+
|
|
43
|
+
# Collect y fields: from chart.y (multi-metric) or from layers (type: layered)
|
|
44
|
+
y_fields = chart.y or []
|
|
45
|
+
if not isinstance(y_fields, list):
|
|
46
|
+
y_fields = [y_fields]
|
|
47
|
+
if not y_fields and chart.layers:
|
|
48
|
+
y_fields = [layer.y for layer in chart.layers if layer.y]
|
|
49
|
+
|
|
50
|
+
# When layers use different datasets, there's no shared x field for
|
|
51
|
+
# a combined tooltip. Fall back to per-layer tooltips.
|
|
52
|
+
x_field = mapped.encoding.get("x", {}).get("field") if not has_datasets else None
|
|
53
|
+
|
|
54
|
+
# Build tooltip showing all metrics at once (only for shared-data layers)
|
|
55
|
+
tooltip_fields: list[dict[str, Any]] = []
|
|
56
|
+
if x_field:
|
|
57
|
+
tooltip_fields.append(
|
|
58
|
+
tooltip_entry(
|
|
59
|
+
x_field,
|
|
60
|
+
mapped.encoding.get("x", {}).get("type", "nominal"),
|
|
61
|
+
title=format_display_text(
|
|
62
|
+
x_field, from_slug=True, font=chart.resolved_style.axis_x.title.font
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
if not has_datasets:
|
|
67
|
+
for metric in y_fields:
|
|
68
|
+
tooltip_fields.append(
|
|
69
|
+
tooltip_entry(
|
|
70
|
+
metric,
|
|
71
|
+
"quantitative",
|
|
72
|
+
title=format_display_text(
|
|
73
|
+
metric,
|
|
74
|
+
from_slug=True,
|
|
75
|
+
font=chart.resolved_style.axis_y.title.font,
|
|
76
|
+
),
|
|
77
|
+
format=tooltip_number_format,
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Normalize datasets values when present
|
|
82
|
+
normalized_datasets = None
|
|
83
|
+
if mapped.datasets:
|
|
84
|
+
normalized_datasets = {
|
|
85
|
+
name: normalize_data_types(rows) for name, rows in mapped.datasets.items()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
spec = new_chart_spec(data, layer=True, datasets=normalized_datasets)
|
|
89
|
+
set_chart_dimensions(spec, width, height, min_height=min_height)
|
|
90
|
+
|
|
91
|
+
if mapped.encoding:
|
|
92
|
+
spec["encoding"] = dict(mapped.encoding)
|
|
93
|
+
|
|
94
|
+
# Prevents future-date rows in secondary layers from stretching the axis
|
|
95
|
+
# beyond the primary query's range (Looker merged-result visual match).
|
|
96
|
+
first_dataset_name: str | None = None
|
|
97
|
+
first_layer_x_field: str | None = None
|
|
98
|
+
if chart.x_domain == "primary" and mapped.layers:
|
|
99
|
+
if not has_datasets:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"Chart '{chart.id}': x_domain: primary requires per-layer datasets "
|
|
102
|
+
"but no datasets were resolved. Ensure each layer specifies its own query."
|
|
103
|
+
)
|
|
104
|
+
first_layer = mapped.layers[0]
|
|
105
|
+
first_dataset_name = first_layer.data_name
|
|
106
|
+
if first_dataset_name is None:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"Chart '{chart.id}': x_domain: primary requires the first layer's "
|
|
109
|
+
"dataset to be present, but the first layer has no data_name. "
|
|
110
|
+
"Ensure the first layer's query name appears in the datasets dict."
|
|
111
|
+
)
|
|
112
|
+
first_layer_x_field = first_layer.encoding.get("x", {}).get("field")
|
|
113
|
+
if first_layer_x_field is None:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"Chart '{chart.id}': x_domain: primary requires an x field on "
|
|
116
|
+
"the first layer's encoding, but none was resolved. "
|
|
117
|
+
"Add `x:` to the first layer or the chart level."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
for layer_idx, layer in enumerate(mapped.layers):
|
|
121
|
+
layer_encoding = dict(layer.encoding)
|
|
122
|
+
|
|
123
|
+
# Combined tooltip shows all metrics at the shared x position.
|
|
124
|
+
# Only used when all layers share the same dataset.
|
|
125
|
+
use_combined = (
|
|
126
|
+
not has_datasets
|
|
127
|
+
and chart.chart_type in ("line", "area", "layered")
|
|
128
|
+
and x_field
|
|
129
|
+
and tooltip_fields
|
|
130
|
+
)
|
|
131
|
+
if use_combined:
|
|
132
|
+
layer_encoding["tooltip"] = tooltip_fields
|
|
133
|
+
|
|
134
|
+
# Inject scale.domain override on layer-0's x-encoding when x_domain is
|
|
135
|
+
# "primary". VL resolves the shared x-scale from the first mark's domain,
|
|
136
|
+
# so placing the override here anchors the axis to the primary dataset's
|
|
137
|
+
# x-values and clips secondary layers rather than extending the axis.
|
|
138
|
+
if layer_idx == 0 and first_dataset_name is not None:
|
|
139
|
+
x_enc = dict(layer_encoding.get("x", {}))
|
|
140
|
+
scale = dict(x_enc.get("scale", {}))
|
|
141
|
+
scale["domain"] = {"data": first_dataset_name, "field": first_layer_x_field}
|
|
142
|
+
x_enc["scale"] = scale
|
|
143
|
+
layer_encoding["x"] = x_enc
|
|
144
|
+
|
|
145
|
+
layer_spec: dict[str, Any] = {
|
|
146
|
+
"mark": dict(layer.mark),
|
|
147
|
+
"encoding": layer_encoding,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if layer.transform:
|
|
151
|
+
layer_spec["transform"] = [dict(t) for t in layer.transform]
|
|
152
|
+
|
|
153
|
+
# Per-layer data reference for multi-query layered charts. Inline
|
|
154
|
+
# ``layer.data`` (used by the zero/top-rule layers to halt parent
|
|
155
|
+
# data iteration) takes precedence — a layer that ships its own
|
|
156
|
+
# values shouldn't also pull from a named dataset.
|
|
157
|
+
if layer.data is not None:
|
|
158
|
+
layer_spec["data"] = dict(layer.data)
|
|
159
|
+
elif layer.data_name is not None:
|
|
160
|
+
layer_spec["data"] = {"name": layer.data_name}
|
|
161
|
+
|
|
162
|
+
spec["layer"].append(layer_spec)
|
|
163
|
+
|
|
164
|
+
if mapped.derived_resolve:
|
|
165
|
+
spec["resolve"] = mapped.derived_resolve
|
|
166
|
+
|
|
167
|
+
set_chart_title(spec, chart.title, chart.subtitle, style=chart.resolved_style)
|
|
168
|
+
|
|
169
|
+
return spec
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _generate_histogram_spec(
|
|
173
|
+
mapped: MappedChart,
|
|
174
|
+
chart: ResolvedChart,
|
|
175
|
+
data: list[dict[str, Any]],
|
|
176
|
+
width: float | None = None,
|
|
177
|
+
height: float | None = None,
|
|
178
|
+
) -> dict[str, Any]:
|
|
179
|
+
"""Assemble histogram spec from profile-mapped mark and encoding."""
|
|
180
|
+
data = normalize_data_types(data)
|
|
181
|
+
|
|
182
|
+
spec = new_chart_spec(data, mark=mapped.mark)
|
|
183
|
+
set_chart_dimensions(spec, width, height, min_height=100)
|
|
184
|
+
spec["encoding"] = dict(mapped.encoding)
|
|
185
|
+
|
|
186
|
+
set_chart_title(spec, chart.title, chart.subtitle, style=chart.resolved_style)
|
|
187
|
+
return spec
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _generate_arc_spec(
|
|
191
|
+
mapped: MappedChart,
|
|
192
|
+
chart: ResolvedChart,
|
|
193
|
+
data: list[dict[str, Any]],
|
|
194
|
+
width: float | None = None,
|
|
195
|
+
height: float | None = None,
|
|
196
|
+
) -> dict[str, Any]:
|
|
197
|
+
"""Assemble arc/pie spec from profile-mapped mark and encoding."""
|
|
198
|
+
if mapped.layers:
|
|
199
|
+
# Donut center total (and future per-slice labels) emit arc + text
|
|
200
|
+
# layers; defer to the shared layered assembler. ``min_height=100``
|
|
201
|
+
# matches the non-layered arc path's contract (small donuts otherwise
|
|
202
|
+
# collapse into unreadable blobs).
|
|
203
|
+
return _generate_layered_spec(
|
|
204
|
+
mapped, chart, data, width, height, min_height=100
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
data = normalize_data_types(data)
|
|
208
|
+
|
|
209
|
+
spec = new_chart_spec(data, mark=mapped.mark)
|
|
210
|
+
set_chart_dimensions(spec, width, height, min_height=100)
|
|
211
|
+
spec["encoding"] = dict(mapped.encoding)
|
|
212
|
+
|
|
213
|
+
set_chart_title(spec, chart.title, chart.subtitle, style=chart.resolved_style)
|
|
214
|
+
return spec
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _generate_rect_spec(
|
|
218
|
+
mapped: MappedChart,
|
|
219
|
+
chart: ResolvedChart,
|
|
220
|
+
data: list[dict[str, Any]],
|
|
221
|
+
width: float | None = None,
|
|
222
|
+
height: float | None = None,
|
|
223
|
+
) -> dict[str, Any]:
|
|
224
|
+
"""Assemble rect/square/heatmap spec from profile-mapped mark and encoding."""
|
|
225
|
+
data = normalize_data_types(data)
|
|
226
|
+
|
|
227
|
+
spec = new_chart_spec(data, mark=mapped.mark)
|
|
228
|
+
set_chart_dimensions(spec, width, height, min_width=150, min_height=100)
|
|
229
|
+
spec["encoding"] = dict(mapped.encoding)
|
|
230
|
+
|
|
231
|
+
set_chart_title(spec, chart.title, chart.subtitle, style=chart.resolved_style)
|
|
232
|
+
return spec
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _generate_boxplot_spec(
|
|
236
|
+
mapped: MappedChart,
|
|
237
|
+
chart: ResolvedChart,
|
|
238
|
+
data: list[dict[str, Any]],
|
|
239
|
+
width: float | None = None,
|
|
240
|
+
height: float | None = None,
|
|
241
|
+
) -> dict[str, Any]:
|
|
242
|
+
"""Assemble boxplot spec from profile-mapped mark and encoding."""
|
|
243
|
+
data = normalize_data_types(data)
|
|
244
|
+
|
|
245
|
+
spec = new_chart_spec(data, mark=mapped.mark)
|
|
246
|
+
set_chart_dimensions(spec, width, height)
|
|
247
|
+
spec["encoding"] = dict(mapped.encoding)
|
|
248
|
+
|
|
249
|
+
set_chart_title(spec, chart.title, chart.subtitle, style=chart.resolved_style)
|
|
250
|
+
return spec
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _generate_error_spec(
|
|
254
|
+
mapped: MappedChart,
|
|
255
|
+
chart: ResolvedChart,
|
|
256
|
+
data: list[dict[str, Any]],
|
|
257
|
+
width: float | None = None,
|
|
258
|
+
height: float | None = None,
|
|
259
|
+
) -> dict[str, Any]:
|
|
260
|
+
"""Assemble errorbar/errorband spec from profile-mapped mark and encoding."""
|
|
261
|
+
data = normalize_data_types(data)
|
|
262
|
+
|
|
263
|
+
spec = new_chart_spec(data, mark=mapped.mark)
|
|
264
|
+
set_chart_dimensions(spec, width, height)
|
|
265
|
+
spec["encoding"] = dict(mapped.encoding)
|
|
266
|
+
|
|
267
|
+
set_chart_title(spec, chart.title, chart.subtitle, style=chart.resolved_style)
|
|
268
|
+
return spec
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Vega-Lite field name maps and helper for snake_case → camelCase mapping."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
SCALE_FIELD_MAP: dict[str, str] = {
|
|
10
|
+
# ``paddingInner`` / ``paddingOuter`` are the encoding-level VL field names
|
|
11
|
+
# for band-scale gutter and edge padding. The ``bandPaddingInner`` /
|
|
12
|
+
# ``bandPaddingOuter`` variants only work at the global config level
|
|
13
|
+
# (``config.scale.bandPaddingInner``) — they are silently ignored when
|
|
14
|
+
# placed on ``encoding.{x,y}.scale``.
|
|
15
|
+
"band_padding_inner": "paddingInner",
|
|
16
|
+
"band_padding_outer": "paddingOuter",
|
|
17
|
+
"point_padding": "pointPadding",
|
|
18
|
+
"rect_band_padding_inner": "rectBandPaddingInner",
|
|
19
|
+
"round": "round",
|
|
20
|
+
"zero": "zero",
|
|
21
|
+
"clamp": "clamp",
|
|
22
|
+
"continuous_padding": "continuousPadding",
|
|
23
|
+
"quantile_count": "quantileCount",
|
|
24
|
+
"quantize_count": "quantizeCount",
|
|
25
|
+
"use_unaggregated_domain": "useUnaggregatedDomain",
|
|
26
|
+
"x_reverse": "xReverse",
|
|
27
|
+
"type": "type",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Encoding-level-only scale fields applied to encoding.x.scale / encoding.y.scale.
|
|
31
|
+
SCALE_ENCODING_FIELD_MAP: dict[str, str] = {
|
|
32
|
+
"domain": "domain",
|
|
33
|
+
"nice": "nice",
|
|
34
|
+
# Unified shorthand. VL dispatches based on scale type (bandPaddingOuter,
|
|
35
|
+
# pointPadding, continuousPadding). Encoding-level only.
|
|
36
|
+
"padding": "padding",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
VIEW_FIELD_MAP: dict[str, str] = {
|
|
40
|
+
"fill": "fill",
|
|
41
|
+
"stroke": "stroke",
|
|
42
|
+
"stroke_width": "strokeWidth",
|
|
43
|
+
"continuous_width": "continuousWidth",
|
|
44
|
+
"continuous_height": "continuousHeight",
|
|
45
|
+
"corner_radius": "cornerRadius",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
from typing import Any
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _n(obj: Any, *path: str) -> Any:
|
|
53
|
+
"""Get nested attribute, returning None if any level is None or missing."""
|
|
54
|
+
for attr in path:
|
|
55
|
+
if obj is None:
|
|
56
|
+
return None
|
|
57
|
+
obj = getattr(obj, attr, None)
|
|
58
|
+
return obj
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _dasharray_to_vl(dasharray: str) -> list[float]:
|
|
62
|
+
"""Convert SVG stroke-dasharray string (e.g. '4 2') to VL strokeDash number array."""
|
|
63
|
+
return [float(x) for x in dasharray.split()]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _stroke_to_vl(stroke: Any) -> dict[str, Any]:
|
|
67
|
+
"""Map a StrokeStyle → VL stroke properties (non-None fields only).
|
|
68
|
+
|
|
69
|
+
Single canonical primitive shared by all per-mark mappers and halo
|
|
70
|
+
path builders so new StrokeStyle fields automatically flow everywhere.
|
|
71
|
+
"""
|
|
72
|
+
d: dict[str, Any] = {}
|
|
73
|
+
if (v := _n(stroke, "color")) is not None:
|
|
74
|
+
d["stroke"] = v
|
|
75
|
+
if (v := _n(stroke, "width")) is not None:
|
|
76
|
+
d["strokeWidth"] = v
|
|
77
|
+
if (v := _n(stroke, "cap")) is not None:
|
|
78
|
+
d["strokeCap"] = v
|
|
79
|
+
if (v := _n(stroke, "join")) is not None:
|
|
80
|
+
d["strokeJoin"] = v
|
|
81
|
+
if (v := _n(stroke, "dasharray")) is not None:
|
|
82
|
+
d["strokeDash"] = _dasharray_to_vl(v)
|
|
83
|
+
return d
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def axis_to_vl(axis: Any) -> dict[str, Any]:
|
|
87
|
+
"""Map any axis style object → VL axis config dict (non-None fields only).
|
|
88
|
+
|
|
89
|
+
Works for AxisStyle (all fields concrete, always emitted) and
|
|
90
|
+
AxisStylePatch (skip None). Single canonical mapper for all axis
|
|
91
|
+
variants — theme global/channel/type-conditional and chart-local patches.
|
|
92
|
+
"""
|
|
93
|
+
if axis is None:
|
|
94
|
+
return {}
|
|
95
|
+
d: dict[str, Any] = {}
|
|
96
|
+
|
|
97
|
+
# Label font (nested via label.font)
|
|
98
|
+
if (v := _n(axis, "label", "font", "color")) is not None:
|
|
99
|
+
d["labelColor"] = v
|
|
100
|
+
if (v := _n(axis, "label", "font", "family")) is not None:
|
|
101
|
+
d["labelFont"] = v
|
|
102
|
+
if (v := _n(axis, "label", "font", "size")) is not None:
|
|
103
|
+
d["labelFontSize"] = v
|
|
104
|
+
if (v := _n(axis, "label", "font", "weight")) is not None:
|
|
105
|
+
d["labelFontWeight"] = v
|
|
106
|
+
if (v := _n(axis, "label", "padding")) is not None:
|
|
107
|
+
d["labelPadding"] = v
|
|
108
|
+
if (v := _n(axis, "label", "max_width")) is not None:
|
|
109
|
+
d["labelLimit"] = v
|
|
110
|
+
if (v := _n(axis, "label", "angle")) is not None:
|
|
111
|
+
d["labelAngle"] = v
|
|
112
|
+
if (v := _n(axis, "label", "align")) is not None:
|
|
113
|
+
d["labelAlign"] = v
|
|
114
|
+
if (v := _n(axis, "label", "baseline")) is not None:
|
|
115
|
+
d["labelBaseline"] = v
|
|
116
|
+
# "smart" → omit labelOverlap (VL applies per-scale adaptive default).
|
|
117
|
+
# "allow" → false (no reduction; labels may overlap).
|
|
118
|
+
# "parity" / "greedy" → pass through as-is.
|
|
119
|
+
_overlap = _n(axis, "label", "overlap")
|
|
120
|
+
if _overlap == "allow":
|
|
121
|
+
d["labelOverlap"] = False
|
|
122
|
+
elif _overlap in ("parity", "greedy"):
|
|
123
|
+
d["labelOverlap"] = _overlap
|
|
124
|
+
if (v := _n(axis, "label", "separation")) is not None:
|
|
125
|
+
d["labelSeparation"] = v
|
|
126
|
+
if (v := _n(axis, "label", "visible")) is not None:
|
|
127
|
+
d["labels"] = v
|
|
128
|
+
if (v := _n(axis, "label", "expr")) is not None:
|
|
129
|
+
d["labelExpr"] = v
|
|
130
|
+
if (v := _n(axis, "label", "bound")) is not None:
|
|
131
|
+
d["labelBound"] = v
|
|
132
|
+
if (v := _n(axis, "label", "flush")) is not None:
|
|
133
|
+
d["labelFlush"] = v
|
|
134
|
+
if (v := _n(axis, "label", "offset")) is not None:
|
|
135
|
+
d["labelOffset"] = v
|
|
136
|
+
if (v := _n(axis, "label", "line_height")) is not None:
|
|
137
|
+
d["labelLineHeight"] = v
|
|
138
|
+
if (v := _n(axis, "label", "anchor")) is not None:
|
|
139
|
+
d["labelAnchor"] = v
|
|
140
|
+
|
|
141
|
+
# Title font
|
|
142
|
+
if (v := _n(axis, "title", "font", "color")) is not None:
|
|
143
|
+
d["titleColor"] = v
|
|
144
|
+
if (v := _n(axis, "title", "font", "family")) is not None:
|
|
145
|
+
d["titleFont"] = v
|
|
146
|
+
if (v := _n(axis, "title", "font", "size")) is not None:
|
|
147
|
+
d["titleFontSize"] = v
|
|
148
|
+
if (v := _n(axis, "title", "font", "weight")) is not None:
|
|
149
|
+
d["titleFontWeight"] = v
|
|
150
|
+
if (v := _n(axis, "title", "padding")) is not None:
|
|
151
|
+
d["titlePadding"] = v
|
|
152
|
+
|
|
153
|
+
# Grid
|
|
154
|
+
if (v := _n(axis, "grid", "visible")) is not None:
|
|
155
|
+
d["grid"] = v
|
|
156
|
+
if (v := _n(axis, "grid", "opacity")) is not None:
|
|
157
|
+
d["gridOpacity"] = v
|
|
158
|
+
if (v := _n(axis, "grid", "width")) is not None:
|
|
159
|
+
d["gridWidth"] = v
|
|
160
|
+
if (v := _n(axis, "grid", "color")) is not None:
|
|
161
|
+
d["gridColor"] = v
|
|
162
|
+
if (v := _n(axis, "grid", "dash")) is not None:
|
|
163
|
+
d["gridDash"] = v
|
|
164
|
+
|
|
165
|
+
# Domain
|
|
166
|
+
if (v := _n(axis, "domain", "visible")) is not None:
|
|
167
|
+
d["domain"] = v
|
|
168
|
+
if (v := _n(axis, "domain", "width")) is not None:
|
|
169
|
+
d["domainWidth"] = v
|
|
170
|
+
if (v := _n(axis, "domain", "color")) is not None:
|
|
171
|
+
d["domainColor"] = v
|
|
172
|
+
|
|
173
|
+
# Ticks
|
|
174
|
+
if (v := _n(axis, "ticks", "visible")) is not None:
|
|
175
|
+
d["ticks"] = v
|
|
176
|
+
if (v := _n(axis, "ticks", "color")) is not None:
|
|
177
|
+
d["tickColor"] = v
|
|
178
|
+
if (v := _n(axis, "ticks", "size")) is not None:
|
|
179
|
+
d["tickSize"] = v
|
|
180
|
+
if (v := _n(axis, "ticks", "width")) is not None:
|
|
181
|
+
d["tickWidth"] = v
|
|
182
|
+
|
|
183
|
+
# Flat fields
|
|
184
|
+
if (v := _n(axis, "orient")) is not None:
|
|
185
|
+
d["orient"] = v
|
|
186
|
+
if (v := _n(axis, "offset")) is not None:
|
|
187
|
+
d["offset"] = v
|
|
188
|
+
if (v := _n(axis, "band_position")) is not None:
|
|
189
|
+
d["bandPosition"] = v
|
|
190
|
+
if (v := _n(axis, "format")) is not None:
|
|
191
|
+
d["format"] = v
|
|
192
|
+
if (v := _n(axis, "values")) is not None:
|
|
193
|
+
d["values"] = v
|
|
194
|
+
|
|
195
|
+
return d
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def legend_to_vl(legend: Any) -> dict[str, Any] | None:
|
|
199
|
+
"""Map any legend style object → VL legend config dict (non-None fields only).
|
|
200
|
+
|
|
201
|
+
Returns None when disable=True (VL legend: null disables the legend entirely).
|
|
202
|
+
Works for LegendStyle and LegendStylePatch.
|
|
203
|
+
"""
|
|
204
|
+
if legend is None:
|
|
205
|
+
return {}
|
|
206
|
+
if _n(legend, "disable") is True:
|
|
207
|
+
return None # VL: encoding.color.legend = null
|
|
208
|
+
|
|
209
|
+
d: dict[str, Any] = {}
|
|
210
|
+
if (v := _n(legend, "orient")) is not None:
|
|
211
|
+
d["orient"] = v
|
|
212
|
+
if (v := _n(legend, "direction")) is not None:
|
|
213
|
+
d["direction"] = v
|
|
214
|
+
if (v := _n(legend, "label", "font", "color")) is not None:
|
|
215
|
+
d["labelColor"] = v
|
|
216
|
+
if (v := _n(legend, "label", "font", "family")) is not None:
|
|
217
|
+
d["labelFont"] = v
|
|
218
|
+
if (v := _n(legend, "label", "font", "size")) is not None:
|
|
219
|
+
d["labelFontSize"] = v
|
|
220
|
+
if (v := _n(legend, "label", "font", "weight")) is not None:
|
|
221
|
+
d["labelFontWeight"] = v
|
|
222
|
+
if (v := _n(legend, "title", "font", "color")) is not None:
|
|
223
|
+
d["titleColor"] = v
|
|
224
|
+
if (v := _n(legend, "title", "font", "family")) is not None:
|
|
225
|
+
d["titleFont"] = v
|
|
226
|
+
if (v := _n(legend, "title", "font", "size")) is not None:
|
|
227
|
+
d["titleFontSize"] = v
|
|
228
|
+
if (v := _n(legend, "title", "font", "weight")) is not None:
|
|
229
|
+
d["titleFontWeight"] = v
|
|
230
|
+
return d
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# Per-type mark mappers are intentionally separate: each chart type exposes a different
|
|
234
|
+
# subset of mark properties with different VL field names (e.g. bar uses
|
|
235
|
+
# continuousBandSize, line uses interpolate, scatter uses size). A single generic mapper
|
|
236
|
+
# would need to union all fields and conditionally skip inapplicable ones — more complex
|
|
237
|
+
# with no benefit since callers always know the chart type.
|
|
238
|
+
def bar_mark_to_vl(
|
|
239
|
+
bar: Any, orientation: Literal["vertical", "horizontal"]
|
|
240
|
+
) -> dict[str, Any]:
|
|
241
|
+
"""Map BarStyle/Patch → VL extended mark dict (non-None only).
|
|
242
|
+
|
|
243
|
+
``orientation`` controls which dimension receives the band fraction:
|
|
244
|
+
- ``"vertical"``: ``mark.width = {"band": band_width}`` — x categorical band.
|
|
245
|
+
- ``"horizontal"``: ``mark.height = {"band": band_width}`` — y categorical band.
|
|
246
|
+
"""
|
|
247
|
+
if bar is None:
|
|
248
|
+
return {}
|
|
249
|
+
d: dict[str, Any] = {}
|
|
250
|
+
if (v := _n(bar, "border", "radius")) is not None:
|
|
251
|
+
# Round only the value-end corners (top for vertical bars, right for
|
|
252
|
+
# horizontal). Rounding all four corners carves wedges out of the
|
|
253
|
+
# baseline-meeting edge that expose chart background through the bold
|
|
254
|
+
# zero rule.
|
|
255
|
+
d["cornerRadiusEnd"] = v
|
|
256
|
+
if (v := _n(bar, "border", "color")) is not None:
|
|
257
|
+
d["stroke"] = v
|
|
258
|
+
if (v := _n(bar, "border", "width")) is not None:
|
|
259
|
+
d["strokeWidth"] = v
|
|
260
|
+
if (v := _n(bar, "size")) is not None:
|
|
261
|
+
d["continuousBandSize"] = v
|
|
262
|
+
if (v := _n(bar, "band_width")) is not None:
|
|
263
|
+
if orientation == "horizontal":
|
|
264
|
+
d["height"] = {"band": v}
|
|
265
|
+
else:
|
|
266
|
+
d["width"] = {"band": v}
|
|
267
|
+
return d
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _emit_point_mark(point: Any) -> dict[str, Any]:
|
|
271
|
+
"""Build a VL point mark config dict from a MarkPointStyle/Patch (non-None fields only)."""
|
|
272
|
+
d: dict[str, Any] = {}
|
|
273
|
+
if (v := _n(point, "size")) is not None:
|
|
274
|
+
d["size"] = v
|
|
275
|
+
if (v := _n(point, "color")) is not None:
|
|
276
|
+
d["color"] = v
|
|
277
|
+
if (v := _n(point, "shape")) is not None:
|
|
278
|
+
d["shape"] = v
|
|
279
|
+
if (v := _n(point, "opacity")) is not None:
|
|
280
|
+
d["opacity"] = v
|
|
281
|
+
filled = _n(point, "filled")
|
|
282
|
+
if filled is not None:
|
|
283
|
+
d["filled"] = filled
|
|
284
|
+
# fill only applies when filled=false; encoding.color owns the fill channel otherwise.
|
|
285
|
+
if filled is False and (v := _n(point, "fill")) is not None:
|
|
286
|
+
d["fill"] = v
|
|
287
|
+
return d
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def line_mark_to_vl(line: Any, point: Any = None) -> dict[str, Any]:
|
|
291
|
+
"""Map LineMarkStyle → VL extended mark dict (non-None only).
|
|
292
|
+
|
|
293
|
+
``point`` is an optional PointMarkStyle; when provided and point.size > 0,
|
|
294
|
+
emits a VL point overlay on the line.
|
|
295
|
+
"""
|
|
296
|
+
if line is None:
|
|
297
|
+
return {}
|
|
298
|
+
d: dict[str, Any] = _stroke_to_vl(_n(line, "stroke"))
|
|
299
|
+
if (v := _n(line, "curve")) is not None:
|
|
300
|
+
d["interpolate"] = v
|
|
301
|
+
if point is not None:
|
|
302
|
+
pt_size = _n(point, "size")
|
|
303
|
+
if pt_size is not None and pt_size > 0:
|
|
304
|
+
d["point"] = _emit_point_mark(point)
|
|
305
|
+
return d
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def area_mark_to_vl(area: Any) -> dict[str, Any]:
|
|
309
|
+
"""Map AreaStyle/Patch → VL extended mark dict (non-None only)."""
|
|
310
|
+
if area is None:
|
|
311
|
+
return {}
|
|
312
|
+
d: dict[str, Any] = {}
|
|
313
|
+
if (v := _n(area, "opacity")) is not None:
|
|
314
|
+
d["opacity"] = v
|
|
315
|
+
d.update(_stroke_to_vl(_n(area, "stroke")))
|
|
316
|
+
return d
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def scatter_mark_to_vl(point: Any) -> dict[str, Any]:
|
|
320
|
+
"""Map PointMarkStyle → VL extended mark dict (non-None only)."""
|
|
321
|
+
if point is None:
|
|
322
|
+
return {}
|
|
323
|
+
return _emit_point_mark(point)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def slice_mark_to_vl(arc: Any) -> dict[str, Any]:
|
|
327
|
+
"""Map SliceStyle/Patch → VL extended mark dict (non-None only)."""
|
|
328
|
+
if arc is None:
|
|
329
|
+
return {}
|
|
330
|
+
d: dict[str, Any] = {}
|
|
331
|
+
if (v := _n(arc, "gap")) is not None:
|
|
332
|
+
d["padAngle"] = v
|
|
333
|
+
if (v := _n(arc, "corner_radius")) is not None:
|
|
334
|
+
d["cornerRadius"] = v
|
|
335
|
+
d.update(_stroke_to_vl(_n(arc, "stroke")))
|
|
336
|
+
return d
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def map_fields(source: BaseModel, field_map: dict[str, str]) -> dict[str, object]:
|
|
340
|
+
"""Extract non-None fields from source, mapping snake_case to camelCase."""
|
|
341
|
+
result: dict[str, object] = {}
|
|
342
|
+
for src_name, dst_name in field_map.items():
|
|
343
|
+
value = getattr(source, src_name, None)
|
|
344
|
+
if value is not None:
|
|
345
|
+
result[dst_name] = value
|
|
346
|
+
return result
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Core-owned chart hover interactivity runtime."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from dataface.core.render.script_embedding import embed_svg_script
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from dataface.core.compile.models.style.merged import MergedStyle
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def generate_svg_chart_interactivity_script(
|
|
14
|
+
resolved_style: "MergedStyle",
|
|
15
|
+
) -> str:
|
|
16
|
+
"""Embed the chart hover runtime into SVG output."""
|
|
17
|
+
assert resolved_style is not None
|
|
18
|
+
font_family = str(resolved_style.font.family)
|
|
19
|
+
script_path = (
|
|
20
|
+
Path(__file__).parent / "templates" / "scripts" / "chart_interactivity.js"
|
|
21
|
+
)
|
|
22
|
+
script = script_path.read_text()
|
|
23
|
+
script = script.replace('"__DATAFACE_FONT_FAMILY__"', json.dumps(font_family))
|
|
24
|
+
return embed_svg_script(script)
|