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,946 @@
|
|
|
1
|
+
"""Variable control rendering for SVG/HTML output.
|
|
2
|
+
|
|
3
|
+
Stage: RENDER
|
|
4
|
+
Purpose: Render interactive and read-only variable controls.
|
|
5
|
+
|
|
6
|
+
This module provides functions for rendering variable controls in SVG:
|
|
7
|
+
- Interactive controls using foreignObject for HTML form elements
|
|
8
|
+
- Read-only labels for PNG/PDF export
|
|
9
|
+
- JavaScript for variable interactivity
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import datetime
|
|
15
|
+
import html
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
from dataface.core.compile.models.face.compiled import VariableValues
|
|
21
|
+
from dataface.core.compile.models.style.merged import MergedStyle, resolve_cascaded_font
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from dataface.core.compile.models.variable.authored import Variable
|
|
25
|
+
|
|
26
|
+
from dataface.core.compile.jinja import resolve_jinja_template
|
|
27
|
+
from dataface.core.compile.models.query.compiled import SqlQuery
|
|
28
|
+
from dataface.core.compile.models.style.compiled import font_weight_as_css
|
|
29
|
+
from dataface.core.compile.models.variable.authored import SingleRowBoolProbe
|
|
30
|
+
from dataface.core.compile.sizing import compute_variable_controls_height
|
|
31
|
+
from dataface.core.execute.executor import Executor
|
|
32
|
+
from dataface.core.render.script_embedding import (
|
|
33
|
+
embed_svg_script,
|
|
34
|
+
)
|
|
35
|
+
from dataface.core.render.text.case import format_display_text
|
|
36
|
+
from dataface.core.render.utils import is_valid_sql_identifier
|
|
37
|
+
from dataface.core.render.variable_input_refinement import refine_input_type_from_data
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
_READ_ONLY_UNSET_SELECT = "All"
|
|
42
|
+
_READ_ONLY_UNSET_DATERANGE = "All dates"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _read_only_unset_label(var_def: Variable, *, default: str) -> str:
|
|
46
|
+
if var_def.placeholder:
|
|
47
|
+
return var_def.placeholder
|
|
48
|
+
return default
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _select_value_is_unset(value: Any) -> bool:
|
|
52
|
+
return value is None or value == ""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _daterange_is_unset(value: Any) -> bool:
|
|
56
|
+
if value is None:
|
|
57
|
+
return True
|
|
58
|
+
if not isinstance(value, (list, tuple)):
|
|
59
|
+
return False
|
|
60
|
+
if len(value) != 2:
|
|
61
|
+
return False
|
|
62
|
+
return value[0] in (None, "") or value[1] in (None, "")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def format_variable_display_value(var_def: Variable, value: Any) -> str:
|
|
66
|
+
"""Human-readable filter state for read-only variable strips (PNG/PDF/static SVG)."""
|
|
67
|
+
input_type = var_def.input
|
|
68
|
+
|
|
69
|
+
if input_type in ("select", "radio"):
|
|
70
|
+
if _select_value_is_unset(value):
|
|
71
|
+
return _read_only_unset_label(var_def, default=_READ_ONLY_UNSET_SELECT)
|
|
72
|
+
return str(value)
|
|
73
|
+
|
|
74
|
+
if input_type == "multiselect":
|
|
75
|
+
if value is None or value == []:
|
|
76
|
+
return _read_only_unset_label(var_def, default=_READ_ONLY_UNSET_SELECT)
|
|
77
|
+
if not isinstance(value, (list, tuple)):
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"multiselect variable expects a list, got {type(value).__name__}"
|
|
80
|
+
)
|
|
81
|
+
return ", ".join(str(v) for v in value)
|
|
82
|
+
|
|
83
|
+
if input_type == "daterange":
|
|
84
|
+
if _daterange_is_unset(value):
|
|
85
|
+
return _read_only_unset_label(var_def, default=_READ_ONLY_UNSET_DATERANGE)
|
|
86
|
+
if not isinstance(value, (list, tuple)):
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"daterange variable expects a 2-element list, got {type(value).__name__}"
|
|
89
|
+
)
|
|
90
|
+
if len(value) != 2:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"daterange variable expects exactly 2 elements, got {len(value)}"
|
|
93
|
+
)
|
|
94
|
+
start, end = value[0], value[1]
|
|
95
|
+
if start in (None, "") or end in (None, ""):
|
|
96
|
+
return _read_only_unset_label(var_def, default=_READ_ONLY_UNSET_DATERANGE)
|
|
97
|
+
return _format_daterange_label(str(start), str(end))
|
|
98
|
+
|
|
99
|
+
if input_type == "checkbox":
|
|
100
|
+
if value is None:
|
|
101
|
+
return "No"
|
|
102
|
+
if isinstance(value, str):
|
|
103
|
+
return "Yes" if value.lower().strip() in ("true", "yes", "1") else "No"
|
|
104
|
+
return "Yes" if value else "No"
|
|
105
|
+
|
|
106
|
+
if input_type in (
|
|
107
|
+
"text",
|
|
108
|
+
"input",
|
|
109
|
+
"textarea",
|
|
110
|
+
"number",
|
|
111
|
+
"slider",
|
|
112
|
+
"range",
|
|
113
|
+
"date",
|
|
114
|
+
"datepicker",
|
|
115
|
+
):
|
|
116
|
+
if value is None or value == "":
|
|
117
|
+
return var_def.placeholder or ""
|
|
118
|
+
return str(value)
|
|
119
|
+
|
|
120
|
+
if input_type == "auto":
|
|
121
|
+
raise ValueError(
|
|
122
|
+
"read-only variable display requires a resolved input type, not 'auto'"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"unsupported variable input type for read-only display: {input_type!r}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def render_variables_svg(
|
|
131
|
+
variable_defs: dict[str, Any],
|
|
132
|
+
current_values: VariableValues,
|
|
133
|
+
width: float,
|
|
134
|
+
*,
|
|
135
|
+
resolved_style: MergedStyle,
|
|
136
|
+
) -> tuple[str, float]:
|
|
137
|
+
"""Render variables as read-only labels in SVG.
|
|
138
|
+
|
|
139
|
+
Used for PNG/PDF export where foreignObject is not supported.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
variable_defs: Dictionary of variable definitions
|
|
143
|
+
current_values: Current variable values
|
|
144
|
+
width: Available width
|
|
145
|
+
resolved_style: MergedStyle for color and font resolution.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Tuple of (SVG string, height used)
|
|
149
|
+
"""
|
|
150
|
+
if not variable_defs:
|
|
151
|
+
return "", 0.0
|
|
152
|
+
|
|
153
|
+
variables_style = resolved_style.variables
|
|
154
|
+
_font_family = variables_style.label.font.family
|
|
155
|
+
assert (
|
|
156
|
+
_font_family is not None
|
|
157
|
+
), "cascade should have populated variables.label.font.family"
|
|
158
|
+
|
|
159
|
+
_var_font_size = variables_style.font.size
|
|
160
|
+
assert _var_font_size is not None, "style.variables.font.size must be configured"
|
|
161
|
+
font_size = float(_var_font_size)
|
|
162
|
+
line_height = variables_style.line_height
|
|
163
|
+
padding = variables_style.padding
|
|
164
|
+
font_family = html.escape(str(_font_family))
|
|
165
|
+
items: list[str] = []
|
|
166
|
+
current_x = 0.0
|
|
167
|
+
current_y = font_size + padding
|
|
168
|
+
|
|
169
|
+
for var_name, var_def in variable_defs.items():
|
|
170
|
+
current_value = current_values.get(var_name, var_def.default)
|
|
171
|
+
display_value = format_variable_display_value(var_def, current_value)
|
|
172
|
+
|
|
173
|
+
label = var_def.label or format_display_text(
|
|
174
|
+
var_name,
|
|
175
|
+
from_slug=True,
|
|
176
|
+
font=resolve_cascaded_font(
|
|
177
|
+
variables_style.label.font, "variables.label.font"
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
escaped_label = html.escape(label)
|
|
181
|
+
escaped_value = html.escape(display_value)
|
|
182
|
+
|
|
183
|
+
# Create label: value text
|
|
184
|
+
label_text = f"{escaped_label}:"
|
|
185
|
+
value_text = f" {escaped_value}"
|
|
186
|
+
|
|
187
|
+
# Estimate width (rough calculation)
|
|
188
|
+
label_width = len(label_text) * font_size * 0.6
|
|
189
|
+
value_width = len(value_text) * font_size * 0.55
|
|
190
|
+
|
|
191
|
+
# Check if we need to wrap to next line
|
|
192
|
+
if current_x + label_width + value_width > width and current_x > 0:
|
|
193
|
+
current_x = 0
|
|
194
|
+
current_y += line_height
|
|
195
|
+
|
|
196
|
+
label_weight_raw = variables_style.label.font.weight
|
|
197
|
+
assert (
|
|
198
|
+
label_weight_raw is not None
|
|
199
|
+
), "cascade should have populated variables.label.font.weight"
|
|
200
|
+
label_weight = font_weight_as_css(label_weight_raw)
|
|
201
|
+
# Render the label and value with theme-appropriate colors
|
|
202
|
+
items.append(
|
|
203
|
+
f'<text x="{current_x}" y="{current_y}" font-size="{font_size}" fill="{resolved_style.variables.font.color}" '
|
|
204
|
+
f'font-family="{font_family}">'
|
|
205
|
+
f'<tspan font-weight="{label_weight}">{label_text}</tspan>'
|
|
206
|
+
f'<tspan fill="{resolved_style.font.color}">{value_text}</tspan>'
|
|
207
|
+
f"</text>"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
current_x += label_width + value_width + variables_style.gap
|
|
211
|
+
|
|
212
|
+
if not items:
|
|
213
|
+
return "", 0.0
|
|
214
|
+
|
|
215
|
+
total_height = current_y + padding
|
|
216
|
+
svg_content = "\n".join(items)
|
|
217
|
+
|
|
218
|
+
# Wrap in a rect background with theme color
|
|
219
|
+
return (
|
|
220
|
+
f'<rect x="0" y="0" width="{width}" height="{total_height}" fill="{resolved_style.variables.input.background}" rx="{variables_style.border.radius}"/>'
|
|
221
|
+
f"{svg_content}",
|
|
222
|
+
total_height,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def resolve_query_options_for_svg(
|
|
227
|
+
query_ref: str, executor: Executor, variables: dict[str, Any]
|
|
228
|
+
) -> list[str]:
|
|
229
|
+
"""Resolve options from a named query for SVG rendering.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
query_ref: Query name reference (e.g., "queries.city_options" or "city_options")
|
|
233
|
+
executor: Executor for running queries
|
|
234
|
+
variables: Current variable values for query execution
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
List of option values from the query
|
|
238
|
+
"""
|
|
239
|
+
try:
|
|
240
|
+
# Strip "queries." prefix if present
|
|
241
|
+
query_name = query_ref.replace("queries.", "")
|
|
242
|
+
|
|
243
|
+
# Execute the query
|
|
244
|
+
result = executor.execute_query(query_name, variables)
|
|
245
|
+
|
|
246
|
+
if result and len(result) > 0:
|
|
247
|
+
# Look for 'value' column first, then 'label', then first column
|
|
248
|
+
first_row = result[0]
|
|
249
|
+
value_key = None
|
|
250
|
+
for key in ["value", "label", "name", "id"]:
|
|
251
|
+
if key in first_row:
|
|
252
|
+
value_key = key
|
|
253
|
+
break
|
|
254
|
+
if value_key is None:
|
|
255
|
+
value_key = list(first_row.keys())[0]
|
|
256
|
+
|
|
257
|
+
return [
|
|
258
|
+
str(row[value_key]) for row in result if row.get(value_key) is not None
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
return []
|
|
262
|
+
except (ValueError, KeyError, TypeError, RuntimeError):
|
|
263
|
+
# Log as warning with context to make failures visible for debugging
|
|
264
|
+
# Don't raise to keep rendering, but make the failure actionable
|
|
265
|
+
logger.warning(
|
|
266
|
+
"Failed to resolve query options for SVG variable control",
|
|
267
|
+
exc_info=True,
|
|
268
|
+
)
|
|
269
|
+
return []
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def resolve_column_options_for_svg(column_ref: str, executor: Executor) -> list[str]:
|
|
273
|
+
"""Resolve distinct values from a column for variable options in SVG.
|
|
274
|
+
|
|
275
|
+
For column binding to work, the table must be defined in sources or be
|
|
276
|
+
an actual database table. For CSV-based data loaded via read_csv(),
|
|
277
|
+
use options.query with a dedicated query instead.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
column_ref: Column reference in format "table.column" or just "column"
|
|
281
|
+
executor: Executor for running queries
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
List of distinct values from the column, or empty list if unavailable
|
|
285
|
+
"""
|
|
286
|
+
try:
|
|
287
|
+
# Parse the column reference (table.column or just column)
|
|
288
|
+
if "." not in column_ref:
|
|
289
|
+
return []
|
|
290
|
+
|
|
291
|
+
table_name, column_name = column_ref.rsplit(".", 1)
|
|
292
|
+
|
|
293
|
+
# Validate identifiers to prevent SQL injection
|
|
294
|
+
if not is_valid_sql_identifier(table_name) or not is_valid_sql_identifier(
|
|
295
|
+
column_name
|
|
296
|
+
):
|
|
297
|
+
logger.warning(f"Invalid SQL identifier in column reference: {column_ref}")
|
|
298
|
+
return []
|
|
299
|
+
|
|
300
|
+
# Build a query to get distinct values
|
|
301
|
+
sql = f"SELECT DISTINCT {column_name} FROM {table_name} ORDER BY {column_name}"
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
result = executor.adapter_registry.execute(SqlQuery(sql=sql))
|
|
305
|
+
if result.is_success and result.data:
|
|
306
|
+
first_key = list(result.data[0].keys())[0]
|
|
307
|
+
if first_key:
|
|
308
|
+
return [
|
|
309
|
+
str(row[first_key])
|
|
310
|
+
for row in result.data
|
|
311
|
+
if row[first_key] is not None
|
|
312
|
+
]
|
|
313
|
+
except (ValueError, KeyError, TypeError, RuntimeError):
|
|
314
|
+
# Column binding requires the table to exist in the database
|
|
315
|
+
# For CSV-based data, use options.query instead
|
|
316
|
+
logger.warning(
|
|
317
|
+
f"Failed to bind column '{column_ref}' for variable options. "
|
|
318
|
+
"For CSV-based data, use options.query instead.",
|
|
319
|
+
exc_info=True,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
return []
|
|
323
|
+
except (ValueError, KeyError, TypeError):
|
|
324
|
+
logger.warning(
|
|
325
|
+
"Failed to resolve column options for SVG variable control",
|
|
326
|
+
exc_info=True,
|
|
327
|
+
)
|
|
328
|
+
return []
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def render_interactive_variables_svg(
|
|
332
|
+
variable_defs: dict[str, Any],
|
|
333
|
+
current_values: VariableValues,
|
|
334
|
+
width: float,
|
|
335
|
+
executor: Executor | None = None,
|
|
336
|
+
*,
|
|
337
|
+
resolved_style: MergedStyle,
|
|
338
|
+
) -> tuple[str, float]:
|
|
339
|
+
"""Render interactive variable controls in SVG using foreignObject.
|
|
340
|
+
|
|
341
|
+
Uses real HTML form elements embedded via foreignObject for:
|
|
342
|
+
- Native keyboard/accessibility support
|
|
343
|
+
- Mobile-friendly controls
|
|
344
|
+
- Less custom code to maintain
|
|
345
|
+
|
|
346
|
+
Variables with visible=False are filtered out and not rendered.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
variable_defs: Dictionary of variable definitions
|
|
350
|
+
current_values: Current variable values
|
|
351
|
+
width: Available width
|
|
352
|
+
executor: Executor for resolving query-based options
|
|
353
|
+
resolved_style: MergedStyle for color and font resolution.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Tuple of (SVG string with foreignObject, height used)
|
|
357
|
+
"""
|
|
358
|
+
if not variable_defs:
|
|
359
|
+
return "", 0.0
|
|
360
|
+
|
|
361
|
+
# Filter out non-visible variables — they can be used in queries but not rendered
|
|
362
|
+
visible_defs = {
|
|
363
|
+
name: var_def for name, var_def in variable_defs.items() if var_def.visible
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if not visible_defs:
|
|
367
|
+
return "", 0.0
|
|
368
|
+
|
|
369
|
+
# Use filtered visible_defs for rendering
|
|
370
|
+
variable_defs = visible_defs
|
|
371
|
+
|
|
372
|
+
variables_style = resolved_style.variables
|
|
373
|
+
_font_family = variables_style.label.font.family
|
|
374
|
+
assert (
|
|
375
|
+
_font_family is not None
|
|
376
|
+
), "cascade should have populated variables.label.font.family"
|
|
377
|
+
|
|
378
|
+
height = compute_variable_controls_height(
|
|
379
|
+
variable_defs, width, variables_style=resolved_style.variables
|
|
380
|
+
)
|
|
381
|
+
font_family = html.escape(str(_font_family))
|
|
382
|
+
|
|
383
|
+
# Build HTML controls.
|
|
384
|
+
# Shared icon symbols (e.g. calendar for daterange) are prepended once so
|
|
385
|
+
# multiple daterange controls on the same face don't duplicate the id.
|
|
386
|
+
controls_html: list[str] = []
|
|
387
|
+
icons_html = generate_svg_variable_icons(variable_defs)
|
|
388
|
+
if icons_html:
|
|
389
|
+
controls_html.append(icons_html)
|
|
390
|
+
for var_name, var_def in variable_defs.items():
|
|
391
|
+
control = render_html_control_for_svg(
|
|
392
|
+
var_name,
|
|
393
|
+
var_def,
|
|
394
|
+
current_values,
|
|
395
|
+
executor,
|
|
396
|
+
resolved_style=resolved_style,
|
|
397
|
+
)
|
|
398
|
+
controls_html.append(control)
|
|
399
|
+
|
|
400
|
+
label_font = variables_style.label.font
|
|
401
|
+
label_color = label_font.color
|
|
402
|
+
assert (
|
|
403
|
+
label_color is not None
|
|
404
|
+
), "cascade should have populated variables.label.font.color"
|
|
405
|
+
label_weight_raw = label_font.weight
|
|
406
|
+
assert (
|
|
407
|
+
label_weight_raw is not None
|
|
408
|
+
), "cascade should have populated variables.label.font.weight"
|
|
409
|
+
label_weight = font_weight_as_css(label_weight_raw)
|
|
410
|
+
label_size_raw = label_font.size
|
|
411
|
+
assert (
|
|
412
|
+
label_size_raw is not None
|
|
413
|
+
), "cascade should have populated variables.label.font.size"
|
|
414
|
+
value_font = variables_style.value.font
|
|
415
|
+
value_weight_raw = value_font.weight
|
|
416
|
+
assert (
|
|
417
|
+
value_weight_raw is not None
|
|
418
|
+
), "cascade should have populated variables.value.font.weight"
|
|
419
|
+
value_weight = font_weight_as_css(value_weight_raw)
|
|
420
|
+
value_color = value_font.color
|
|
421
|
+
assert (
|
|
422
|
+
value_color is not None
|
|
423
|
+
), "cascade should have populated variables.value.font.color"
|
|
424
|
+
value_family_raw = value_font.family
|
|
425
|
+
assert (
|
|
426
|
+
value_family_raw is not None
|
|
427
|
+
), "cascade should have populated variables.value.font.family"
|
|
428
|
+
value_family = html.escape(str(value_family_raw))
|
|
429
|
+
value_size_raw = value_font.size
|
|
430
|
+
assert (
|
|
431
|
+
value_size_raw is not None
|
|
432
|
+
), "cascade should have populated variables.value.font.size"
|
|
433
|
+
value_numeric_variant = variables_style.value.numeric_variant
|
|
434
|
+
placeholder_color = variables_style.placeholder.font.color
|
|
435
|
+
assert (
|
|
436
|
+
placeholder_color is not None
|
|
437
|
+
), "cascade should have populated variables.placeholder.font.color"
|
|
438
|
+
accent = resolved_style.accent
|
|
439
|
+
# Semi-transparent focus ring derived from accent color.
|
|
440
|
+
# Inline hex/rgb8 → rgba string so we don't pull in a color library.
|
|
441
|
+
focus_ring = f"color-mix(in srgb, {accent} 25%, transparent)"
|
|
442
|
+
|
|
443
|
+
# Wrap in foreignObject with proper namespace using theme colors.
|
|
444
|
+
# CSS custom properties on .dft-variables are consumed by controls/_styles.css
|
|
445
|
+
# (included in svg/styles.css) for hover, focus, disabled interaction states.
|
|
446
|
+
# overflow:visible on the foreignObject lets absolute-positioned popovers (daterange
|
|
447
|
+
# chip) escape the element's clipping rectangle in Chromium and Safari.
|
|
448
|
+
_vars = resolved_style.variables
|
|
449
|
+
# Title-inline packs controls against the right edge of the band so they
|
|
450
|
+
# sit flush with the board's right margin opposite the title. Other
|
|
451
|
+
# positions (top/bottom) leave them flex-start so they flow naturally from
|
|
452
|
+
# the leading edge of the content area.
|
|
453
|
+
_justify = (
|
|
454
|
+
"flex-end" if variables_style.position == "title-inline" else "flex-start"
|
|
455
|
+
)
|
|
456
|
+
svg = f"""<foreignObject x="0" y="0" width="{width}" height="{height}" style="overflow: visible;">
|
|
457
|
+
<div xmlns="http://www.w3.org/1999/xhtml" class="dft-variables"
|
|
458
|
+
style="display: flex; flex-wrap: wrap; justify-content: {_justify}; gap: {variables_style.gap}px; align-items: center;
|
|
459
|
+
background: {_vars.input.background}; padding: {variables_style.padding}px {variables_style.container_padding}px; border-radius: {variables_style.border.radius}px;
|
|
460
|
+
font-size: {variables_style.font.size}px; box-sizing: border-box; height: 100%;
|
|
461
|
+
overflow: visible;
|
|
462
|
+
--dft-font-family: {font_family};
|
|
463
|
+
--dft-label-color: {label_color};
|
|
464
|
+
--dft-label-weight: {label_weight};
|
|
465
|
+
--dft-label-size: {label_size_raw}px;
|
|
466
|
+
--dft-text-color: {resolved_style.font.color};
|
|
467
|
+
--dft-value-color: {value_color};
|
|
468
|
+
--dft-value-family: {value_family};
|
|
469
|
+
--dft-value-size: {value_size_raw}px;
|
|
470
|
+
--dft-value-weight: {value_weight};
|
|
471
|
+
--dft-value-numeric-variant: {value_numeric_variant};
|
|
472
|
+
--dft-placeholder-color: {placeholder_color};
|
|
473
|
+
--dft-input-bg: {_vars.input.background};
|
|
474
|
+
--dft-input-border: {_vars.border.color};
|
|
475
|
+
--dft-accent-color: {accent};
|
|
476
|
+
--dft-focus-ring: {focus_ring};
|
|
477
|
+
--dft-muted-color: {_vars.font.color};
|
|
478
|
+
--dft-popover-rail-bg: {_vars.popover_rail_background};">
|
|
479
|
+
{"".join(controls_html)}
|
|
480
|
+
</div>
|
|
481
|
+
</foreignObject>"""
|
|
482
|
+
return svg, height
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _resolve_option_values(
|
|
486
|
+
var_def: Variable,
|
|
487
|
+
executor: Executor | None,
|
|
488
|
+
current_values: dict[str, Any],
|
|
489
|
+
) -> list[str]:
|
|
490
|
+
"""Resolve option values from static list, query, or column binding."""
|
|
491
|
+
if var_def.options and var_def.options.static:
|
|
492
|
+
return [str(opt) for opt in var_def.options.static]
|
|
493
|
+
if var_def.options and var_def.options.query and executor:
|
|
494
|
+
return resolve_query_options_for_svg(
|
|
495
|
+
var_def.options.query, executor, current_values
|
|
496
|
+
)
|
|
497
|
+
if var_def.options and var_def.options.column and executor:
|
|
498
|
+
return resolve_column_options_for_svg(var_def.options.column, executor)
|
|
499
|
+
if var_def.column and executor:
|
|
500
|
+
return resolve_column_options_for_svg(var_def.column, executor)
|
|
501
|
+
return []
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _coerce_bool(value: Any) -> bool:
|
|
505
|
+
"""Coerce value to bool; raise ValueError for unrecognized inputs."""
|
|
506
|
+
if isinstance(value, bool):
|
|
507
|
+
return value
|
|
508
|
+
if isinstance(value, (int, float)):
|
|
509
|
+
if value == 1:
|
|
510
|
+
return True
|
|
511
|
+
if value == 0:
|
|
512
|
+
return False
|
|
513
|
+
if isinstance(value, str):
|
|
514
|
+
lower = value.strip().lower()
|
|
515
|
+
if lower in ("true", "1", "yes"):
|
|
516
|
+
return True
|
|
517
|
+
if lower in ("false", "0", "no"):
|
|
518
|
+
return False
|
|
519
|
+
raise ValueError(f"Cannot coerce {value!r} to bool; expected true/false/1/0/yes/no")
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _eval_bool_condition(
|
|
523
|
+
expr: bool | str | SingleRowBoolProbe | None,
|
|
524
|
+
*,
|
|
525
|
+
if_none: bool,
|
|
526
|
+
current_values: dict[str, Any],
|
|
527
|
+
executor: Executor | None,
|
|
528
|
+
context: str,
|
|
529
|
+
) -> bool:
|
|
530
|
+
"""Evaluate a bool/str/probe condition and return the result.
|
|
531
|
+
|
|
532
|
+
Shared core for Variable.disabled and LayoutItem.visible; callers pass
|
|
533
|
+
``if_none`` to control the meaning of a missing (None) value.
|
|
534
|
+
|
|
535
|
+
- None: return ``if_none`` (disabled → False, visible → True).
|
|
536
|
+
- bool: return as-is.
|
|
537
|
+
- str: auto-wrap in ``{{ }}`` if needed, evaluate as Jinja, coerce to bool.
|
|
538
|
+
- SingleRowBoolProbe: execute the named query; expect exactly 1 row; coerce
|
|
539
|
+
the named column to bool.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
expr: The condition value (from YAML or a compiled model field).
|
|
543
|
+
if_none: Value to return when expr is None.
|
|
544
|
+
current_values: Current variable values for Jinja resolution.
|
|
545
|
+
executor: Required when expr is a SingleRowBoolProbe.
|
|
546
|
+
context: Human-readable label for error messages (e.g. "Variable 'region' disabled").
|
|
547
|
+
|
|
548
|
+
Raises:
|
|
549
|
+
ValueError: Wrong row count, missing column, non-bool-coercible value, or
|
|
550
|
+
no executor for a query-backed condition.
|
|
551
|
+
"""
|
|
552
|
+
if expr is None:
|
|
553
|
+
return if_none
|
|
554
|
+
if isinstance(expr, bool):
|
|
555
|
+
return expr
|
|
556
|
+
if isinstance(expr, str):
|
|
557
|
+
# Auto-wrap bare variable names / Jinja expressions (no {{ }} required).
|
|
558
|
+
jinja_expr = expr if "{{" in expr else f"{{{{ {expr} }}}}"
|
|
559
|
+
rendered = resolve_jinja_template(jinja_expr, variables=current_values)
|
|
560
|
+
return _coerce_bool(rendered)
|
|
561
|
+
if isinstance(expr, SingleRowBoolProbe):
|
|
562
|
+
if executor is None:
|
|
563
|
+
raise ValueError(
|
|
564
|
+
f"{context} query '{expr.query}' requires an executor but none was provided"
|
|
565
|
+
)
|
|
566
|
+
rows = executor.execute_query(expr.query, current_values)
|
|
567
|
+
if not rows:
|
|
568
|
+
raise ValueError(
|
|
569
|
+
f"{context} query '{expr.query}' returned no rows; expected exactly 1"
|
|
570
|
+
)
|
|
571
|
+
if len(rows) > 1:
|
|
572
|
+
raise ValueError(
|
|
573
|
+
f"{context} query '{expr.query}' returned {len(rows)} rows; expected exactly 1"
|
|
574
|
+
)
|
|
575
|
+
row = rows[0]
|
|
576
|
+
if expr.column not in row:
|
|
577
|
+
raise ValueError(
|
|
578
|
+
f"{context} query '{expr.query}' has no column '{expr.column}'; "
|
|
579
|
+
f"available: {list(row.keys())}"
|
|
580
|
+
)
|
|
581
|
+
return _coerce_bool(row[expr.column])
|
|
582
|
+
raise TypeError(f"unsupported visible/disabled value: {expr!r}")
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _evaluate_disabled(
|
|
586
|
+
var_def: Variable,
|
|
587
|
+
current_values: dict[str, Any],
|
|
588
|
+
executor: Executor | None,
|
|
589
|
+
) -> bool:
|
|
590
|
+
"""Return True when the variable control should be disabled."""
|
|
591
|
+
return _eval_bool_condition(
|
|
592
|
+
var_def.disabled,
|
|
593
|
+
if_none=False,
|
|
594
|
+
current_values=current_values,
|
|
595
|
+
executor=executor,
|
|
596
|
+
context="Variable disabled",
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def evaluate_visible(
|
|
601
|
+
expr: bool | str | SingleRowBoolProbe | None,
|
|
602
|
+
current_values: dict[str, Any],
|
|
603
|
+
executor: Executor | None,
|
|
604
|
+
context: str = "layout item",
|
|
605
|
+
) -> bool:
|
|
606
|
+
"""Return True when the layout item should be rendered.
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
expr: The visible value from the layout item.
|
|
610
|
+
current_values: Current variable values for Jinja resolution.
|
|
611
|
+
executor: Required when expr is a SingleRowBoolProbe.
|
|
612
|
+
context: Human-readable label for error messages (e.g. "layout item 'kpi_a'").
|
|
613
|
+
"""
|
|
614
|
+
return _eval_bool_condition(
|
|
615
|
+
expr,
|
|
616
|
+
if_none=True,
|
|
617
|
+
current_values=current_values,
|
|
618
|
+
executor=executor,
|
|
619
|
+
context=f"{context} visible",
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
_MONTHS_SHORT = [
|
|
624
|
+
"Jan",
|
|
625
|
+
"Feb",
|
|
626
|
+
"Mar",
|
|
627
|
+
"Apr",
|
|
628
|
+
"May",
|
|
629
|
+
"Jun",
|
|
630
|
+
"Jul",
|
|
631
|
+
"Aug",
|
|
632
|
+
"Sep",
|
|
633
|
+
"Oct",
|
|
634
|
+
"Nov",
|
|
635
|
+
"Dec",
|
|
636
|
+
]
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _format_daterange_label(start_iso: str, end_iso: str) -> str:
|
|
640
|
+
"""Human-readable chip label from ISO date strings.
|
|
641
|
+
|
|
642
|
+
Mirrors JS formatRange() so the server-rendered initial label matches
|
|
643
|
+
what the client renders post-interaction. Falls back to raw ISO on parse error.
|
|
644
|
+
"""
|
|
645
|
+
try:
|
|
646
|
+
s = datetime.date.fromisoformat(start_iso)
|
|
647
|
+
e = datetime.date.fromisoformat(end_iso)
|
|
648
|
+
except ValueError:
|
|
649
|
+
return f"{start_iso} – {end_iso}"
|
|
650
|
+
|
|
651
|
+
def fmt(d: datetime.date) -> str:
|
|
652
|
+
return f"{d.day} {_MONTHS_SHORT[d.month - 1]} {d.year}"
|
|
653
|
+
|
|
654
|
+
def fmt_no_yr(d: datetime.date) -> str:
|
|
655
|
+
return f"{d.day} {_MONTHS_SHORT[d.month - 1]}"
|
|
656
|
+
|
|
657
|
+
if s == e:
|
|
658
|
+
return fmt(s)
|
|
659
|
+
if s.year == e.year and s.month == e.month:
|
|
660
|
+
return f"{s.day}–{e.day} {_MONTHS_SHORT[s.month - 1]} {s.year}"
|
|
661
|
+
if s.year == e.year:
|
|
662
|
+
return f"{fmt_no_yr(s)} – {fmt(e)}"
|
|
663
|
+
return f"{fmt(s)} – {fmt(e)}"
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def render_html_control_for_svg(
|
|
667
|
+
var_name: str,
|
|
668
|
+
var_def: Variable,
|
|
669
|
+
current_values: dict[str, Any],
|
|
670
|
+
executor: Executor | None = None,
|
|
671
|
+
*,
|
|
672
|
+
resolved_style: MergedStyle,
|
|
673
|
+
) -> str:
|
|
674
|
+
"""Render a single HTML form control for embedding in SVG foreignObject.
|
|
675
|
+
|
|
676
|
+
Colors come from CSS custom properties on the .dft-variables container
|
|
677
|
+
(set by render_interactive_variables_svg); per-instance widths/padding
|
|
678
|
+
are still inline.
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
var_name: Variable name
|
|
682
|
+
var_def: Variable definition object
|
|
683
|
+
current_values: Current variable values dict
|
|
684
|
+
executor: Executor for resolving query-based options
|
|
685
|
+
resolved_style: Resolved style for visual properties
|
|
686
|
+
|
|
687
|
+
Returns:
|
|
688
|
+
HTML string for the control
|
|
689
|
+
"""
|
|
690
|
+
variables_style = resolved_style.variables
|
|
691
|
+
|
|
692
|
+
input_config = variables_style.input
|
|
693
|
+
control_gap = variables_style.control_gap
|
|
694
|
+
input_padding = input_config.padding
|
|
695
|
+
input_border_radius = input_config.border.radius
|
|
696
|
+
|
|
697
|
+
current = current_values.get(var_name, var_def.default)
|
|
698
|
+
label = var_def.label or format_display_text(
|
|
699
|
+
var_name,
|
|
700
|
+
from_slug=True,
|
|
701
|
+
font=resolve_cascaded_font(variables_style.label.font, "variables.label.font"),
|
|
702
|
+
)
|
|
703
|
+
escaped_label = html.escape(label)
|
|
704
|
+
description = var_def.description
|
|
705
|
+
description_attr = f' title="{html.escape(description)}"' if description else ""
|
|
706
|
+
# Use html.escape for HTML attribute context (name, data-variable)
|
|
707
|
+
escaped_name = html.escape(var_name)
|
|
708
|
+
# Use json.dumps for JavaScript string context, then escape single quotes
|
|
709
|
+
# since we use single-quoted JS strings in HTML attributes
|
|
710
|
+
js_safe_name = json.dumps(var_name)[1:-1].replace(
|
|
711
|
+
"'", "\\'"
|
|
712
|
+
) # Escape for JS single-quoted strings
|
|
713
|
+
|
|
714
|
+
# Build data attribute for variable dependencies (for cascading dropdowns)
|
|
715
|
+
# This tells the UI which other variables this variable depends on
|
|
716
|
+
depends_on_attr = ""
|
|
717
|
+
if var_def.variable_dependencies:
|
|
718
|
+
deps_list = sorted(var_def.variable_dependencies)
|
|
719
|
+
deps_json = json.dumps(deps_list)
|
|
720
|
+
depends_on_attr = f' data-depends-on="{html.escape(deps_json)}"'
|
|
721
|
+
|
|
722
|
+
# Evaluate disabled state before building any HTML.
|
|
723
|
+
is_disabled = _evaluate_disabled(var_def, current_values, executor)
|
|
724
|
+
disabled_attr = " disabled" if is_disabled else ""
|
|
725
|
+
disabled_wrapper_attrs = (
|
|
726
|
+
' aria-disabled="true" data-disabled="true"' if is_disabled else ""
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
input_type = var_def.input
|
|
730
|
+
|
|
731
|
+
# Slider bounds from data-aware refinement (set below, used in slider branch)
|
|
732
|
+
slider_min_override: int | float | None = None
|
|
733
|
+
slider_max_override: int | float | None = None
|
|
734
|
+
slider_step_override: int | float | None = None
|
|
735
|
+
|
|
736
|
+
# Resolve options for select/multiselect — needed for both rendering and
|
|
737
|
+
# data-aware type refinement
|
|
738
|
+
option_values: list[str] = []
|
|
739
|
+
if input_type in ("select", "multiselect"):
|
|
740
|
+
option_values = list(
|
|
741
|
+
dict.fromkeys(_resolve_option_values(var_def, executor, current_values))
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
# Data-aware refinement: inspect resolved options to pick a better widget
|
|
745
|
+
if var_def.input_auto_detected and option_values:
|
|
746
|
+
refined = refine_input_type_from_data(var_def, option_values)
|
|
747
|
+
input_type = refined.input_type
|
|
748
|
+
# Apply auto-computed slider bounds (without mutating the compiled var_def)
|
|
749
|
+
if refined.slider_min is not None:
|
|
750
|
+
slider_min_override = refined.slider_min
|
|
751
|
+
slider_max_override = refined.slider_max
|
|
752
|
+
slider_step_override = refined.slider_step
|
|
753
|
+
|
|
754
|
+
if input_type in ("select", "multiselect"):
|
|
755
|
+
options = ['<option value="">-- All --</option>']
|
|
756
|
+
|
|
757
|
+
is_placeholder = current is None or current == ""
|
|
758
|
+
for opt in option_values:
|
|
759
|
+
escaped_opt = html.escape(opt)
|
|
760
|
+
selected = " selected" if str(current) == opt else ""
|
|
761
|
+
options.append(
|
|
762
|
+
f'<option value="{escaped_opt}"{selected}>{escaped_opt}</option>'
|
|
763
|
+
)
|
|
764
|
+
placeholder_attr = ' data-placeholder="true"' if is_placeholder else ""
|
|
765
|
+
|
|
766
|
+
return f"""<div class="variable-control dft-variable" data-variable-id="{escaped_name}" data-variable-label="{escaped_label}"{depends_on_attr}{description_attr}{disabled_wrapper_attrs} style="display: flex; align-items: center; gap: {control_gap}px;">
|
|
767
|
+
<label class="dft-variable-label">{escaped_label}:</label>
|
|
768
|
+
<select name="{escaped_name}" class="dft-variable-input" data-variable="{escaped_name}" onchange="updateVariable('{js_safe_name}', this.value)"{disabled_attr}{placeholder_attr}
|
|
769
|
+
style="padding: {input_padding.top}px {input_padding.right}px; border-radius: {input_border_radius}px; font-size: {variables_style.font.size}px;">
|
|
770
|
+
{"".join(options)}
|
|
771
|
+
</select>
|
|
772
|
+
</div>"""
|
|
773
|
+
|
|
774
|
+
elif input_type == "checkbox":
|
|
775
|
+
# Handle string booleans from data-aware refinement (select → checkbox)
|
|
776
|
+
if isinstance(current, str):
|
|
777
|
+
current = current.lower().strip() in ("true", "yes", "1")
|
|
778
|
+
checked = " checked" if current else ""
|
|
779
|
+
return f"""<div class="variable-control dft-variable" data-variable-id="{escaped_name}" data-variable-label="{escaped_label}"{depends_on_attr}{description_attr}{disabled_wrapper_attrs} style="display: flex; align-items: center; gap: {control_gap}px; cursor: pointer;">
|
|
780
|
+
<input type="checkbox" name="{escaped_name}" class="dft-variable-input" data-variable="{escaped_name}"
|
|
781
|
+
onchange="updateVariable('{js_safe_name}', this.checked)"{checked}{disabled_attr}
|
|
782
|
+
style="width: {input_config.widths.checkbox}px; height: {input_config.widths.checkbox}px;"/>
|
|
783
|
+
<span class="dft-variable-label">{escaped_label}</span>
|
|
784
|
+
</div>"""
|
|
785
|
+
|
|
786
|
+
elif input_type in ("slider", "range"):
|
|
787
|
+
# Data-aware overrides (all-or-none from RefinedType) take precedence
|
|
788
|
+
if slider_min_override is not None:
|
|
789
|
+
min_val = slider_min_override
|
|
790
|
+
max_val = slider_max_override
|
|
791
|
+
step_val = slider_step_override
|
|
792
|
+
else:
|
|
793
|
+
min_val = (
|
|
794
|
+
var_def.min
|
|
795
|
+
if var_def.min is not None
|
|
796
|
+
else input_config.range.default_min
|
|
797
|
+
)
|
|
798
|
+
max_val = (
|
|
799
|
+
var_def.max
|
|
800
|
+
if var_def.max is not None
|
|
801
|
+
else input_config.range.default_max
|
|
802
|
+
)
|
|
803
|
+
step_val = (
|
|
804
|
+
var_def.step
|
|
805
|
+
if var_def.step is not None
|
|
806
|
+
else input_config.range.default_step
|
|
807
|
+
)
|
|
808
|
+
raw_value = current if current is not None else min_val
|
|
809
|
+
# Escape value for HTML attribute context to prevent XSS
|
|
810
|
+
escaped_value = html.escape(str(raw_value))
|
|
811
|
+
return f"""<div class="variable-control dft-variable" data-variable-id="{escaped_name}" data-variable-label="{escaped_label}"{depends_on_attr}{description_attr}{disabled_wrapper_attrs} style="display: flex; align-items: center; gap: {control_gap}px;">
|
|
812
|
+
<label class="dft-variable-label">{escaped_label}:</label>
|
|
813
|
+
<input type="range" name="{escaped_name}" class="dft-variable-input" data-variable="{escaped_name}"
|
|
814
|
+
min="{min_val}" max="{max_val}" step="{step_val}" value="{escaped_value}"
|
|
815
|
+
oninput="this.nextElementSibling.textContent = this.value"
|
|
816
|
+
onchange="updateVariable('{js_safe_name}', this.value)"{disabled_attr}
|
|
817
|
+
style="width: {input_config.widths.range}px;"/>
|
|
818
|
+
<span class="dft-variable-slider-value" style="min-width: {input_config.widths.slider_value_min}px;">{escaped_value}</span>
|
|
819
|
+
</div>"""
|
|
820
|
+
|
|
821
|
+
elif input_type in ("text", "input"):
|
|
822
|
+
value = html.escape(str(current or ""))
|
|
823
|
+
placeholder_attr = (
|
|
824
|
+
' data-placeholder="true"' if current is None or current == "" else ""
|
|
825
|
+
)
|
|
826
|
+
return f"""<div class="variable-control dft-variable" data-variable-id="{escaped_name}" data-variable-label="{escaped_label}"{depends_on_attr}{description_attr}{disabled_wrapper_attrs} style="display: flex; align-items: center; gap: {control_gap}px;">
|
|
827
|
+
<label class="dft-variable-label">{escaped_label}:</label>
|
|
828
|
+
<input type="text" name="{escaped_name}" class="dft-variable-input" data-variable="{escaped_name}" value="{value}"
|
|
829
|
+
onchange="updateVariable('{js_safe_name}', this.value)"{disabled_attr}{placeholder_attr}
|
|
830
|
+
style="padding: {input_padding.top}px {input_padding.right}px; border-radius: {input_border_radius}px; width: {input_config.widths.text}px;"/>
|
|
831
|
+
</div>"""
|
|
832
|
+
|
|
833
|
+
elif input_type == "number":
|
|
834
|
+
# Escape value for HTML attribute context to prevent XSS
|
|
835
|
+
value = html.escape(str(current)) if current is not None else ""
|
|
836
|
+
placeholder_attr = ' data-placeholder="true"' if current is None else ""
|
|
837
|
+
min_attr = f' min="{var_def.min}"' if var_def.min is not None else ""
|
|
838
|
+
max_attr = f' max="{var_def.max}"' if var_def.max is not None else ""
|
|
839
|
+
step_attr = f' step="{var_def.step}"' if var_def.step is not None else ""
|
|
840
|
+
return f"""<div class="variable-control dft-variable" data-variable-id="{escaped_name}" data-variable-label="{escaped_label}"{depends_on_attr}{description_attr}{disabled_wrapper_attrs} style="display: flex; align-items: center; gap: {control_gap}px;">
|
|
841
|
+
<label class="dft-variable-label">{escaped_label}:</label>
|
|
842
|
+
<input type="number" name="{escaped_name}" class="dft-variable-input" data-variable="{escaped_name}" value="{value}"{min_attr}{max_attr}{step_attr}
|
|
843
|
+
onchange="updateVariable('{js_safe_name}', this.value)"{disabled_attr}{placeholder_attr}
|
|
844
|
+
style="padding: {input_padding.top}px {input_padding.right}px; border-radius: {input_border_radius}px; width: {input_config.widths.number}px;"/>
|
|
845
|
+
</div>"""
|
|
846
|
+
|
|
847
|
+
elif input_type in ("date", "datepicker"):
|
|
848
|
+
# Escape value for HTML attribute context to prevent XSS
|
|
849
|
+
value = html.escape(str(current or ""))
|
|
850
|
+
placeholder_attr = (
|
|
851
|
+
' data-placeholder="true"' if current is None or current == "" else ""
|
|
852
|
+
)
|
|
853
|
+
return f"""<div class="variable-control dft-variable" data-variable-id="{escaped_name}" data-variable-label="{escaped_label}"{depends_on_attr}{description_attr}{disabled_wrapper_attrs} style="display: flex; align-items: center; gap: {control_gap}px;">
|
|
854
|
+
<label class="dft-variable-label">{escaped_label}:</label>
|
|
855
|
+
<input type="date" name="{escaped_name}" class="dft-variable-input" data-variable="{escaped_name}" value="{value}"
|
|
856
|
+
onchange="updateVariable('{js_safe_name}', this.value)"{disabled_attr}{placeholder_attr}
|
|
857
|
+
style="padding: {input_padding.top}px {input_padding.right}px; border-radius: {input_border_radius}px;"/>
|
|
858
|
+
</div>"""
|
|
859
|
+
|
|
860
|
+
elif input_type == "daterange":
|
|
861
|
+
# Resolve initial chip state from pre-loaded values.
|
|
862
|
+
start_value = ""
|
|
863
|
+
end_value = ""
|
|
864
|
+
if isinstance(current, list) and len(current) >= 2:
|
|
865
|
+
start_value = html.escape(str(current[0]))
|
|
866
|
+
end_value = html.escape(str(current[1]))
|
|
867
|
+
chip_active = bool(start_value and end_value)
|
|
868
|
+
chip_label = (
|
|
869
|
+
_format_daterange_label(start_value, end_value)
|
|
870
|
+
if chip_active
|
|
871
|
+
else _READ_ONLY_UNSET_DATERANGE
|
|
872
|
+
)
|
|
873
|
+
chip_state = 'data-active="true"' if chip_active else 'data-placeholder="true"'
|
|
874
|
+
|
|
875
|
+
return f"""<div class="variable-control dft-variable" data-variable-id="{escaped_name}" data-variable-label="{escaped_label}"{depends_on_attr}{description_attr}{disabled_wrapper_attrs} style="display: flex; align-items: center; gap: {control_gap}px;">
|
|
876
|
+
<label class="dft-variable-label">{escaped_label}:</label>
|
|
877
|
+
<div class="dft-chip-host">
|
|
878
|
+
<button type="button" class="dft-chip" data-variable="{escaped_name}" {chip_state} aria-haspopup="true" aria-expanded="false"{disabled_attr}>
|
|
879
|
+
<span class="dft-chip-icon" aria-hidden="true">
|
|
880
|
+
<svg width="16" height="16"><use href="#dft-icon-calendar"/></svg>
|
|
881
|
+
</span>
|
|
882
|
+
<span class="dft-chip-label">{chip_label}</span>
|
|
883
|
+
</button>
|
|
884
|
+
<button type="button" class="dft-chip-clear" title="Clear" aria-label="Clear date range"{disabled_attr}>×</button>
|
|
885
|
+
<div class="dft-popover" aria-hidden="true">
|
|
886
|
+
<div class="dft-preset-rail"></div>
|
|
887
|
+
<div class="dft-calendar-area" data-variable="{escaped_name}" data-start="{start_value}" data-end="{end_value}"></div>
|
|
888
|
+
</div>
|
|
889
|
+
</div>
|
|
890
|
+
</div>"""
|
|
891
|
+
|
|
892
|
+
# Fallback: read-only display
|
|
893
|
+
return f"""<div class="variable-control dft-variable" data-variable-id="{escaped_name}" data-variable-label="{escaped_label}"{depends_on_attr}{description_attr}{disabled_wrapper_attrs} style="display: flex; align-items: center; gap: {control_gap}px;">
|
|
894
|
+
<span class="dft-variable-label">{escaped_label}:</span>
|
|
895
|
+
<span class="dft-variable-readonly-value">{html.escape(str(current or ""))}</span>
|
|
896
|
+
</div>"""
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def generate_svg_variable_script() -> str:
|
|
900
|
+
"""Generate JavaScript for SVG variable interactions.
|
|
901
|
+
|
|
902
|
+
This script is embedded in the SVG and handles:
|
|
903
|
+
- Variable change events (updateVariable function)
|
|
904
|
+
- URL parameter updates
|
|
905
|
+
- Chart loading states
|
|
906
|
+
- Parent frame communication (for playground/suite embedding)
|
|
907
|
+
- URL parameter initialization on load
|
|
908
|
+
|
|
909
|
+
Returns:
|
|
910
|
+
SVG script element with embedded JavaScript
|
|
911
|
+
"""
|
|
912
|
+
from pathlib import Path
|
|
913
|
+
|
|
914
|
+
script_path = Path(__file__).parent / "templates/scripts/variables.js"
|
|
915
|
+
return embed_svg_script(script_path.read_text())
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
# Calendar icon SVG symbol — emitted once per face via generate_svg_variable_icons().
|
|
919
|
+
# Each daterange chip references it via <use href="#dft-icon-calendar">.
|
|
920
|
+
_CALENDAR_ICON_SYMBOL = (
|
|
921
|
+
'<svg width="0" height="0" style="position:absolute;overflow:hidden" aria-hidden="true">'
|
|
922
|
+
'<symbol id="dft-icon-calendar" viewBox="0 0 24 24" fill="none"'
|
|
923
|
+
' stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">'
|
|
924
|
+
'<rect x="3.5" y="5.5" width="17" height="4.5" rx="1" fill="currentColor" opacity="0.13" stroke="none"/>'
|
|
925
|
+
'<rect x="3" y="5" width="18" height="16" rx="2" stroke-width="1.75"/>'
|
|
926
|
+
'<line x1="3" y1="10" x2="21" y2="10" stroke-width="1.75"/>'
|
|
927
|
+
'<line x1="8" y1="3" x2="8" y2="7" stroke-width="2.25"/>'
|
|
928
|
+
'<line x1="16" y1="3" x2="16" y2="7" stroke-width="2.25"/>'
|
|
929
|
+
"</symbol></svg>"
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def generate_svg_variable_icons(variable_defs: dict[str, Any]) -> str:
|
|
934
|
+
"""Generate shared SVG icon symbols for variable controls.
|
|
935
|
+
|
|
936
|
+
Emits the calendar symbol when at least one daterange control is present.
|
|
937
|
+
Emitting it once avoids duplicate id='dft-icon-calendar' when multiple
|
|
938
|
+
daterange variables appear on the same face.
|
|
939
|
+
|
|
940
|
+
Returns:
|
|
941
|
+
HTML string with hidden SVG symbol definitions, or empty string.
|
|
942
|
+
"""
|
|
943
|
+
needs_calendar = any(
|
|
944
|
+
getattr(v, "input", None) == "daterange" for v in variable_defs.values()
|
|
945
|
+
)
|
|
946
|
+
return _CALENDAR_ICON_SYMBOL if needs_calendar else ""
|