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,287 @@
|
|
|
1
|
+
"""Control template discovery and loading.
|
|
2
|
+
|
|
3
|
+
Stage: RENDER
|
|
4
|
+
Purpose: Discover and render variable control templates.
|
|
5
|
+
|
|
6
|
+
This module provides the ControlRegistry class that:
|
|
7
|
+
1. Discovers control templates from built-in and project directories
|
|
8
|
+
2. Supports flexible directory organization (flat, self-contained, categorized)
|
|
9
|
+
3. Renders templates with Jinja2
|
|
10
|
+
4. Collects CSS/JS assets for used controls
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ControlRegistry:
|
|
24
|
+
"""Registry for variable control templates.
|
|
25
|
+
|
|
26
|
+
Discovers and loads control templates from:
|
|
27
|
+
1. Project controls directory (user customizations) - checked first
|
|
28
|
+
2. Built-in controls (dataface defaults)
|
|
29
|
+
|
|
30
|
+
Supports flexible directory organization:
|
|
31
|
+
- controls/rating.html (flat)
|
|
32
|
+
- controls/rating/rating.html (self-contained)
|
|
33
|
+
- controls/pickers/color/color.html (categorized)
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
registry = ControlRegistry(project_root=Path("/my/project"))
|
|
37
|
+
html = registry.render_control("select", {
|
|
38
|
+
"name": "region",
|
|
39
|
+
"label": "Region",
|
|
40
|
+
"value": "US",
|
|
41
|
+
"options": [{"value": "US", "label": "United States", "selected": True}]
|
|
42
|
+
})
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, project_root: Path | None = None):
|
|
46
|
+
"""Initialize the control registry.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
project_root: Optional project root for loading custom controls.
|
|
50
|
+
If provided, looks for controls in {project_root}/controls/
|
|
51
|
+
"""
|
|
52
|
+
self._project_root = project_root
|
|
53
|
+
self._builtin_dir = Path(__file__).parent / "templates" / "controls"
|
|
54
|
+
self._templates: dict[str, Path] = {}
|
|
55
|
+
self._css: dict[str, str] = {}
|
|
56
|
+
self._js: dict[str, str] = {}
|
|
57
|
+
self._jinja_env: Environment | None = None
|
|
58
|
+
self._used_controls: set[str] = set()
|
|
59
|
+
self._base_css: str | None = None
|
|
60
|
+
|
|
61
|
+
self._load_base_css()
|
|
62
|
+
self._load_builtins()
|
|
63
|
+
if project_root:
|
|
64
|
+
self._load_project_controls(project_root)
|
|
65
|
+
|
|
66
|
+
def _load_base_css(self) -> None:
|
|
67
|
+
"""Load the base styles CSS file."""
|
|
68
|
+
emoji_partial = Path(__file__).parent / "fonts" / "_emoji_font_face.css"
|
|
69
|
+
base_css_path = self._builtin_dir / "_styles.css"
|
|
70
|
+
if base_css_path.exists():
|
|
71
|
+
self._base_css = emoji_partial.read_text() + base_css_path.read_text()
|
|
72
|
+
|
|
73
|
+
def _load_builtins(self) -> None:
|
|
74
|
+
"""Load built-in control templates."""
|
|
75
|
+
if not self._builtin_dir.exists():
|
|
76
|
+
logger.warning(
|
|
77
|
+
f"Built-in controls directory not found: {self._builtin_dir}"
|
|
78
|
+
)
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
for template in self._builtin_dir.glob("*.html"):
|
|
82
|
+
if not template.name.startswith("_"):
|
|
83
|
+
self._register_control(template.stem, template)
|
|
84
|
+
|
|
85
|
+
def _load_project_controls(self, project_root: Path) -> None:
|
|
86
|
+
"""Load controls from project directory with flexible organization.
|
|
87
|
+
|
|
88
|
+
Supports:
|
|
89
|
+
- controls/rating.html (flat)
|
|
90
|
+
- controls/rating/rating.html (self-contained directory)
|
|
91
|
+
- controls/pickers/color/color.html (categorized)
|
|
92
|
+
|
|
93
|
+
Files starting with _ are ignored (for shared partials/imports).
|
|
94
|
+
"""
|
|
95
|
+
controls_dir = project_root / "controls"
|
|
96
|
+
if not controls_dir.exists():
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
# Find all HTML templates (excluding _prefixed files)
|
|
100
|
+
for template in controls_dir.rglob("*.html"):
|
|
101
|
+
rel_path = template.relative_to(controls_dir)
|
|
102
|
+
|
|
103
|
+
# Skip files/dirs starting with _ (check relative path only)
|
|
104
|
+
if any(part.startswith("_") for part in rel_path.parts):
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
if template.stem == template.parent.name:
|
|
108
|
+
# Self-contained: controls/rating/rating.html -> "rating"
|
|
109
|
+
# Or nested: controls/pickers/color/color.html -> "pickers/color"
|
|
110
|
+
parent_rel = rel_path.parent
|
|
111
|
+
if parent_rel != Path("."):
|
|
112
|
+
control_name = str(parent_rel).replace("\\", "/")
|
|
113
|
+
else:
|
|
114
|
+
control_name = template.stem
|
|
115
|
+
else:
|
|
116
|
+
# Flat: controls/toggle.html -> "toggle"
|
|
117
|
+
# Or categorized flat: controls/pickers/icon.html -> "pickers/icon"
|
|
118
|
+
if rel_path.parent != Path("."):
|
|
119
|
+
control_name = str(rel_path.parent / template.stem).replace(
|
|
120
|
+
"\\", "/"
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
control_name = template.stem
|
|
124
|
+
|
|
125
|
+
# Project controls override built-ins
|
|
126
|
+
self._register_control(control_name, template, override=True)
|
|
127
|
+
logger.debug(f"Loaded project control: {control_name} from {template}")
|
|
128
|
+
|
|
129
|
+
def _register_control(
|
|
130
|
+
self, name: str, template_path: Path, override: bool = False
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Register a control and discover its companion assets (CSS, JS)."""
|
|
133
|
+
if name in self._templates and not override:
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
self._templates[name] = template_path
|
|
137
|
+
|
|
138
|
+
# Load companion CSS if exists
|
|
139
|
+
css_path = template_path.with_suffix(".css")
|
|
140
|
+
if css_path.exists():
|
|
141
|
+
self._css[name] = css_path.read_text()
|
|
142
|
+
|
|
143
|
+
# Load companion JS if exists
|
|
144
|
+
js_path = template_path.with_suffix(".js")
|
|
145
|
+
if js_path.exists():
|
|
146
|
+
self._js[name] = js_path.read_text()
|
|
147
|
+
|
|
148
|
+
def find_control(self, input_type: str) -> Path | None:
|
|
149
|
+
"""Find control template by type name.
|
|
150
|
+
|
|
151
|
+
Supports:
|
|
152
|
+
- "rating" -> controls/rating.html or controls/rating/rating.html
|
|
153
|
+
- "pickers/color" -> controls/pickers/color/color.html
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
input_type: The control type (e.g., "select", "checkbox", "rating")
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Path to the template file, or None if not found
|
|
160
|
+
"""
|
|
161
|
+
# Direct match
|
|
162
|
+
if input_type in self._templates:
|
|
163
|
+
return self._templates[input_type]
|
|
164
|
+
|
|
165
|
+
# Try without namespace prefix for built-ins
|
|
166
|
+
base_name = input_type.split("/")[-1]
|
|
167
|
+
if base_name in self._templates:
|
|
168
|
+
return self._templates[base_name]
|
|
169
|
+
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def has_control(self, input_type: str) -> bool:
|
|
173
|
+
"""Check if a control type is available."""
|
|
174
|
+
return self.find_control(input_type) is not None
|
|
175
|
+
|
|
176
|
+
def render_control(
|
|
177
|
+
self,
|
|
178
|
+
input_type: str,
|
|
179
|
+
context: dict[str, Any],
|
|
180
|
+
) -> str:
|
|
181
|
+
"""Render a control template with the given context.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
input_type: The control type (e.g., "select", "checkbox")
|
|
185
|
+
context: Template context dict containing:
|
|
186
|
+
- name: Variable name
|
|
187
|
+
- label: Display label
|
|
188
|
+
- value: Current value
|
|
189
|
+
- default: Default value
|
|
190
|
+
- options: List of {value, label, selected} for select controls
|
|
191
|
+
- min, max, step: For number/slider controls
|
|
192
|
+
- config: Full variable config (for advanced templates)
|
|
193
|
+
- depends_on: List of variable dependencies
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Rendered HTML string
|
|
197
|
+
"""
|
|
198
|
+
template_path = self.find_control(input_type)
|
|
199
|
+
|
|
200
|
+
if not template_path:
|
|
201
|
+
logger.warning(f"Unknown control type '{input_type}', using readonly")
|
|
202
|
+
template_path = self.find_control("readonly")
|
|
203
|
+
if not template_path:
|
|
204
|
+
# Ultimate fallback if no readonly template
|
|
205
|
+
name = context.get("name", "unknown")
|
|
206
|
+
label = context.get("label", name)
|
|
207
|
+
value = context.get("value", "")
|
|
208
|
+
return f'<span class="dft-variable-readonly"><span class="dft-variable-label">{_html_escape(label)}:</span> <span class="dft-variable-readonly-value">{_html_escape(str(value))}</span></span>'
|
|
209
|
+
|
|
210
|
+
# Track which controls are used (for CSS/JS collection)
|
|
211
|
+
self._used_controls.add(input_type)
|
|
212
|
+
|
|
213
|
+
env = self._get_jinja_env()
|
|
214
|
+
# Load template content directly to support project overrides
|
|
215
|
+
template_content = template_path.read_text()
|
|
216
|
+
template = env.from_string(template_content)
|
|
217
|
+
return template.render(**context)
|
|
218
|
+
|
|
219
|
+
def get_base_css(self) -> str:
|
|
220
|
+
"""Get the base CSS styles for all controls."""
|
|
221
|
+
return self._base_css or ""
|
|
222
|
+
|
|
223
|
+
def get_used_css(self) -> str:
|
|
224
|
+
"""Get combined CSS for controls that were actually rendered.
|
|
225
|
+
|
|
226
|
+
Returns CSS only for controls that have been rendered via render_control(),
|
|
227
|
+
avoiding unnecessary CSS bloat.
|
|
228
|
+
"""
|
|
229
|
+
css_parts = []
|
|
230
|
+
for control_name in self._used_controls:
|
|
231
|
+
if control_name in self._css:
|
|
232
|
+
css_parts.append(f"/* {control_name} */\n{self._css[control_name]}")
|
|
233
|
+
return "\n\n".join(css_parts)
|
|
234
|
+
|
|
235
|
+
def get_all_css(self) -> str:
|
|
236
|
+
"""Get base CSS plus any control-specific CSS for used controls."""
|
|
237
|
+
parts = []
|
|
238
|
+
if self._base_css:
|
|
239
|
+
parts.append(self._base_css)
|
|
240
|
+
used_css = self.get_used_css()
|
|
241
|
+
if used_css:
|
|
242
|
+
parts.append(used_css)
|
|
243
|
+
return "\n\n".join(parts)
|
|
244
|
+
|
|
245
|
+
def reset_used_controls(self) -> None:
|
|
246
|
+
"""Reset the set of used controls (call before rendering a new face)."""
|
|
247
|
+
self._used_controls.clear()
|
|
248
|
+
|
|
249
|
+
def _get_jinja_env(self) -> Environment:
|
|
250
|
+
"""Get or create Jinja environment with all template directories."""
|
|
251
|
+
if self._jinja_env is None:
|
|
252
|
+
# Templates are loaded directly by path, not by name lookup
|
|
253
|
+
# So we just need any directory for the loader (it's not used for discovery)
|
|
254
|
+
template_dirs = [str(self._builtin_dir)]
|
|
255
|
+
|
|
256
|
+
self._jinja_env = Environment(
|
|
257
|
+
loader=FileSystemLoader(template_dirs),
|
|
258
|
+
autoescape=select_autoescape(["html", "xml"]),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Add custom filters
|
|
262
|
+
self._jinja_env.filters["js_escape"] = _js_escape_filter
|
|
263
|
+
self._jinja_env.filters["tojson"] = _tojson_filter
|
|
264
|
+
|
|
265
|
+
# Add useful globals
|
|
266
|
+
self._jinja_env.globals["range"] = range
|
|
267
|
+
|
|
268
|
+
return self._jinja_env
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _js_escape_filter(s: str) -> str:
|
|
272
|
+
"""Escape string for JavaScript single-quoted string context."""
|
|
273
|
+
if s is None:
|
|
274
|
+
return ""
|
|
275
|
+
return json.dumps(str(s))[1:-1].replace("'", "\\'")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _tojson_filter(obj: Any) -> str:
|
|
279
|
+
"""Convert object to JSON string."""
|
|
280
|
+
return json.dumps(obj)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _html_escape(s: str) -> str:
|
|
284
|
+
"""HTML escape a string."""
|
|
285
|
+
import html
|
|
286
|
+
|
|
287
|
+
return html.escape(str(s))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Format conversion functions for rendered output.
|
|
2
|
+
|
|
3
|
+
Stage: RENDER
|
|
4
|
+
Purpose: Convert SVG output to other formats (HTML, PNG, PDF).
|
|
5
|
+
|
|
6
|
+
This package provides format converters:
|
|
7
|
+
- html.py: SVG to HTML conversion (with wrapper page)
|
|
8
|
+
- png.py: SVG to PNG conversion (using svglib)
|
|
9
|
+
- pdf.py: SVG to PDF conversion (using svglib)
|
|
10
|
+
|
|
11
|
+
Dependencies:
|
|
12
|
+
- svglib (optional, for PNG/PDF export)
|
|
13
|
+
- reportlab (optional, for PNG/PDF export)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from dataface.core.render.converters.html import to_html
|
|
17
|
+
from dataface.core.render.converters.pdf import to_pdf
|
|
18
|
+
from dataface.core.render.converters.png import to_png
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"to_html",
|
|
22
|
+
"to_png",
|
|
23
|
+
"to_pdf",
|
|
24
|
+
]
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Chart output conversion helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from dataface.core.compile.sizing import DEFAULT_CHART_HEIGHT
|
|
12
|
+
from dataface.core.render.board_links import get_link_context, resolve_href
|
|
13
|
+
from dataface.core.render.chart.artifacts import RenderArtifact
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from dataface.core.compile.models.style.merged import MergedStyle
|
|
17
|
+
from dataface.core.render.chart.title_overflow import fix_title_alignment
|
|
18
|
+
from dataface.core.render.converters.pdf import to_pdf
|
|
19
|
+
from dataface.core.render.converters.png import to_png
|
|
20
|
+
from dataface.core.render.errors import ChartDataError, FormatError
|
|
21
|
+
from dataface.core.render.font_support import register_vl_convert_fonts
|
|
22
|
+
|
|
23
|
+
# Strip sentinel prefix from vl_convert-rendered chart href <a> elements.
|
|
24
|
+
# vl_convert uses xlink:href and mangles relative URLs, so we embed a sentinel
|
|
25
|
+
# prefix that we can detect and strip here. See standard_renderer._HREF_SENTINEL.
|
|
26
|
+
_SENTINEL_HREF_RE = re.compile(r'<a xlink:href="http://dft\.invalid([^"]*)"')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _fix_chart_click_hrefs(svg: str) -> str:
|
|
30
|
+
"""Convert sentinel xlink:href to plain href in SVG <a> elements.
|
|
31
|
+
|
|
32
|
+
Converts ``<a xlink:href="http://dft.invalid/path?q=v">``
|
|
33
|
+
to ``<a href="/path?q=v">`` so variables.js can intercept variable-update
|
|
34
|
+
clicks and the browser can follow navigation clicks directly.
|
|
35
|
+
|
|
36
|
+
When a link context is active (board resolver), board-root paths are
|
|
37
|
+
rewritten for the current runtime (serve vs Cloud). Query-string-only
|
|
38
|
+
links (?var=value) pass through unchanged — they are in-page variable
|
|
39
|
+
updates intercepted by variables.js, not cross-board navigation.
|
|
40
|
+
"""
|
|
41
|
+
ctx = get_link_context()
|
|
42
|
+
|
|
43
|
+
def _replace(m: re.Match[str]) -> str:
|
|
44
|
+
url = m.group(1)
|
|
45
|
+
if ctx is not None:
|
|
46
|
+
url = resolve_href(url, ctx)
|
|
47
|
+
return f'<a href="{url}"'
|
|
48
|
+
|
|
49
|
+
return _SENTINEL_HREF_RE.sub(_replace, svg)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Inject stroke-linecap="round" on legend-symbol <path> elements emitted by
|
|
53
|
+
# vl_convert. Vega-Lite has no spec-level surface for this (probed
|
|
54
|
+
# legend.symbolStrokeCap, config.legend.symbolStrokeCap, config.style.symbol.
|
|
55
|
+
# strokeCap, config.mark.strokeCap — all silently dropped), so the legend's
|
|
56
|
+
# dash segments render with butt caps while the chart lines use round caps
|
|
57
|
+
# (LineStyle theme default). The mismatch is most visible when the dash
|
|
58
|
+
# palette contains a dotted entry: round caps render 0-length dashes as
|
|
59
|
+
# circular dots, butt caps render them as nothing. Match the chart's cap by
|
|
60
|
+
# stamping round onto the legend paths during post-processing — but only on
|
|
61
|
+
# charts that actually use the strokeDash encoding (gated by the caller),
|
|
62
|
+
# so that charts with no dashes keep producing byte-identical SVG output.
|
|
63
|
+
_LEGEND_SYMBOL_LINECAP_RE = re.compile(
|
|
64
|
+
r'(class="[^"]*role-legend-symbol[^"]*"[^>]*><path)(?![^/]*stroke-linecap)([^/]*?)(/>)'
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _fix_legend_symbol_linecap(svg: str) -> str:
|
|
69
|
+
"""Stamp ``stroke-linecap="round"`` onto legend-symbol paths."""
|
|
70
|
+
return _LEGEND_SYMBOL_LINECAP_RE.sub(r'\1 stroke-linecap="round"\2\3', svg)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _spec_has_encoding(spec: dict[str, Any], channels: Iterable[str]) -> bool:
|
|
74
|
+
"""True when ``spec`` carries any of ``channels`` in an encoding.
|
|
75
|
+
|
|
76
|
+
Recurses through ``spec["encoding"]``, ``spec["layer"]``, ``spec["hconcat"]``,
|
|
77
|
+
and ``spec["vconcat"]`` at every depth — deep enough to catch the
|
|
78
|
+
endpoint-label-rail wrapper that wraps a layered line chart inside
|
|
79
|
+
``hconcat[0]``, and any future nested composition. Cheap gate used before
|
|
80
|
+
SVG post-processors so charts that don't use the channel keep producing
|
|
81
|
+
byte-identical SVG.
|
|
82
|
+
"""
|
|
83
|
+
channel_set = frozenset(channels)
|
|
84
|
+
|
|
85
|
+
def _has(node: dict[str, Any]) -> bool:
|
|
86
|
+
if channel_set & node.get("encoding", {}).keys():
|
|
87
|
+
return True
|
|
88
|
+
for child in node.get("layer", []):
|
|
89
|
+
if isinstance(child, dict) and _has(child):
|
|
90
|
+
return True
|
|
91
|
+
for child in node.get("hconcat", []):
|
|
92
|
+
if isinstance(child, dict) and _has(child):
|
|
93
|
+
return True
|
|
94
|
+
for child in node.get("vconcat", []):
|
|
95
|
+
if isinstance(child, dict) and _has(child):
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
return _has(spec)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _correct_concat_overshoot(
|
|
103
|
+
spec: dict[str, Any],
|
|
104
|
+
target_width: float | None,
|
|
105
|
+
target_height: float | None,
|
|
106
|
+
vlc: Any,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Two-pass width/height correction for hconcat/vconcat endpoint-label specs.
|
|
109
|
+
|
|
110
|
+
vl-convert ignores ``autosize: fit`` on concat children, so the first
|
|
111
|
+
render measures the actual SVG dimensions; the overshoot is subtracted from
|
|
112
|
+
the resizable pane(s) before the real render.
|
|
113
|
+
|
|
114
|
+
Width: hconcat shrinks pane[0] only (label pane is fixed-width). vconcat
|
|
115
|
+
shrinks both panes — they share the x scale and must resize together so rail
|
|
116
|
+
labels stay centered.
|
|
117
|
+
|
|
118
|
+
Height: hconcat shrinks both panes equally so neither governs a taller total.
|
|
119
|
+
vconcat height is handled upfront in _wrap_horizontal_top_row_rail, so
|
|
120
|
+
target_height is not expected on vconcat wrappers.
|
|
121
|
+
"""
|
|
122
|
+
if target_width is None and target_height is None:
|
|
123
|
+
return
|
|
124
|
+
is_hconcat = "hconcat" in spec and spec["hconcat"]
|
|
125
|
+
is_vconcat = "vconcat" in spec and spec["vconcat"]
|
|
126
|
+
if not is_hconcat and not is_vconcat:
|
|
127
|
+
return
|
|
128
|
+
try:
|
|
129
|
+
probe_svg = vlc.vegalite_to_svg(spec)
|
|
130
|
+
except Exception: # noqa: BLE001, S110 — vl-convert throws untyped JS errors
|
|
131
|
+
return
|
|
132
|
+
if target_width is not None:
|
|
133
|
+
m = re.search(r"<svg[^>]*\bwidth=\"([0-9.]+)\"", probe_svg)
|
|
134
|
+
if m:
|
|
135
|
+
overshoot = float(m.group(1)) - float(target_width)
|
|
136
|
+
if overshoot > 0:
|
|
137
|
+
width_panes = (
|
|
138
|
+
[spec["hconcat"][0]] if is_hconcat else list(spec["vconcat"])
|
|
139
|
+
)
|
|
140
|
+
for pane in width_panes:
|
|
141
|
+
orig_w = float(pane.get("width", target_width))
|
|
142
|
+
new_w = orig_w - overshoot
|
|
143
|
+
if new_w > 0:
|
|
144
|
+
pane["width"] = new_w
|
|
145
|
+
if target_height is not None and is_hconcat:
|
|
146
|
+
m = re.search(r"<svg[^>]*\bheight=\"([0-9.]+)\"", probe_svg)
|
|
147
|
+
if m:
|
|
148
|
+
overshoot = float(m.group(1)) - float(target_height)
|
|
149
|
+
if overshoot > 0:
|
|
150
|
+
for pane in spec["hconcat"]:
|
|
151
|
+
orig_h = float(pane.get("height", target_height))
|
|
152
|
+
new_h = orig_h - overshoot
|
|
153
|
+
if new_h > 0:
|
|
154
|
+
pane["height"] = new_h
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def render_svg_content(svg_content: str, format: str, *, scale: float = 1.0) -> str:
|
|
158
|
+
"""Convert SVG content to the requested encoded output."""
|
|
159
|
+
if format == "svg":
|
|
160
|
+
return svg_content
|
|
161
|
+
if format == "png":
|
|
162
|
+
return base64.b64encode(to_png(svg_content, scale=scale)).decode("utf-8")
|
|
163
|
+
if format == "pdf":
|
|
164
|
+
return base64.b64encode(to_pdf(svg_content)).decode("utf-8")
|
|
165
|
+
raise ValueError(f"Unsupported SVG format: {format}")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def render_vega_spec(
|
|
169
|
+
spec: dict[str, Any],
|
|
170
|
+
format: str,
|
|
171
|
+
width: float | None,
|
|
172
|
+
height: float | None,
|
|
173
|
+
is_placeholder: bool,
|
|
174
|
+
resolved_style: MergedStyle | None = None,
|
|
175
|
+
) -> str:
|
|
176
|
+
"""Render a Vega-Lite spec into SVG, PNG, or PDF."""
|
|
177
|
+
try:
|
|
178
|
+
import vl_convert as vlc
|
|
179
|
+
except ImportError:
|
|
180
|
+
raise FormatError(
|
|
181
|
+
f"vl-convert-python is required for {format} rendering. "
|
|
182
|
+
"Install with: pip install vl-convert-python",
|
|
183
|
+
) from None
|
|
184
|
+
|
|
185
|
+
register_vl_convert_fonts(vlc)
|
|
186
|
+
|
|
187
|
+
# Two-pass width/height correction for hconcat endpoint-label specs.
|
|
188
|
+
# vl-convert ignores autosize:fit on concat children, so the first render
|
|
189
|
+
# measures the actual SVG dimensions; the overshoots are subtracted from
|
|
190
|
+
# the resizable pane(s) before the real render. Both sentinels are stamped
|
|
191
|
+
# by _maybe_wrap_endpoint_label_pane and must be popped before rendering.
|
|
192
|
+
target_width = spec.pop("$df_target_width", None)
|
|
193
|
+
target_height = spec.pop("$df_target_height", None)
|
|
194
|
+
if target_width is not None or target_height is not None:
|
|
195
|
+
_correct_concat_overshoot(spec, target_width, target_height, vlc)
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
svg_result = vlc.vegalite_to_svg(spec)
|
|
199
|
+
except Exception as exc:
|
|
200
|
+
# vl-convert JS errors (e.g. Vega scene-graph TypeErrors on unsupported
|
|
201
|
+
# layered specs) escape as Python exceptions. Re-raise as ChartDataError
|
|
202
|
+
# so render_chart_item records a per-tile error card instead of aborting
|
|
203
|
+
# the entire dashboard render process.
|
|
204
|
+
raise ChartDataError(str(exc)) from exc
|
|
205
|
+
svg_result = _fix_chart_click_hrefs(svg_result)
|
|
206
|
+
if _spec_has_encoding(spec, ["strokeDash"]):
|
|
207
|
+
svg_result = _fix_legend_symbol_linecap(svg_result)
|
|
208
|
+
|
|
209
|
+
# For hconcat specs (endpoint-label wrapper), padding lives on pane[0], not
|
|
210
|
+
# the wrapper root — the wrapper has no padding so spec.get("padding") returns
|
|
211
|
+
# nothing and fix_title_alignment would never run without this lookup.
|
|
212
|
+
padding = (
|
|
213
|
+
spec["hconcat"][0].get("padding")
|
|
214
|
+
if "hconcat" in spec and spec["hconcat"]
|
|
215
|
+
else spec.get("padding")
|
|
216
|
+
) or {}
|
|
217
|
+
if isinstance(padding, dict):
|
|
218
|
+
padding_left = float(padding.get("left", 0) or 0)
|
|
219
|
+
elif isinstance(padding, (int, float)):
|
|
220
|
+
padding_left = float(padding)
|
|
221
|
+
else:
|
|
222
|
+
padding_left = 0.0
|
|
223
|
+
if padding_left > 0:
|
|
224
|
+
svg_result = fix_title_alignment(svg_result, padding_left)
|
|
225
|
+
|
|
226
|
+
if is_placeholder:
|
|
227
|
+
from dataface.core.compile.models.primitives import FontStyle
|
|
228
|
+
from dataface.core.render.placeholder import (
|
|
229
|
+
add_placeholder_overlay,
|
|
230
|
+
apply_placeholder_opacity,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
assert resolved_style is not None
|
|
234
|
+
chart_width = width or 400
|
|
235
|
+
chart_height = height or DEFAULT_CHART_HEIGHT
|
|
236
|
+
svg_result = apply_placeholder_opacity(svg_result)
|
|
237
|
+
svg_result = add_placeholder_overlay(
|
|
238
|
+
svg_result,
|
|
239
|
+
chart_width,
|
|
240
|
+
chart_height,
|
|
241
|
+
font=FontStyle(family=resolved_style.font.family),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return render_svg_content(svg_result, format, scale=1.0)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def render_chart_artifact(
|
|
248
|
+
artifact: RenderArtifact,
|
|
249
|
+
format: str,
|
|
250
|
+
width: float | None,
|
|
251
|
+
height: float | None,
|
|
252
|
+
is_placeholder: bool = False,
|
|
253
|
+
resolved_style: MergedStyle | None = None,
|
|
254
|
+
) -> str:
|
|
255
|
+
"""Render a chart-domain artifact into the requested output format."""
|
|
256
|
+
if artifact.kind == "json":
|
|
257
|
+
if format != "json":
|
|
258
|
+
raise ValueError(f"JSON artifact cannot render as {format}")
|
|
259
|
+
return json.dumps(artifact.payload, indent=2)
|
|
260
|
+
|
|
261
|
+
if artifact.kind == "svg":
|
|
262
|
+
return render_svg_content(str(artifact.payload), format)
|
|
263
|
+
|
|
264
|
+
if artifact.kind == "vega_spec":
|
|
265
|
+
if not isinstance(artifact.payload, dict):
|
|
266
|
+
raise ValueError("Vega spec artifact payload must be a dictionary")
|
|
267
|
+
return render_vega_spec(
|
|
268
|
+
artifact.payload,
|
|
269
|
+
format,
|
|
270
|
+
width,
|
|
271
|
+
height,
|
|
272
|
+
is_placeholder=is_placeholder,
|
|
273
|
+
resolved_style=resolved_style,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
raise ValueError(f"Unsupported artifact kind: {artifact.kind}")
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""HTML format conversion.
|
|
2
|
+
|
|
3
|
+
Stage: RENDER
|
|
4
|
+
Purpose: Convert SVG output to HTML page with minimal wrapper.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import html as html_module
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from dataface.core.compile.models.face.compiled import Face, VariableValues
|
|
12
|
+
from dataface.core.execute.executor import Executor
|
|
13
|
+
|
|
14
|
+
_EMOJI_FONT_FACE_PATH = Path(__file__).parent.parent / "fonts" / "_emoji_font_face.css"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def to_html(
|
|
18
|
+
face: Face,
|
|
19
|
+
svg_content: str,
|
|
20
|
+
background: str | None,
|
|
21
|
+
executor: Executor,
|
|
22
|
+
variables: VariableValues,
|
|
23
|
+
**options: Any,
|
|
24
|
+
) -> str:
|
|
25
|
+
"""Convert SVG to HTML page with minimal wrapper.
|
|
26
|
+
|
|
27
|
+
SVG-First Migration: HTML format now simply wraps SVG output in an HTML
|
|
28
|
+
document. The SVG already contains:
|
|
29
|
+
- Interactive variable controls (via foreignObject)
|
|
30
|
+
- JavaScript for variable interactivity (via <script> tags)
|
|
31
|
+
- All chart rendering and layout
|
|
32
|
+
|
|
33
|
+
This approach:
|
|
34
|
+
- Eliminates duplicate HTML-specific rendering code paths
|
|
35
|
+
- Ensures consistent output between SVG and HTML formats
|
|
36
|
+
- Simplifies maintenance by having a single rendering path
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
face: Face being rendered
|
|
40
|
+
svg_content: The SVG content to wrap
|
|
41
|
+
background: Background color for the page
|
|
42
|
+
executor: Executor instance (unused, for API compatibility)
|
|
43
|
+
variables: Variable values (for title resolution)
|
|
44
|
+
Returns:
|
|
45
|
+
Complete HTML document as string
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
from dataface.core.compile.jinja import resolve_jinja_template
|
|
49
|
+
|
|
50
|
+
# Resolve title for <title> tag
|
|
51
|
+
page_title = face.title or "Dataface"
|
|
52
|
+
page_title = resolve_jinja_template(page_title, variables, strict=False)
|
|
53
|
+
escaped_title = html_module.escape(page_title)
|
|
54
|
+
|
|
55
|
+
font_family = face.resolved_style.font.family
|
|
56
|
+
emoji_font_face = _EMOJI_FONT_FACE_PATH.read_text()
|
|
57
|
+
|
|
58
|
+
page_bg = face.resolved_style.page.background
|
|
59
|
+
bg_style = f"background-color: {page_bg};" if page_bg else ""
|
|
60
|
+
|
|
61
|
+
return f"""<!DOCTYPE html>
|
|
62
|
+
<html>
|
|
63
|
+
<head>
|
|
64
|
+
<meta charset="UTF-8">
|
|
65
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
66
|
+
<title>{escaped_title}</title>
|
|
67
|
+
<style>
|
|
68
|
+
{emoji_font_face}
|
|
69
|
+
* {{
|
|
70
|
+
box-sizing: border-box;
|
|
71
|
+
}}
|
|
72
|
+
body {{
|
|
73
|
+
margin: 0;
|
|
74
|
+
padding: 0;
|
|
75
|
+
font-family: {font_family};
|
|
76
|
+
{bg_style}
|
|
77
|
+
}}
|
|
78
|
+
.dataface-wrapper {{
|
|
79
|
+
width: 100%;
|
|
80
|
+
margin: 0 auto;
|
|
81
|
+
}}
|
|
82
|
+
.dataface-svg-container {{
|
|
83
|
+
width: 100%;
|
|
84
|
+
}}
|
|
85
|
+
.dataface-svg-container svg {{
|
|
86
|
+
max-width: 100%;
|
|
87
|
+
height: auto;
|
|
88
|
+
}}
|
|
89
|
+
</style>
|
|
90
|
+
</head>
|
|
91
|
+
<body>
|
|
92
|
+
<div class="dataface-wrapper">
|
|
93
|
+
<div class="dataface-svg-container">
|
|
94
|
+
{svg_content}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</body>
|
|
98
|
+
</html>"""
|