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,976 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variable interactivity for datafaces.
|
|
3
|
+
*
|
|
4
|
+
* This script handles:
|
|
5
|
+
* - Variable change events (updateVariable function)
|
|
6
|
+
* - URL parameter updates
|
|
7
|
+
* - Chart loading states
|
|
8
|
+
* - Parent frame communication (for playground/suite embedding)
|
|
9
|
+
* - URL parameter initialization on load
|
|
10
|
+
* - Variable hover highlighting
|
|
11
|
+
* - Date range chip+popover control (makeChip factory)
|
|
12
|
+
*
|
|
13
|
+
* Note: Chart menus are handled by Suite's JavaScript (init.js), not here.
|
|
14
|
+
*
|
|
15
|
+
*/
|
|
16
|
+
(function() {
|
|
17
|
+
// Guard against double execution (e.g., if script runs both on parse and via executeEmbeddedScripts)
|
|
18
|
+
if (window.__dfVariablesInitialized) return;
|
|
19
|
+
window.__dfVariablesInitialized = true;
|
|
20
|
+
|
|
21
|
+
function setVariableIfMissing(vars, name, value) {
|
|
22
|
+
if (Object.prototype.hasOwnProperty.call(vars, name)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
vars[name] = value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function markDependentChartsLoading(name) {
|
|
29
|
+
var charts = document.querySelectorAll('[data-var-' + name + ']');
|
|
30
|
+
for (var i = 0; i < charts.length; i++) {
|
|
31
|
+
var chart = charts[i];
|
|
32
|
+
// Add loading class
|
|
33
|
+
var currentClass = chart.getAttribute('class') || '';
|
|
34
|
+
if (currentClass.indexOf('loading') === -1) {
|
|
35
|
+
chart.setAttribute('class', currentClass + (currentClass ? ' ' : '') + 'loading');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Add spinner if not already present
|
|
39
|
+
if (!chart.querySelector('.dft-chart-spinner')) {
|
|
40
|
+
var spinner = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
41
|
+
spinner.setAttribute('class', 'dft-chart-spinner');
|
|
42
|
+
// Get chart bounds for centering spinner
|
|
43
|
+
var bbox = chart.getBBox ? chart.getBBox() : {width: 200, height: 200};
|
|
44
|
+
var centerX = bbox.x + bbox.width / 2;
|
|
45
|
+
var centerY = bbox.y + bbox.height / 2;
|
|
46
|
+
|
|
47
|
+
var circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
48
|
+
circle.setAttribute('cx', centerX);
|
|
49
|
+
circle.setAttribute('cy', centerY);
|
|
50
|
+
circle.setAttribute('r', '14');
|
|
51
|
+
circle.setAttribute('fill', 'none');
|
|
52
|
+
circle.setAttribute('stroke', '#e0e0e0');
|
|
53
|
+
circle.setAttribute('stroke-width', '3');
|
|
54
|
+
circle.setAttribute('stroke-dasharray', '20 60');
|
|
55
|
+
circle.setAttribute('stroke-dashoffset', '0');
|
|
56
|
+
circle.style.animation = 'dft-spin 0.8s linear infinite';
|
|
57
|
+
|
|
58
|
+
spinner.appendChild(circle);
|
|
59
|
+
chart.appendChild(spinner);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return charts;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function scheduleLoadingCleanup(charts) {
|
|
67
|
+
setTimeout(function() {
|
|
68
|
+
for (var i = 0; i < charts.length; i++) {
|
|
69
|
+
var chart = charts[i];
|
|
70
|
+
var currentClass = chart.getAttribute('class') || '';
|
|
71
|
+
chart.setAttribute('class', currentClass.replace(/\s*loading\s*/g, ' ').trim());
|
|
72
|
+
var spinner = chart.querySelector('.dft-chart-spinner');
|
|
73
|
+
if (spinner) {
|
|
74
|
+
spinner.remove();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}, 5000);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Update URL and reload (or notify parent)
|
|
81
|
+
function updateVariable(name, value) {
|
|
82
|
+
// Mark dependent charts as loading (add loading class and spinner)
|
|
83
|
+
var charts = markDependentChartsLoading(name);
|
|
84
|
+
|
|
85
|
+
// Restore after timeout (fallback if reload fails or is prevented)
|
|
86
|
+
scheduleLoadingCleanup(charts);
|
|
87
|
+
|
|
88
|
+
// Update URL
|
|
89
|
+
var url = new URL(window.location);
|
|
90
|
+
if (value === '' || value === null || value === false) {
|
|
91
|
+
url.searchParams.delete(name);
|
|
92
|
+
} else {
|
|
93
|
+
url.searchParams.set(name, String(value));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Notify parent (suite/playground) or reload (standalone)
|
|
97
|
+
// Using '*' is safe here because:
|
|
98
|
+
// 1. Iframe content is generated by us (not user input)
|
|
99
|
+
// 2. Parent checks message.type before processing
|
|
100
|
+
// 3. Blob URLs have null origin, so specific origin targeting doesn't work
|
|
101
|
+
if (window.parent !== window) {
|
|
102
|
+
var vars = getAllVariableValues();
|
|
103
|
+
// Form controls haven't re-rendered yet, so patch the outgoing
|
|
104
|
+
// snapshot with the pending value before notifying the parent.
|
|
105
|
+
setVariableIfMissing(vars, name, value);
|
|
106
|
+
window.parent.postMessage({
|
|
107
|
+
type: 'dft-variable-change',
|
|
108
|
+
variables: vars
|
|
109
|
+
}, '*');
|
|
110
|
+
} else if (typeof window.__dfHandleVariableUpdate === 'function') {
|
|
111
|
+
// Suite registers this hook to re-render in place (no page reload).
|
|
112
|
+
window.__dfHandleVariableUpdate(url);
|
|
113
|
+
} else {
|
|
114
|
+
// Plain dft serve: full page reload. Save scroll so initializeFromURL
|
|
115
|
+
// can restore it after the new page renders.
|
|
116
|
+
try {
|
|
117
|
+
sessionStorage.setItem('__dfScrollY_' + window.location.pathname, String(window.scrollY));
|
|
118
|
+
} catch (e) { /* sessionStorage may be unavailable */ }
|
|
119
|
+
window.location.href = url.toString();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Expose functions to window for inline HTML event handlers (onchange="updateVariable(...)")
|
|
124
|
+
// These are used by foreignObject controls rendered by render_html_control_for_svg
|
|
125
|
+
window.updateVariable = updateVariable;
|
|
126
|
+
|
|
127
|
+
// ── Date range chip+popover ──────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
// 8 presets: Today / Last 7 / Last 28 / Last 90 / Last 12 months / MTD / YTD / Custom
|
|
130
|
+
var PRESETS = [
|
|
131
|
+
{ id: 'today', label: 'Today' },
|
|
132
|
+
{ id: 'last_7_days', label: 'Last 7 days' },
|
|
133
|
+
{ id: 'last_28_days', label: 'Last 28 days' },
|
|
134
|
+
{ id: 'last_90_days', label: 'Last 90 days' },
|
|
135
|
+
{ id: 'last_12_months', label: 'Last 12 months' },
|
|
136
|
+
{ id: 'divider' },
|
|
137
|
+
{ id: 'mtd', label: 'Month to date' },
|
|
138
|
+
{ id: 'ytd', label: 'Year to date' },
|
|
139
|
+
{ id: 'divider' },
|
|
140
|
+
{ id: 'custom', label: 'Custom' },
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// All date math is relative to "today at midnight local time" so that
|
|
144
|
+
// a frozen today in tests resolves deterministically.
|
|
145
|
+
function resolvePreset(id) {
|
|
146
|
+
var now = new Date();
|
|
147
|
+
var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
148
|
+
var end = new Date(today);
|
|
149
|
+
var start = new Date(today);
|
|
150
|
+
switch (id) {
|
|
151
|
+
case 'today': break;
|
|
152
|
+
case 'last_7_days': start.setDate(end.getDate() - 6); break;
|
|
153
|
+
case 'last_28_days': start.setDate(end.getDate() - 27); break;
|
|
154
|
+
case 'last_90_days': start.setDate(end.getDate() - 89); break;
|
|
155
|
+
// Compute start as one year back from today + 1 day in a single constructor call
|
|
156
|
+
// to avoid the two-step setFullYear+setDate rollover on Feb 29 leap years.
|
|
157
|
+
case 'last_12_months': start = new Date(end.getFullYear() - 1, end.getMonth(), end.getDate() + 1); break;
|
|
158
|
+
case 'mtd': start = new Date(end.getFullYear(), end.getMonth(), 1); break;
|
|
159
|
+
case 'ytd': start = new Date(end.getFullYear(), 0, 1); break;
|
|
160
|
+
case 'custom': return null;
|
|
161
|
+
default: return null;
|
|
162
|
+
}
|
|
163
|
+
return [start, end];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ISO date string from a Date object (YYYY-MM-DD local time).
|
|
167
|
+
function toISO(d) {
|
|
168
|
+
var m = String(d.getMonth() + 1).padStart(2, '0');
|
|
169
|
+
var day = String(d.getDate()).padStart(2, '0');
|
|
170
|
+
return d.getFullYear() + '-' + m + '-' + day;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ISO YYYY-MM-DD shape check (does not check calendar validity, just structure).
|
|
174
|
+
var _ISO_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
175
|
+
|
|
176
|
+
// Date from an ISO string without timezone shift (treat as local midnight).
|
|
177
|
+
// Returns null when the string is not a well-formed YYYY-MM-DD literal.
|
|
178
|
+
function fromISO(s) {
|
|
179
|
+
if (!_ISO_RE.test(s)) return null;
|
|
180
|
+
var parts = s.split('-');
|
|
181
|
+
var d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
|
|
182
|
+
// Reject dates that JS rolled over (e.g. Feb 30 → Mar 2).
|
|
183
|
+
if (d.getFullYear() !== parseInt(parts[0], 10) ||
|
|
184
|
+
d.getMonth() !== parseInt(parts[1], 10) - 1 ||
|
|
185
|
+
d.getDate() !== parseInt(parts[2], 10)) return null;
|
|
186
|
+
return d;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Human-readable chip label from a [start, end] Date pair.
|
|
190
|
+
// Collapses same-day / same-month / same-year / cross-year ranges.
|
|
191
|
+
var MONTHS_SHORT = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
192
|
+
function fmtDate(d) { return d.getDate() + ' ' + MONTHS_SHORT[d.getMonth()] + ' ' + d.getFullYear(); }
|
|
193
|
+
function fmtDateNoYr(d) { return d.getDate() + ' ' + MONTHS_SHORT[d.getMonth()]; }
|
|
194
|
+
|
|
195
|
+
function formatRange(start, end) {
|
|
196
|
+
var sameDay = start.getTime() === end.getTime();
|
|
197
|
+
var sameMonth = start.getFullYear() === end.getFullYear() && start.getMonth() === end.getMonth();
|
|
198
|
+
var sameYear = start.getFullYear() === end.getFullYear();
|
|
199
|
+
if (sameDay) return fmtDate(start);
|
|
200
|
+
if (sameMonth) return start.getDate() + '–' + end.getDate() + ' ' + MONTHS_SHORT[start.getMonth()] + ' ' + start.getFullYear();
|
|
201
|
+
if (sameYear) return fmtDateNoYr(start) + ' – ' + fmtDate(end);
|
|
202
|
+
return fmtDate(start) + ' – ' + fmtDate(end);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function buildRail(railEl, onPick) {
|
|
206
|
+
railEl.innerHTML = '';
|
|
207
|
+
PRESETS.forEach(function(p) {
|
|
208
|
+
if (p.id === 'divider') {
|
|
209
|
+
var div = document.createElement('div');
|
|
210
|
+
div.className = 'dft-preset-divider';
|
|
211
|
+
railEl.appendChild(div);
|
|
212
|
+
} else {
|
|
213
|
+
var btn = document.createElement('button');
|
|
214
|
+
btn.type = 'button';
|
|
215
|
+
btn.textContent = p.label;
|
|
216
|
+
btn.dataset.preset = p.id;
|
|
217
|
+
btn.addEventListener('click', function() { onPick(p); });
|
|
218
|
+
railEl.appendChild(btn);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function markRailActive(railEl, presetId) {
|
|
224
|
+
railEl.querySelectorAll('button').forEach(function(b) {
|
|
225
|
+
b.classList.toggle('dft-preset-active', b.dataset.preset === presetId);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Given a [start, end] pair, return the preset id whose resolved range
|
|
230
|
+
// matches, or 'custom' for a non-matching range, or null for an empty range.
|
|
231
|
+
// Used to restore the active-preset highlight after an iframe re-render
|
|
232
|
+
// wipes the rail's local mark.
|
|
233
|
+
function matchPreset(range) {
|
|
234
|
+
if (!range[0] || !range[1]) return null;
|
|
235
|
+
var s = toISO(range[0]), e = toISO(range[1]);
|
|
236
|
+
for (var i = 0; i < PRESETS.length; i++) {
|
|
237
|
+
var p = PRESETS[i];
|
|
238
|
+
if (!p.id || p.id === 'divider' || p.id === 'custom') continue;
|
|
239
|
+
var r = resolvePreset(p.id);
|
|
240
|
+
if (r && toISO(r[0]) === s && toISO(r[1]) === e) return p.id;
|
|
241
|
+
}
|
|
242
|
+
return 'custom';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Per-instance chip+popover factory.
|
|
246
|
+
// rootEl is the .dft-chip-host element; varName is the DFT variable id.
|
|
247
|
+
// Returns a handle so the global outside-click + Esc handlers can reach in.
|
|
248
|
+
function makeChip(rootEl, varName) {
|
|
249
|
+
var trigger = rootEl.querySelector('.dft-chip');
|
|
250
|
+
var labelEl = rootEl.querySelector('.dft-chip-label');
|
|
251
|
+
var clearBtn = rootEl.querySelector('.dft-chip-clear');
|
|
252
|
+
var popover = rootEl.querySelector('.dft-popover');
|
|
253
|
+
var rail = rootEl.querySelector('.dft-preset-rail');
|
|
254
|
+
var calEl = rootEl.querySelector('.dft-calendar-area');
|
|
255
|
+
|
|
256
|
+
var range = [null, null];
|
|
257
|
+
var hoverDate = null;
|
|
258
|
+
var viewMonth = new Date();
|
|
259
|
+
var now = new Date();
|
|
260
|
+
var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
261
|
+
|
|
262
|
+
// Restore server-rendered initial value if present (data-start / data-end).
|
|
263
|
+
var initStart = calEl.dataset.start;
|
|
264
|
+
var initEnd = calEl.dataset.end;
|
|
265
|
+
if (initStart && initEnd) {
|
|
266
|
+
var parsedStart = fromISO(initStart);
|
|
267
|
+
var parsedEnd = fromISO(initEnd);
|
|
268
|
+
if (parsedStart && parsedEnd) {
|
|
269
|
+
range = [parsedStart, parsedEnd];
|
|
270
|
+
viewMonth = new Date(range[1].getFullYear(), range[1].getMonth(), 1);
|
|
271
|
+
}
|
|
272
|
+
// Chip label and state were server-rendered; don't repaint here.
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Persist the resolved [start, end] pair as ISO strings via the standard variable pipeline.
|
|
276
|
+
function commitRange() {
|
|
277
|
+
if (range[0] && range[1]) {
|
|
278
|
+
calEl.dataset.start = toISO(range[0]);
|
|
279
|
+
calEl.dataset.end = toISO(range[1]);
|
|
280
|
+
updateVariable(varName, JSON.stringify([toISO(range[0]), toISO(range[1])]));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function setChipValue(text) {
|
|
285
|
+
labelEl.textContent = text;
|
|
286
|
+
trigger.setAttribute('data-active', 'true');
|
|
287
|
+
trigger.removeAttribute('data-placeholder');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function setChipEmpty() {
|
|
291
|
+
labelEl.textContent = 'All dates';
|
|
292
|
+
trigger.removeAttribute('data-active');
|
|
293
|
+
trigger.setAttribute('data-placeholder', 'true');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function clearAll() {
|
|
297
|
+
range = [null, null];
|
|
298
|
+
hoverDate = null;
|
|
299
|
+
calEl.dataset.start = '';
|
|
300
|
+
calEl.dataset.end = '';
|
|
301
|
+
setChipEmpty();
|
|
302
|
+
markRailActive(rail, null);
|
|
303
|
+
rebuildCalendar();
|
|
304
|
+
updateVariable(varName, '');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── Calendar render pipeline ─────────────────────────────────────────
|
|
308
|
+
// rebuildCalendar — full DOM rebuild. Called on pickDay, month nav, preset.
|
|
309
|
+
// paintCalendarState — class-only mutation on existing cells. Called on hover.
|
|
310
|
+
// Re-rendering on hover replaces the hovered cell and the next mouseenter
|
|
311
|
+
// doesn't fire until the user moves away and back — sticky-hover bug.
|
|
312
|
+
// Class mutation leaves the DOM stable and avoids this.
|
|
313
|
+
|
|
314
|
+
function rebuildCalendar() {
|
|
315
|
+
var year = viewMonth.getFullYear();
|
|
316
|
+
var month = viewMonth.getMonth();
|
|
317
|
+
var firstDay = new Date(year, month, 1);
|
|
318
|
+
var startWeek = firstDay.getDay();
|
|
319
|
+
var monthName = firstDay.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
|
320
|
+
|
|
321
|
+
// Build via DOM API (not innerHTML) so structural nesting errors
|
|
322
|
+
// become type errors rather than silent mis-parses.
|
|
323
|
+
calEl.innerHTML = '';
|
|
324
|
+
|
|
325
|
+
// Header: ‹ Month YYYY ›
|
|
326
|
+
var header = document.createElement('div');
|
|
327
|
+
header.className = 'dft-cal-header';
|
|
328
|
+
|
|
329
|
+
var prevBtn = document.createElement('button');
|
|
330
|
+
prevBtn.type = 'button';
|
|
331
|
+
prevBtn.className = 'dft-cal-nav';
|
|
332
|
+
prevBtn.setAttribute('aria-label', 'Previous month');
|
|
333
|
+
prevBtn.textContent = '‹'; // ‹
|
|
334
|
+
prevBtn.addEventListener('click', function() {
|
|
335
|
+
viewMonth = new Date(year, month - 1, 1);
|
|
336
|
+
rebuildCalendar();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
var monthSpan = document.createElement('span');
|
|
340
|
+
monthSpan.className = 'dft-cal-month';
|
|
341
|
+
monthSpan.textContent = monthName;
|
|
342
|
+
|
|
343
|
+
var nextBtn = document.createElement('button');
|
|
344
|
+
nextBtn.type = 'button';
|
|
345
|
+
nextBtn.className = 'dft-cal-nav';
|
|
346
|
+
nextBtn.setAttribute('aria-label', 'Next month');
|
|
347
|
+
nextBtn.textContent = '›'; // ›
|
|
348
|
+
nextBtn.addEventListener('click', function() {
|
|
349
|
+
viewMonth = new Date(year, month + 1, 1);
|
|
350
|
+
rebuildCalendar();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
header.appendChild(prevBtn);
|
|
354
|
+
header.appendChild(monthSpan);
|
|
355
|
+
header.appendChild(nextBtn);
|
|
356
|
+
calEl.appendChild(header);
|
|
357
|
+
|
|
358
|
+
// Grid: 7 day-of-week labels + 42 cells (6 rows × 7 cols, fixed height)
|
|
359
|
+
var grid = document.createElement('div');
|
|
360
|
+
grid.className = 'dft-cal-grid';
|
|
361
|
+
|
|
362
|
+
var DOW = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
|
363
|
+
DOW.forEach(function(d) {
|
|
364
|
+
var dow = document.createElement('div');
|
|
365
|
+
dow.className = 'dft-cal-dow';
|
|
366
|
+
dow.textContent = d;
|
|
367
|
+
grid.appendChild(dow);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
var cursor = new Date(year, month, 1 - startWeek);
|
|
371
|
+
for (var i = 0; i < 42; i++) {
|
|
372
|
+
var dt = new Date(cursor);
|
|
373
|
+
var isOther = dt.getMonth() !== month;
|
|
374
|
+
var isToday = dt.getTime() === today.getTime();
|
|
375
|
+
var cell = document.createElement('button');
|
|
376
|
+
cell.type = 'button';
|
|
377
|
+
cell.className = 'dft-cal-cell';
|
|
378
|
+
if (isOther) {
|
|
379
|
+
cell.classList.add('dft-other-month');
|
|
380
|
+
cell.tabIndex = -1;
|
|
381
|
+
}
|
|
382
|
+
if (isToday) cell.classList.add('dft-today');
|
|
383
|
+
cell.textContent = dt.getDate();
|
|
384
|
+
cell.dataset.day = String(dt.getTime());
|
|
385
|
+
cell.setAttribute('aria-label', dt.toLocaleDateString('en-US', {weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'}));
|
|
386
|
+
// Capture dt for closure
|
|
387
|
+
(function(cellEl, cellDate) {
|
|
388
|
+
cellEl.addEventListener('click', function() { pickDay(cellDate.getTime()); });
|
|
389
|
+
cellEl.addEventListener('mouseenter', function() {
|
|
390
|
+
if (range[0] && !range[1]) {
|
|
391
|
+
hoverDate = new Date(cellDate.getTime());
|
|
392
|
+
paintCalendarState();
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}(cell, dt));
|
|
396
|
+
grid.appendChild(cell);
|
|
397
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
398
|
+
}
|
|
399
|
+
calEl.appendChild(grid);
|
|
400
|
+
|
|
401
|
+
// Footer: [Clear] | [Apply]. Apply is always present; Clear only
|
|
402
|
+
// when there's a selection to clear.
|
|
403
|
+
//
|
|
404
|
+
// Lingering-popover pattern: adopt the auto-commit half (range[1]
|
|
405
|
+
// pick writes the URL), drop the auto-close half (popover stays
|
|
406
|
+
// open until Apply). Apply's only job is closing the popover; the
|
|
407
|
+
// commit already happened on pickDay. Matches Stripe's auto-commit
|
|
408
|
+
// + Linear's lingering popover.
|
|
409
|
+
var footer = document.createElement('div');
|
|
410
|
+
footer.className = 'dft-cal-footer';
|
|
411
|
+
if (range[0] !== null) {
|
|
412
|
+
var clearAction = document.createElement('button');
|
|
413
|
+
clearAction.type = 'button';
|
|
414
|
+
clearAction.className = 'dft-cal-action';
|
|
415
|
+
clearAction.setAttribute('data-action', 'clear');
|
|
416
|
+
clearAction.textContent = 'Clear';
|
|
417
|
+
// Push Clear to the left; Apply pins to the right via the
|
|
418
|
+
// footer's justify-content: flex-end. CSS-side `:not()` and
|
|
419
|
+
// quoted attribute selectors trip resvg's parser in the PNG
|
|
420
|
+
// converter, so we set this inline rather than in a stylesheet.
|
|
421
|
+
clearAction.style.marginRight = 'auto';
|
|
422
|
+
footer.appendChild(clearAction);
|
|
423
|
+
}
|
|
424
|
+
var applyAction = document.createElement('button');
|
|
425
|
+
applyAction.type = 'button';
|
|
426
|
+
applyAction.className = 'dft-cal-action dft-cal-action-primary';
|
|
427
|
+
applyAction.setAttribute('data-action', 'apply');
|
|
428
|
+
applyAction.textContent = 'Apply';
|
|
429
|
+
footer.appendChild(applyAction);
|
|
430
|
+
// Single dispatch on the footer — matches the prototype's
|
|
431
|
+
// doAction() pattern. Binding to the footer (recreated each
|
|
432
|
+
// rebuild) avoids re-attaching per-button listeners.
|
|
433
|
+
footer.addEventListener('click', function(e) {
|
|
434
|
+
var btn = e.target.closest('[data-action]');
|
|
435
|
+
if (!btn) return;
|
|
436
|
+
var action = btn.getAttribute('data-action');
|
|
437
|
+
if (action === 'clear') clearAll();
|
|
438
|
+
else if (action === 'apply') closePopover();
|
|
439
|
+
});
|
|
440
|
+
calEl.appendChild(footer);
|
|
441
|
+
|
|
442
|
+
paintCalendarState();
|
|
443
|
+
markRailActive(rail, matchPreset(range));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function paintCalendarState() {
|
|
447
|
+
var startTs = range[0] && range[0].getTime();
|
|
448
|
+
var endTs = range[1] && range[1].getTime();
|
|
449
|
+
var previewing = startTs && !endTs && hoverDate;
|
|
450
|
+
var hoverTs = previewing && hoverDate.getTime();
|
|
451
|
+
var lo = previewing ? Math.min(startTs, hoverTs) : null;
|
|
452
|
+
var hi = previewing ? Math.max(startTs, hoverTs) : null;
|
|
453
|
+
|
|
454
|
+
calEl.querySelectorAll('.dft-cal-cell').forEach(function(c) {
|
|
455
|
+
var ts = parseInt(c.dataset.day, 10);
|
|
456
|
+
var isStart = startTs && ts === startTs;
|
|
457
|
+
var isEnd = endTs && ts === endTs;
|
|
458
|
+
var inRange = startTs && endTs && ts > startTs && ts < endTs;
|
|
459
|
+
var inPreview = previewing && ts > lo && ts < hi;
|
|
460
|
+
var isPreviewEnd = previewing && ts === hoverTs && ts !== startTs;
|
|
461
|
+
|
|
462
|
+
c.classList.toggle('dft-selected', Boolean(isStart || isEnd));
|
|
463
|
+
c.classList.toggle('dft-range-start', Boolean(isStart));
|
|
464
|
+
c.classList.toggle('dft-range-end', Boolean(isEnd));
|
|
465
|
+
c.classList.toggle('dft-in-range', Boolean(inRange));
|
|
466
|
+
c.classList.toggle('dft-preview-in-range', Boolean(inPreview));
|
|
467
|
+
c.classList.toggle('dft-preview-end', Boolean(isPreviewEnd));
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function pickDay(timestamp) {
|
|
472
|
+
var dt = new Date(timestamp);
|
|
473
|
+
if (dt.getMonth() !== viewMonth.getMonth()) {
|
|
474
|
+
viewMonth = new Date(dt.getFullYear(), dt.getMonth(), 1);
|
|
475
|
+
}
|
|
476
|
+
if (!range[0] || (range[0] && range[1])) {
|
|
477
|
+
range = [dt, null];
|
|
478
|
+
} else if (dt < range[0]) {
|
|
479
|
+
range = [dt, range[0]];
|
|
480
|
+
} else if (dt.getTime() === range[0].getTime()) {
|
|
481
|
+
range = [dt, dt];
|
|
482
|
+
} else {
|
|
483
|
+
range = [range[0], dt];
|
|
484
|
+
}
|
|
485
|
+
hoverDate = null;
|
|
486
|
+
if (range[0] && range[1]) {
|
|
487
|
+
setChipValue(formatRange(range[0], range[1]));
|
|
488
|
+
markRailActive(rail, 'custom');
|
|
489
|
+
commitRange();
|
|
490
|
+
} else {
|
|
491
|
+
markRailActive(rail, null);
|
|
492
|
+
}
|
|
493
|
+
rebuildCalendar();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ── Wire up listeners ────────────────────────────────────────────────
|
|
497
|
+
clearBtn.addEventListener('click', function(e) {
|
|
498
|
+
e.stopPropagation();
|
|
499
|
+
clearAll();
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
buildRail(rail, function(p) {
|
|
503
|
+
var r = resolvePreset(p.id);
|
|
504
|
+
if (r) {
|
|
505
|
+
range = r;
|
|
506
|
+
viewMonth = new Date(r[1].getFullYear(), r[1].getMonth(), 1);
|
|
507
|
+
setChipValue(p.label);
|
|
508
|
+
markRailActive(rail, p.id);
|
|
509
|
+
commitRange();
|
|
510
|
+
} else {
|
|
511
|
+
markRailActive(rail, p.id);
|
|
512
|
+
}
|
|
513
|
+
rebuildCalendar();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// Popover lives in the SVG foreignObject by default — but SVG paints in
|
|
517
|
+
// document order, so later rows paint over the popover (overflow:visible
|
|
518
|
+
// can't escape paint order). On open, reparent the popover to document.body
|
|
519
|
+
// and switch to position:fixed so it floats above the SVG entirely.
|
|
520
|
+
var popoverOriginalParent = popover.parentNode;
|
|
521
|
+
// CSS custom properties for theme colors are set inline on .dft-variables.
|
|
522
|
+
// Moving the popover to document.body breaks that cascade, so we copy
|
|
523
|
+
// the props that should track the theme onto the popover itself.
|
|
524
|
+
//
|
|
525
|
+
// We deliberately do NOT carry --dft-text-color / --dft-muted-color /
|
|
526
|
+
// --dft-input-bg: the popover is an always-light card (--dft-popover-bg
|
|
527
|
+
// defaults to white), and copying dark-theme text colors onto a white
|
|
528
|
+
// card produces invisible-on-white ghost text. The popover's CSS
|
|
529
|
+
// fallbacks (#333 ink, #6c757d muted, #ced4da border) read correctly
|
|
530
|
+
// on the white card across every shipped theme. We do carry:
|
|
531
|
+
// - --dft-accent-color so Apply, range tint, and focus rings track
|
|
532
|
+
// the theme;
|
|
533
|
+
// - --dft-popover-rail-bg so the preset rail tracks the theme's
|
|
534
|
+
// subtle-surface step (cream on editorial-cream, gray-50 on the
|
|
535
|
+
// grays-scaffold themes).
|
|
536
|
+
var THEME_PROPS = ['--dft-accent-color', '--dft-popover-rail-bg'];
|
|
537
|
+
function snapshotThemePropsFromVariables() {
|
|
538
|
+
var src = popoverOriginalParent.closest('.dft-variables');
|
|
539
|
+
if (!src) return;
|
|
540
|
+
THEME_PROPS.forEach(function(p) {
|
|
541
|
+
var v = src.style.getPropertyValue(p);
|
|
542
|
+
if (v) popover.style.setProperty(p, v);
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
// Position the popover relative to the chip's viewport-coords, auto-
|
|
546
|
+
// flipping to a right-edge anchor when the default left-anchor would
|
|
547
|
+
// push the popover past the viewport's right edge.
|
|
548
|
+
//
|
|
549
|
+
// The popover stays at native pixel size regardless of the SVG's
|
|
550
|
+
// render scale — this is chrome, not content. Matches native browser
|
|
551
|
+
// dropdown / datepicker behavior; chrome doesn't zoom with the page.
|
|
552
|
+
// A prior QA round confirmed this stance over the alternative (apply
|
|
553
|
+
// transform: scale() to match the SVG scale). Don't reintroduce
|
|
554
|
+
// scaling without re-litigating.
|
|
555
|
+
function positionPopoverFixed() {
|
|
556
|
+
var rect = trigger.getBoundingClientRect();
|
|
557
|
+
popover.style.top = (rect.bottom + 6) + 'px';
|
|
558
|
+
var popoverWidth = popover.getBoundingClientRect().width;
|
|
559
|
+
var margin = 8;
|
|
560
|
+
if (rect.left + popoverWidth > window.innerWidth - margin) {
|
|
561
|
+
// Right-anchor: align popover's right edge with chip's right edge.
|
|
562
|
+
// Clamp to a minimum of margin so a popover wider than the
|
|
563
|
+
// viewport still respects the left gutter.
|
|
564
|
+
popover.style.left = Math.max(margin, rect.right - popoverWidth) + 'px';
|
|
565
|
+
} else {
|
|
566
|
+
popover.style.left = rect.left + 'px';
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function openPopover() {
|
|
570
|
+
// Standalone-SVG documents (no <body>) can't be reparented to —
|
|
571
|
+
// skip the escape hatch and let the popover render in place.
|
|
572
|
+
if (document.body && popover.parentNode !== document.body) {
|
|
573
|
+
snapshotThemePropsFromVariables();
|
|
574
|
+
document.body.appendChild(popover);
|
|
575
|
+
popover.style.position = 'fixed';
|
|
576
|
+
}
|
|
577
|
+
popover.classList.add('dft-popover-open');
|
|
578
|
+
trigger.setAttribute('aria-expanded', 'true');
|
|
579
|
+
popover.setAttribute('aria-hidden', 'false');
|
|
580
|
+
// Position AFTER applying dft-popover-open: the class flips
|
|
581
|
+
// display from none to flex, so getBoundingClientRect inside
|
|
582
|
+
// positionPopoverFixed returns the popover's real width for the
|
|
583
|
+
// right-edge collision check.
|
|
584
|
+
positionPopoverFixed();
|
|
585
|
+
// Mark this chip as open so the prototype's lingering-popover
|
|
586
|
+
// intent survives an iframe re-render after commit. The new chip
|
|
587
|
+
// in the re-rendered iframe checks this flag on init.
|
|
588
|
+
try { sessionStorage.setItem('__dfChipOpen_' + varName, '1'); }
|
|
589
|
+
catch (e) { /* sessionStorage may be unavailable */ }
|
|
590
|
+
}
|
|
591
|
+
function closePopover() {
|
|
592
|
+
popover.classList.remove('dft-popover-open');
|
|
593
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
594
|
+
popover.setAttribute('aria-hidden', 'true');
|
|
595
|
+
if (popover.parentNode === document.body) {
|
|
596
|
+
popover.style.position = '';
|
|
597
|
+
popover.style.top = '';
|
|
598
|
+
popover.style.left = '';
|
|
599
|
+
popoverOriginalParent.appendChild(popover);
|
|
600
|
+
}
|
|
601
|
+
try { sessionStorage.removeItem('__dfChipOpen_' + varName); }
|
|
602
|
+
catch (e) { /* sessionStorage may be unavailable */ }
|
|
603
|
+
}
|
|
604
|
+
trigger.addEventListener('click', function(e) {
|
|
605
|
+
e.stopPropagation();
|
|
606
|
+
if (popover.classList.contains('dft-popover-open')) {
|
|
607
|
+
closePopover();
|
|
608
|
+
} else {
|
|
609
|
+
openPopover();
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
// Reposition (or close) when the page scrolls / resizes — a fixed popover
|
|
613
|
+
// doesn't track its anchor on its own.
|
|
614
|
+
window.addEventListener('scroll', function() {
|
|
615
|
+
if (popover.classList.contains('dft-popover-open')) positionPopoverFixed();
|
|
616
|
+
}, true);
|
|
617
|
+
window.addEventListener('resize', function() {
|
|
618
|
+
if (popover.classList.contains('dft-popover-open')) positionPopoverFixed();
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// Stop clicks inside the popover from bubbling to document — prevents
|
|
622
|
+
// the outside-click handler from firing on calendar cell clicks that
|
|
623
|
+
// rebuild the DOM (the just-clicked cell would become an orphan node
|
|
624
|
+
// and trigger.contains(target) would return false, closing the popover).
|
|
625
|
+
popover.addEventListener('click', function(e) { e.stopPropagation(); });
|
|
626
|
+
|
|
627
|
+
// Clear hover preview when the cursor leaves the calendar area.
|
|
628
|
+
calEl.addEventListener('mouseleave', function() {
|
|
629
|
+
if (hoverDate) {
|
|
630
|
+
hoverDate = null;
|
|
631
|
+
paintCalendarState();
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
rebuildCalendar();
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
trigger: trigger,
|
|
639
|
+
popover: popover,
|
|
640
|
+
varName: varName,
|
|
641
|
+
open: openPopover,
|
|
642
|
+
close: closePopover,
|
|
643
|
+
isOpen: function() { return popover.classList.contains('dft-popover-open'); },
|
|
644
|
+
// Restore chip state from a [startISO, endISO] pair (used by initializeFromURL).
|
|
645
|
+
// Silently ignores malformed ISO strings — leaves chip in placeholder state.
|
|
646
|
+
restoreRange: function(startISO, endISO) {
|
|
647
|
+
if (!startISO || !endISO) return;
|
|
648
|
+
var s = fromISO(startISO);
|
|
649
|
+
var e = fromISO(endISO);
|
|
650
|
+
if (!s || !e) return; // invalid ISO — leave chip in placeholder state
|
|
651
|
+
range = [s, e];
|
|
652
|
+
viewMonth = new Date(range[1].getFullYear(), range[1].getMonth(), 1);
|
|
653
|
+
calEl.dataset.start = startISO;
|
|
654
|
+
calEl.dataset.end = endISO;
|
|
655
|
+
setChipValue(formatRange(range[0], range[1]));
|
|
656
|
+
rebuildCalendar();
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ── Global chip registration + outside-click + Esc ──────────────────────
|
|
662
|
+
|
|
663
|
+
var _chipInstances = [];
|
|
664
|
+
|
|
665
|
+
// The controls stylesheet ships embedded in the SVG's <defs><style>, which
|
|
666
|
+
// browsers scope to the SVG subtree — so once the popover is reparented to
|
|
667
|
+
// document.body it loses every rule. Clone the embedded stylesheet content
|
|
668
|
+
// into document.head once so the popover renders correctly anywhere.
|
|
669
|
+
function _liftControlsStylesIntoHead() {
|
|
670
|
+
if (!document.head) return;
|
|
671
|
+
if (document.head.querySelector('style[data-dft-controls-lifted]')) return;
|
|
672
|
+
var styleEls = document.querySelectorAll('svg style');
|
|
673
|
+
if (!styleEls.length) return;
|
|
674
|
+
var css = '';
|
|
675
|
+
styleEls.forEach(function(s) { css += s.textContent + '\n'; });
|
|
676
|
+
var lifted = document.createElement('style');
|
|
677
|
+
lifted.setAttribute('data-dft-controls-lifted', 'true');
|
|
678
|
+
lifted.textContent = css;
|
|
679
|
+
document.head.appendChild(lifted);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function _initAllChips() {
|
|
683
|
+
// Find all chip hosts that have not been initialized yet.
|
|
684
|
+
var hosts = document.querySelectorAll('.dft-chip-host:not([data-chip-initialized])');
|
|
685
|
+
if (hosts.length) _liftControlsStylesIntoHead();
|
|
686
|
+
hosts.forEach(function(host) {
|
|
687
|
+
var calArea = host.querySelector('.dft-calendar-area');
|
|
688
|
+
if (!calArea) return;
|
|
689
|
+
var varName = calArea.getAttribute('data-variable');
|
|
690
|
+
if (!varName) return;
|
|
691
|
+
host.setAttribute('data-chip-initialized', 'true');
|
|
692
|
+
var inst = makeChip(host, varName);
|
|
693
|
+
_chipInstances.push(inst);
|
|
694
|
+
// If this chip was open right before an auto-commit triggered an
|
|
695
|
+
// iframe re-render, reopen it so the prototype's "lingering
|
|
696
|
+
// popover" intent survives the playground's blob-URL swap.
|
|
697
|
+
try {
|
|
698
|
+
if (sessionStorage.getItem('__dfChipOpen_' + varName) === '1') {
|
|
699
|
+
inst.open();
|
|
700
|
+
}
|
|
701
|
+
} catch (e) { /* sessionStorage may be unavailable */ }
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Outside-click closes all open popovers.
|
|
706
|
+
document.addEventListener('click', function(e) {
|
|
707
|
+
_chipInstances.forEach(function(inst) {
|
|
708
|
+
if (!inst.trigger.contains(e.target)) inst.close();
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Esc closes whichever instance is open and returns focus to its trigger.
|
|
713
|
+
document.addEventListener('keydown', function(e) {
|
|
714
|
+
if (e.key !== 'Escape') return;
|
|
715
|
+
_chipInstances.forEach(function(inst) {
|
|
716
|
+
if (inst.isOpen()) {
|
|
717
|
+
inst.close();
|
|
718
|
+
inst.trigger.focus();
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// Opening one popover closes all others.
|
|
724
|
+
// Implemented by attaching a listener after _initAllChips so we have all instances.
|
|
725
|
+
function _wireExclusiveOpen() {
|
|
726
|
+
_chipInstances.forEach(function(inst, i) {
|
|
727
|
+
inst.trigger.addEventListener('click', function() {
|
|
728
|
+
_chipInstances.forEach(function(other, j) {
|
|
729
|
+
if (j !== i) other.close();
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// ── Intercept clicks on SVG <a href="?..."> links ────────────────────────
|
|
736
|
+
// Blob URL iframes can't navigate to query-string URLs, so we parse the
|
|
737
|
+
// params ourselves and send a single postMessage with the merged variable
|
|
738
|
+
// snapshot instead of navigating.
|
|
739
|
+
document.addEventListener('click', function(event) {
|
|
740
|
+
var link = event.target.closest('a[href]');
|
|
741
|
+
if (!link) return;
|
|
742
|
+
if (!link.ownerSVGElement && link.namespaceURI !== 'http://www.w3.org/2000/svg') return;
|
|
743
|
+
var href = link.getAttribute('href');
|
|
744
|
+
if (!href || href.charAt(0) !== '?') return;
|
|
745
|
+
var inIframe = window.parent !== window;
|
|
746
|
+
var hasHook = typeof window.__dfHandleVariableUpdate === 'function';
|
|
747
|
+
var params = new URLSearchParams(href.slice(1));
|
|
748
|
+
if (!params.toString()) {
|
|
749
|
+
if (inIframe || hasHook) event.preventDefault();
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (!inIframe && !hasHook) {
|
|
754
|
+
// Standalone dft serve: let the browser navigate, but save scroll
|
|
755
|
+
// first so restoreScrollIfSaved() can recover position after reload.
|
|
756
|
+
try {
|
|
757
|
+
sessionStorage.setItem('__dfScrollY_' + window.location.pathname, String(window.scrollY));
|
|
758
|
+
} catch (e) { /* sessionStorage may be unavailable */ }
|
|
759
|
+
return; // browser follows the link naturally
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
event.preventDefault();
|
|
763
|
+
var vars = getAllVariableValues();
|
|
764
|
+
var tabUrl = inIframe ? null : new URL(window.location);
|
|
765
|
+
var chartsToCleanup = [];
|
|
766
|
+
params.forEach(function(value, name) {
|
|
767
|
+
var charts = markDependentChartsLoading(name);
|
|
768
|
+
for (var i = 0; i < charts.length; i++) {
|
|
769
|
+
chartsToCleanup.push(charts[i]);
|
|
770
|
+
}
|
|
771
|
+
vars[name] = value;
|
|
772
|
+
if (tabUrl) { tabUrl.searchParams.set(name, value); }
|
|
773
|
+
});
|
|
774
|
+
scheduleLoadingCleanup(chartsToCleanup);
|
|
775
|
+
if (inIframe) {
|
|
776
|
+
window.parent.postMessage({
|
|
777
|
+
type: 'dft-variable-change',
|
|
778
|
+
variables: vars
|
|
779
|
+
}, '*');
|
|
780
|
+
} else {
|
|
781
|
+
window.__dfHandleVariableUpdate(tabUrl);
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
// Collect all variable values from form controls
|
|
786
|
+
function getAllVariableValues() {
|
|
787
|
+
var allVars = {};
|
|
788
|
+
var controls = document.querySelectorAll('[data-variable]');
|
|
789
|
+
for (var i = 0; i < controls.length; i++) {
|
|
790
|
+
var control = controls[i];
|
|
791
|
+
var name = control.getAttribute('data-variable');
|
|
792
|
+
|
|
793
|
+
// Date range chip — read committed ISO values from data-start/end attrs.
|
|
794
|
+
if (control.classList.contains('dft-calendar-area')) {
|
|
795
|
+
if (!Object.prototype.hasOwnProperty.call(allVars, name)) {
|
|
796
|
+
var s = control.dataset.start || '';
|
|
797
|
+
var en = control.dataset.end || '';
|
|
798
|
+
allVars[name] = [s, en];
|
|
799
|
+
}
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Skip chip trigger button — value is read via dft-calendar-area above.
|
|
804
|
+
if (control.classList.contains('dft-chip')) continue;
|
|
805
|
+
|
|
806
|
+
// Regular controls
|
|
807
|
+
if (control.type === 'checkbox') {
|
|
808
|
+
allVars[name] = control.checked;
|
|
809
|
+
} else if (control.type === 'range') {
|
|
810
|
+
allVars[name] = parseFloat(control.value);
|
|
811
|
+
} else if (control.type === 'number') {
|
|
812
|
+
allVars[name] = control.value ? parseFloat(control.value) : null;
|
|
813
|
+
} else {
|
|
814
|
+
allVars[name] = control.value;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return allVars;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Restore scroll position saved before a standalone page reload (dft serve).
|
|
821
|
+
function restoreScrollIfSaved() {
|
|
822
|
+
try {
|
|
823
|
+
var key = '__dfScrollY_' + window.location.pathname;
|
|
824
|
+
var saved = sessionStorage.getItem(key);
|
|
825
|
+
if (saved !== null) {
|
|
826
|
+
sessionStorage.removeItem(key);
|
|
827
|
+
window.scrollTo(0, parseInt(saved, 10));
|
|
828
|
+
}
|
|
829
|
+
} catch (e) { /* sessionStorage may be unavailable */ }
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Initialize controls from URL params on load
|
|
833
|
+
// Wrapped in DOMContentLoaded check for dynamic SVG insertion scenarios
|
|
834
|
+
function initializeFromURL() {
|
|
835
|
+
var params = new URLSearchParams(window.location.search);
|
|
836
|
+
var entries = params.entries();
|
|
837
|
+
var entry = entries.next();
|
|
838
|
+
while (!entry.done) {
|
|
839
|
+
var name = entry.value[0];
|
|
840
|
+
var value = entry.value[1];
|
|
841
|
+
|
|
842
|
+
// Find chip instance for this variable name (daterange).
|
|
843
|
+
var chipInst = null;
|
|
844
|
+
for (var ci = 0; ci < _chipInstances.length; ci++) {
|
|
845
|
+
if (_chipInstances[ci].varName === name) {
|
|
846
|
+
chipInst = _chipInstances[ci];
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (chipInst) {
|
|
852
|
+
try {
|
|
853
|
+
var rangeValue = JSON.parse(value);
|
|
854
|
+
if (Array.isArray(rangeValue) && rangeValue.length >= 2) {
|
|
855
|
+
chipInst.restoreRange(rangeValue[0], rangeValue[1]);
|
|
856
|
+
}
|
|
857
|
+
} catch (e) {
|
|
858
|
+
// Malformed URL param — ignore; leave chip in placeholder state.
|
|
859
|
+
}
|
|
860
|
+
} else {
|
|
861
|
+
// Regular control
|
|
862
|
+
var control = document.querySelector('[data-variable="' + name + '"]:not(.dft-chip):not(.dft-calendar-area)');
|
|
863
|
+
if (control) {
|
|
864
|
+
if (control.type === 'checkbox') {
|
|
865
|
+
control.checked = value === 'true' || value === '1';
|
|
866
|
+
} else if (control.type === 'range') {
|
|
867
|
+
control.value = value;
|
|
868
|
+
var valueSpan = control.nextElementSibling;
|
|
869
|
+
if (valueSpan && valueSpan.classList.contains('dft-variable-slider-value')) {
|
|
870
|
+
valueSpan.textContent = value;
|
|
871
|
+
}
|
|
872
|
+
} else {
|
|
873
|
+
control.value = value;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
entry = entries.next();
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Run initialization when DOM is ready
|
|
882
|
+
if (document.readyState === 'loading') {
|
|
883
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
884
|
+
_initAllChips();
|
|
885
|
+
_wireExclusiveOpen();
|
|
886
|
+
initializeFromURL();
|
|
887
|
+
restoreScrollIfSaved();
|
|
888
|
+
});
|
|
889
|
+
} else {
|
|
890
|
+
_initAllChips();
|
|
891
|
+
_wireExclusiveOpen();
|
|
892
|
+
initializeFromURL();
|
|
893
|
+
restoreScrollIfSaved();
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Setup variable hover highlighting
|
|
897
|
+
function setupVariableHoverHighlighting() {
|
|
898
|
+
var controls = document.querySelectorAll('.variable-control');
|
|
899
|
+
for (var i = 0; i < controls.length; i++) {
|
|
900
|
+
var control = controls[i];
|
|
901
|
+
var input = control.querySelector('[data-variable]');
|
|
902
|
+
if (!input) continue;
|
|
903
|
+
|
|
904
|
+
var varName = input.getAttribute('data-variable');
|
|
905
|
+
if (!varName) continue;
|
|
906
|
+
|
|
907
|
+
control.addEventListener('mouseenter', function(vName) {
|
|
908
|
+
return function() {
|
|
909
|
+
var charts = document.querySelectorAll('[data-var-' + vName + ']');
|
|
910
|
+
for (var j = 0; j < charts.length; j++) {
|
|
911
|
+
var chart = charts[j];
|
|
912
|
+
|
|
913
|
+
// Use transparent overlay for highlighting (cleaner than offset rects)
|
|
914
|
+
var existingOverlay = chart.querySelector('.dft-chart-highlight-overlay');
|
|
915
|
+
if (!existingOverlay) {
|
|
916
|
+
try {
|
|
917
|
+
// Use allocated dimensions from data attributes (set by renderer)
|
|
918
|
+
var chartWidth = parseFloat(chart.getAttribute('data-chart-width')) || 0;
|
|
919
|
+
var chartHeight = parseFloat(chart.getAttribute('data-chart-height')) || 0;
|
|
920
|
+
|
|
921
|
+
// Fallback to getBBox if data attributes not available
|
|
922
|
+
if (chartWidth === 0 || chartHeight === 0) {
|
|
923
|
+
var bbox = chart.getBBox ? chart.getBBox() : null;
|
|
924
|
+
if (bbox) {
|
|
925
|
+
chartWidth = bbox.width;
|
|
926
|
+
chartHeight = bbox.height;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (chartWidth > 0 && chartHeight > 0) {
|
|
931
|
+
// Create overlay rect that covers entire chart area
|
|
932
|
+
var overlay = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
933
|
+
overlay.setAttribute('class', 'dft-chart-highlight-overlay');
|
|
934
|
+
overlay.setAttribute('x', '0');
|
|
935
|
+
overlay.setAttribute('y', '0');
|
|
936
|
+
overlay.setAttribute('width', chartWidth);
|
|
937
|
+
overlay.setAttribute('height', chartHeight);
|
|
938
|
+
overlay.setAttribute('rx', '4'); // Slight rounding
|
|
939
|
+
// Insert at end so it's on top (but pointer-events: none allows interaction)
|
|
940
|
+
chart.appendChild(overlay);
|
|
941
|
+
}
|
|
942
|
+
} catch (e) {
|
|
943
|
+
// Dimension calculation may fail, ignore silently
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
}(varName));
|
|
949
|
+
|
|
950
|
+
control.addEventListener('mouseleave', function(vName) {
|
|
951
|
+
return function() {
|
|
952
|
+
var charts = document.querySelectorAll('[data-var-' + vName + ']');
|
|
953
|
+
for (var j = 0; j < charts.length; j++) {
|
|
954
|
+
var chart = charts[j];
|
|
955
|
+
|
|
956
|
+
// Remove highlight overlay
|
|
957
|
+
var highlightOverlay = chart.querySelector('.dft-chart-highlight-overlay');
|
|
958
|
+
if (highlightOverlay) {
|
|
959
|
+
highlightOverlay.remove();
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
}(varName));
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Initialize interactions when DOM is ready
|
|
968
|
+
// Note: Chart menus are handled by Suite's JavaScript (init.js)
|
|
969
|
+
if (document.readyState === 'loading') {
|
|
970
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
971
|
+
setupVariableHoverHighlighting();
|
|
972
|
+
});
|
|
973
|
+
} else {
|
|
974
|
+
setupVariableHoverHighlighting();
|
|
975
|
+
}
|
|
976
|
+
})();
|