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,3399 @@
|
|
|
1
|
+
"""Style model skeleton — bare nouns per ADR-001.
|
|
2
|
+
|
|
3
|
+
Stage: COMPILE
|
|
4
|
+
Purpose: Define the target Pydantic model architecture for style.
|
|
5
|
+
|
|
6
|
+
Pattern (ADR-006):
|
|
7
|
+
XX — validated from theme YAML. Required fields; no fallbacks.
|
|
8
|
+
XXPatch — generated mechanically via build_patch_model(). Never hand-duplicated.
|
|
9
|
+
ResolvedXX — final merged result after cascade. Sole type render/sizing accepts.
|
|
10
|
+
|
|
11
|
+
Leaf types:
|
|
12
|
+
FontStyle{family, color, size, weight, style, decoration} — text appearance, cascades (ADR-005)
|
|
13
|
+
BorderStylePatch{radius, color, width} — hand-written all-Optional patch; carries from_css()
|
|
14
|
+
BorderStyle{width, color, radius} — resolved border, all required (ADR-003)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
21
|
+
|
|
22
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
23
|
+
|
|
24
|
+
from dataface.core.compile.models.factories import (
|
|
25
|
+
build_patch_model,
|
|
26
|
+
build_patch_model_ext,
|
|
27
|
+
register_as_own_patch,
|
|
28
|
+
)
|
|
29
|
+
from dataface.core.compile.models.primitives import (
|
|
30
|
+
BorderStyle,
|
|
31
|
+
FontStyle,
|
|
32
|
+
FormatConfig,
|
|
33
|
+
RuleStyle,
|
|
34
|
+
SpacingValues,
|
|
35
|
+
StrokeStyle,
|
|
36
|
+
StyleColorConfig,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# =============================================================================
|
|
40
|
+
# OVERFLOW NORMALIZATION
|
|
41
|
+
# =============================================================================
|
|
42
|
+
|
|
43
|
+
_OVERFLOW_VALUES: frozenset[str] = frozenset({"clip", "truncate", "wrap-two", "wrap"})
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _normalize_overflow_value(value: Any) -> Any:
|
|
47
|
+
"""Normalize overflow mode: snake_case → kebab-case, lower, validate."""
|
|
48
|
+
if value is None or not isinstance(value, str):
|
|
49
|
+
return value
|
|
50
|
+
normalized = value.strip().lower().replace("_", "-")
|
|
51
|
+
if normalized in _OVERFLOW_VALUES:
|
|
52
|
+
return normalized
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"Invalid overflow mode {value!r}. Valid values: clip, truncate, wrap-two, wrap."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
VALID_FONT_WEIGHTS: frozenset[str] = frozenset(
|
|
59
|
+
{"normal", "bold", "100", "200", "300", "400", "500", "600", "700", "800", "900"}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def font_weight_as_css(w: str | float) -> str:
|
|
64
|
+
"""Normalize a FontStyle.weight value to a CSS font-weight string."""
|
|
65
|
+
if isinstance(w, float) and w == int(w):
|
|
66
|
+
return str(int(w))
|
|
67
|
+
return str(w)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PaddingStyle(BaseModel):
|
|
71
|
+
"""Per-chart padding inset (px). All 4 sides required; theme YAML supplies defaults.
|
|
72
|
+
|
|
73
|
+
The board layer adds ``card_padding`` to each side additively so per-chart
|
|
74
|
+
padding composes with the global card layout rather than replacing it.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
model_config = ConfigDict(extra="forbid")
|
|
78
|
+
|
|
79
|
+
left: float = Field(description="Left padding in pixels.")
|
|
80
|
+
right: float = Field(description="Right padding in pixels.")
|
|
81
|
+
top: float = Field(description="Top padding in pixels.")
|
|
82
|
+
bottom: float = Field(description="Bottom padding in pixels.")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# =============================================================================
|
|
86
|
+
# BODY TEXT / COLUMN LAYOUT (per-face CSS-chrome, not theme-cascaded)
|
|
87
|
+
# =============================================================================
|
|
88
|
+
|
|
89
|
+
# Supported SVG stroke styles for column rules. "none"/"hidden" and decorative styles
|
|
90
|
+
# (double, groove, ridge, inset, outset) are deliberately omitted — they have no SVG
|
|
91
|
+
# equivalent and would silently render as solid lines.
|
|
92
|
+
_COLUMN_RULE_SVG_STYLES = frozenset({"solid", "dashed", "dotted"})
|
|
93
|
+
|
|
94
|
+
# Valid hex color lengths: #RGB, #RGBA, #RRGGBB, #RRGGBBAA
|
|
95
|
+
_COLUMN_RULE_HEX_RE = re.compile(
|
|
96
|
+
r"^#[0-9a-fA-F]{3,4}$|^#[0-9a-fA-F]{6}$|^#[0-9a-fA-F]{8}$"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TextColumnStyle(BaseModel):
|
|
101
|
+
"""Multi-column layout for face body text (CSS column-count/gap/rule/width).
|
|
102
|
+
|
|
103
|
+
Two orthogonal controls:
|
|
104
|
+
- ``number``: fixed column count. None = not set.
|
|
105
|
+
- ``width``: minimum column width (px). None = not set. When set,
|
|
106
|
+
column count auto-derives as ``floor((available - gap) / (width + gap))``.
|
|
107
|
+
|
|
108
|
+
When both are set, ``number`` acts as a ceiling on the auto-derived count.
|
|
109
|
+
When neither is set, text renders as a single column.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
model_config = ConfigDict(extra="forbid")
|
|
113
|
+
|
|
114
|
+
number: int | None = Field(
|
|
115
|
+
default=None, ge=1, description="Fixed column count. None = single column."
|
|
116
|
+
)
|
|
117
|
+
gap: float | None = Field(
|
|
118
|
+
default=None,
|
|
119
|
+
description="Gap between columns in pixels. None = use layout gap token.",
|
|
120
|
+
)
|
|
121
|
+
rule: str | None = Field(
|
|
122
|
+
default=None,
|
|
123
|
+
description="CSS column-rule shorthand, e.g. '1px solid #e5e7eb'. None = no rule.",
|
|
124
|
+
)
|
|
125
|
+
width: float | None = Field(
|
|
126
|
+
default=None,
|
|
127
|
+
gt=0,
|
|
128
|
+
description="Minimum column width in pixels for auto-column-count derivation.",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
@field_validator("rule")
|
|
132
|
+
@classmethod
|
|
133
|
+
def _validate_rule_parseable(cls, v: str | None) -> str | None:
|
|
134
|
+
if v is None:
|
|
135
|
+
return v
|
|
136
|
+
tokens = v.strip().split()
|
|
137
|
+
widths = [t for t in tokens if re.match(r"^\d+(?:\.\d+)?px$", t)]
|
|
138
|
+
colors = [t for t in tokens if _COLUMN_RULE_HEX_RE.match(t)]
|
|
139
|
+
styles = [t for t in tokens if t in _COLUMN_RULE_SVG_STYLES]
|
|
140
|
+
remainder = [
|
|
141
|
+
t for t in tokens if t not in widths and t not in colors and t not in styles
|
|
142
|
+
]
|
|
143
|
+
if remainder:
|
|
144
|
+
raise ValueError(
|
|
145
|
+
f"column.rule contains unrecognized tokens {remainder!r} in {v!r}. "
|
|
146
|
+
f"Expected format: '<N>px [solid|dashed|dotted] #<hexcolor>'."
|
|
147
|
+
)
|
|
148
|
+
if len(colors) != 1:
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"column.rule must have exactly one hex color (got {colors!r}): {v!r}"
|
|
151
|
+
)
|
|
152
|
+
if len(widths) != 1:
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"column.rule must have exactly one px width (got {widths!r}): {v!r}"
|
|
155
|
+
)
|
|
156
|
+
if len(styles) > 1:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"column.rule must have at most one style keyword (got {styles!r}): {v!r}"
|
|
159
|
+
)
|
|
160
|
+
return v
|
|
161
|
+
|
|
162
|
+
@model_validator(mode="after")
|
|
163
|
+
def _validate_column_settings(self) -> TextColumnStyle:
|
|
164
|
+
multi = (self.number is not None and self.number > 1) or self.width is not None
|
|
165
|
+
if self.gap is not None and not multi:
|
|
166
|
+
raise ValueError("column.gap requires column.number > 1 or column.width")
|
|
167
|
+
if self.rule is not None and not multi:
|
|
168
|
+
raise ValueError("column.rule requires column.number > 1 or column.width")
|
|
169
|
+
return self
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def has_overrides(self) -> bool:
|
|
173
|
+
"""True when multi-column layout is configured."""
|
|
174
|
+
return (self.number is not None and self.number > 1) or self.width is not None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def compute_text_column_count(
|
|
178
|
+
col: TextColumnStyle, available_width: float, gap: float
|
|
179
|
+
) -> int:
|
|
180
|
+
"""Auto-derive column count from width, with number as ceiling.
|
|
181
|
+
|
|
182
|
+
When only width is set: count = max(1, floor((available - gap) / (width + gap))).
|
|
183
|
+
When only number is set: count = number.
|
|
184
|
+
When both are set: count = min(number, auto-derived).
|
|
185
|
+
When neither is set: single column.
|
|
186
|
+
|
|
187
|
+
``gap`` must be resolved by the caller (fallback to layout gap token if None).
|
|
188
|
+
"""
|
|
189
|
+
if col.width is not None:
|
|
190
|
+
auto = max(1, int((available_width - gap) / (col.width + gap)))
|
|
191
|
+
return min(col.number, auto) if col.number is not None else auto
|
|
192
|
+
if col.number is not None:
|
|
193
|
+
return col.number
|
|
194
|
+
return 1
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _coerce_gap(v: Any) -> float | None:
|
|
198
|
+
"""Coerce gap value from int, float, or CSS px string."""
|
|
199
|
+
if v is None:
|
|
200
|
+
return None
|
|
201
|
+
if isinstance(v, (int, float)):
|
|
202
|
+
return float(v)
|
|
203
|
+
if isinstance(v, str):
|
|
204
|
+
m = re.search(r"(\d+(?:\.\d+)?)", v)
|
|
205
|
+
if m:
|
|
206
|
+
return float(m.group(1))
|
|
207
|
+
raise ValueError(f"Cannot parse gap from {v!r}")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# =============================================================================
|
|
211
|
+
# COMPILEDSTYLE TREE
|
|
212
|
+
# =============================================================================
|
|
213
|
+
|
|
214
|
+
# Board / face dimensions
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class RootFontStyle(FontStyle):
|
|
218
|
+
"""Root-level font configuration: FontStyle fields plus a required emoji mode.
|
|
219
|
+
|
|
220
|
+
The ``emoji`` field is only meaningful at the root — nested FontStyle instances in the
|
|
221
|
+
cascade (axis label, legend, title, etc.) do not carry it. ``extra='forbid'``
|
|
222
|
+
is inherited from FontStyle so nested fonts reject ``emoji:`` at construction.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
emoji: Literal["monochrome", "color", "system-default", "disabled"] = Field(
|
|
226
|
+
description="Emoji rendering mode for the dashboard font stack."
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class BoardStyle(BaseModel):
|
|
231
|
+
"""Face-level structural dimensions. Do NOT cascade to child faces."""
|
|
232
|
+
|
|
233
|
+
model_config = ConfigDict(extra="forbid")
|
|
234
|
+
|
|
235
|
+
width: float = Field(description="Board width in pixels.")
|
|
236
|
+
default_height: float = Field(description="Default row height in pixels.")
|
|
237
|
+
min_height: float = Field(description="Minimum row height in pixels.")
|
|
238
|
+
margin: float = Field(description="Board outer margin in pixels.")
|
|
239
|
+
card_padding: float = Field(
|
|
240
|
+
description="Padding added to each card side in pixels."
|
|
241
|
+
)
|
|
242
|
+
card_gap: float = Field(description="Gap between cards in pixels.")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# Title / text
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class TitleWidthOffsetsStyle(BaseModel):
|
|
249
|
+
"""Additive level offsets applied to a face/chart title's heading level, by card pixel width.
|
|
250
|
+
|
|
251
|
+
Added to the base heading level before indexing into ``style.title.sizes``.
|
|
252
|
+
Tier width breakpoints are constants in ``typography.py``; only the per-tier
|
|
253
|
+
offset lives here so themes own the full type stack without duplicating the
|
|
254
|
+
``sizes`` ramp.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
model_config = ConfigDict(extra="forbid")
|
|
258
|
+
|
|
259
|
+
tiny: int = Field(
|
|
260
|
+
description="Level offset for the tiny tier (cards narrower than ~360px)."
|
|
261
|
+
)
|
|
262
|
+
narrow: int = Field(
|
|
263
|
+
description="Level offset for the narrow tier (cards ~360–559px)."
|
|
264
|
+
)
|
|
265
|
+
medium: int = Field(
|
|
266
|
+
description="Level offset for the medium tier (cards ~560–1099px)."
|
|
267
|
+
)
|
|
268
|
+
wide: int = Field(
|
|
269
|
+
description="Level offset for the wide tier (cards ~1100px and up)."
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class TitlePositionStyle(BaseModel):
|
|
274
|
+
"""Vega-Lite title positioning pass-throughs grouped as a sub-object."""
|
|
275
|
+
|
|
276
|
+
model_config = ConfigDict(extra="forbid")
|
|
277
|
+
|
|
278
|
+
anchor: str = Field(description="Title anchor position; theme always sets this.")
|
|
279
|
+
# Only set in themes that override angle; None lets Vega-Lite choose.
|
|
280
|
+
angle: float | None = Field(
|
|
281
|
+
default=None,
|
|
282
|
+
description="Title rotation angle in degrees; None lets Vega-Lite choose.",
|
|
283
|
+
)
|
|
284
|
+
# Default theme supplies 10; None means inherit Vega-Lite's built-in default.
|
|
285
|
+
offset: float | None = Field(
|
|
286
|
+
default=None,
|
|
287
|
+
description="Title offset from its anchor in pixels; None uses Vega-Lite's default.",
|
|
288
|
+
)
|
|
289
|
+
# Only set in themes that override baseline; None lets Vega-Lite choose.
|
|
290
|
+
baseline: str | None = Field(
|
|
291
|
+
default=None,
|
|
292
|
+
description="Title text baseline alignment; None uses Vega-Lite's default.",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class TitleSubtitleStyle(BaseModel):
|
|
297
|
+
"""VL subtitle font pass-through, grouped for consistency with TitleStyle.font."""
|
|
298
|
+
|
|
299
|
+
model_config = ConfigDict(extra="forbid")
|
|
300
|
+
|
|
301
|
+
# FontStyle is an all-Optional overlay; default_factory creates an empty
|
|
302
|
+
# instance that the cascade fills from theme YAML (same pattern as TitleStyle.font).
|
|
303
|
+
font: FontStyle = Field(
|
|
304
|
+
default_factory=FontStyle,
|
|
305
|
+
description="Subtitle font style overrides (color, family, size, weight).",
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class TitleStyle(BaseModel):
|
|
310
|
+
"""Board and face titles."""
|
|
311
|
+
|
|
312
|
+
model_config = ConfigDict(extra="forbid")
|
|
313
|
+
|
|
314
|
+
font: FontStyle = Field(
|
|
315
|
+
default_factory=FontStyle, description="Title font style overrides."
|
|
316
|
+
)
|
|
317
|
+
line_height: float = Field(
|
|
318
|
+
description=(
|
|
319
|
+
"Line height multiplier for titles and markdown headings. Headings "
|
|
320
|
+
"typically want a tighter multiplier than body prose (~1.1-1.25 vs "
|
|
321
|
+
"the body 1.5-1.6)."
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
sizes: list[float] = Field(
|
|
325
|
+
description=(
|
|
326
|
+
"Font sizes for the H1–H6 heading ramp, indexed by ``face.level - 1``. "
|
|
327
|
+
"Combined with ``width_offsets`` at render time to size titles "
|
|
328
|
+
"responsively by card width."
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
width_offsets: TitleWidthOffsetsStyle = Field(
|
|
332
|
+
description=(
|
|
333
|
+
"Additive level offsets by card width (tiny/narrow/medium/wide). "
|
|
334
|
+
"Added to the title's base level before indexing ``sizes``. "
|
|
335
|
+
"Consumed by chart_title_spec / face_title_spec."
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
min_height: float = Field(description="Minimum title row height in pixels.")
|
|
339
|
+
overflow: str = Field(
|
|
340
|
+
description="Text overflow mode (clip, truncate, wrap-two, wrap)."
|
|
341
|
+
)
|
|
342
|
+
position: TitlePositionStyle = Field(
|
|
343
|
+
description="Vega-Lite title positioning: anchor, angle, offset, baseline."
|
|
344
|
+
)
|
|
345
|
+
# Author/theme override for heading level. "auto" = compute from titled-ancestor count;
|
|
346
|
+
# an integer locks all titles in this scope to that H-level and propagates to descendants.
|
|
347
|
+
# Theme YAML supplies "auto" as the universal default.
|
|
348
|
+
level: int | Literal["auto"] = Field(
|
|
349
|
+
description=(
|
|
350
|
+
"Heading level override for face titles. ``'auto'`` (default) computes the "
|
|
351
|
+
"level semantically as the count of titled ancestors. An integer value "
|
|
352
|
+
"locks all titles in this face and its descendants to that H-level."
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
# VL subtitle font config (ADR-005: color inside font)
|
|
356
|
+
subtitle: TitleSubtitleStyle = Field(
|
|
357
|
+
default_factory=TitleSubtitleStyle,
|
|
358
|
+
description="Subtitle font styles.",
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
@field_validator("level", mode="before")
|
|
362
|
+
@classmethod
|
|
363
|
+
def _validate_level(cls, value: Any) -> Any:
|
|
364
|
+
if value == "auto":
|
|
365
|
+
return value
|
|
366
|
+
if isinstance(value, int):
|
|
367
|
+
if value < 1:
|
|
368
|
+
raise ValueError(f"style.title.level must be >= 1, got {value!r}")
|
|
369
|
+
return value
|
|
370
|
+
if isinstance(value, str):
|
|
371
|
+
raise ValueError(
|
|
372
|
+
f"style.title.level must be 'auto' or a positive integer, got {value!r}"
|
|
373
|
+
)
|
|
374
|
+
return value
|
|
375
|
+
|
|
376
|
+
@field_validator("overflow", mode="before")
|
|
377
|
+
@classmethod
|
|
378
|
+
def _normalize_overflow(cls, value: Any) -> Any:
|
|
379
|
+
return _normalize_overflow_value(value)
|
|
380
|
+
|
|
381
|
+
@model_validator(mode="after")
|
|
382
|
+
def _validate_level_within_ramp(self) -> TitleStyle:
|
|
383
|
+
if isinstance(self.level, int) and self.level > len(self.sizes):
|
|
384
|
+
raise ValueError(
|
|
385
|
+
f"style.title.level={self.level} exceeds the H-ramp "
|
|
386
|
+
f"(sizes has {len(self.sizes)} entries; valid range is 1..{len(self.sizes)})"
|
|
387
|
+
)
|
|
388
|
+
return self
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class TextStyle(BaseModel):
|
|
392
|
+
"""Markdown / plain text content."""
|
|
393
|
+
|
|
394
|
+
model_config = ConfigDict(extra="forbid")
|
|
395
|
+
|
|
396
|
+
font: FontStyle = Field(
|
|
397
|
+
default_factory=FontStyle, description="Text font style overrides."
|
|
398
|
+
)
|
|
399
|
+
line_height: float = Field(description="Line height multiplier for text content.")
|
|
400
|
+
align: Literal["left", "center", "right"] = Field(
|
|
401
|
+
description="Text alignment for the face body-text block."
|
|
402
|
+
)
|
|
403
|
+
column: TextColumnStyle = Field(
|
|
404
|
+
default_factory=TextColumnStyle,
|
|
405
|
+
description="Multi-column layout for the face body-text block.",
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# Callout chart
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class CalloutElementStyle(BaseModel):
|
|
413
|
+
"""Font + y_offset for a callout title or message."""
|
|
414
|
+
|
|
415
|
+
model_config = ConfigDict(extra="forbid")
|
|
416
|
+
|
|
417
|
+
font: FontStyle = Field(
|
|
418
|
+
default_factory=FontStyle, description="Element font style overrides."
|
|
419
|
+
)
|
|
420
|
+
y_offset: float = Field(
|
|
421
|
+
description="Vertical offset from the element's anchor in pixels."
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class CalloutChartStyle(BaseModel):
|
|
426
|
+
"""Chart-family style for ``type: callout`` charts and runtime chart-error fallback cards.
|
|
427
|
+
|
|
428
|
+
Background/border/text colors are resolved from ``{tone}.*`` palette roles
|
|
429
|
+
(bg, border, solid, text) at render time. This model carries structural
|
|
430
|
+
defaults (padding, section_gap, font metrics) plus the semantic tone selector.
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
model_config = ConfigDict(extra="forbid")
|
|
434
|
+
|
|
435
|
+
# Semantic palette selector. None → "negative" at render time.
|
|
436
|
+
tone: str | None = Field(
|
|
437
|
+
default=None,
|
|
438
|
+
description="Semantic tone for palette-role lookup (positive | negative | warning). Defaults to negative.",
|
|
439
|
+
)
|
|
440
|
+
background: str = Field(
|
|
441
|
+
description="Callout card background color (negative.bg default)."
|
|
442
|
+
)
|
|
443
|
+
border: BorderStyle = Field(description="Callout card border style.")
|
|
444
|
+
padding: float = Field(description="Inner padding of the callout card in pixels.")
|
|
445
|
+
section_gap: float = Field(
|
|
446
|
+
description="Vertical gap between callout title and message in pixels."
|
|
447
|
+
)
|
|
448
|
+
title: CalloutElementStyle = Field(description="Callout title element style.")
|
|
449
|
+
message: CalloutElementStyle = Field(description="Callout message element style.")
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
# Placeholder
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class PlaceholderOverlay(BaseModel):
|
|
456
|
+
model_config = ConfigDict(extra="forbid")
|
|
457
|
+
|
|
458
|
+
text: str = Field(description="Placeholder overlay text shown on empty charts.")
|
|
459
|
+
background: str = Field(description="Overlay background color.")
|
|
460
|
+
font: FontStyle = Field(
|
|
461
|
+
default_factory=FontStyle, description="Overlay font style overrides."
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class PlaceholderStyle(BaseModel):
|
|
466
|
+
model_config = ConfigDict(extra="forbid")
|
|
467
|
+
|
|
468
|
+
opacity: float = Field(description="Opacity of the placeholder overlay (0–1).")
|
|
469
|
+
overlay: PlaceholderOverlay = Field(
|
|
470
|
+
description="Overlay text and background style."
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# Axis
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class AxisGridZeroStyle(BaseModel):
|
|
478
|
+
"""Zero-baseline gridline color and width overrides.
|
|
479
|
+
|
|
480
|
+
Nested sub-object under AxisGridStyle.zero. Quantitative axes only —
|
|
481
|
+
band axes never tick at value=0.
|
|
482
|
+
"""
|
|
483
|
+
|
|
484
|
+
model_config = ConfigDict(extra="forbid")
|
|
485
|
+
|
|
486
|
+
# All fields nullable — cascade fills from parent axis into axis_x/y/quantitative.
|
|
487
|
+
color: str | None = Field( # None inherits from parent axis zero.color
|
|
488
|
+
default=None,
|
|
489
|
+
description="Color of the zero-baseline grid line; None inherits from parent axis.",
|
|
490
|
+
)
|
|
491
|
+
width: float | None = Field( # None inherits from parent axis zero.width
|
|
492
|
+
default=None,
|
|
493
|
+
description="Width of the zero-baseline grid line in pixels; None inherits from parent axis.",
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
class AxisGridStyle(BaseModel):
|
|
498
|
+
model_config = ConfigDict(extra="forbid")
|
|
499
|
+
|
|
500
|
+
# All fields are nullable in AxisGridStyle so the cascade can fill
|
|
501
|
+
# missing values from parent `axis` into `axis_x` / `axis_y` /
|
|
502
|
+
# `axis_quantitative`. The base `axis` slot must have all fields non-None;
|
|
503
|
+
# `_build_resolved_axis` validates this at resolve time.
|
|
504
|
+
visible: bool | None = Field(
|
|
505
|
+
default=None, description="Show grid lines; None inherits from parent axis."
|
|
506
|
+
)
|
|
507
|
+
opacity: float | None = Field(
|
|
508
|
+
default=None, description="Grid line opacity; None uses Vega-Lite's default."
|
|
509
|
+
)
|
|
510
|
+
width: float | None = Field(
|
|
511
|
+
default=None,
|
|
512
|
+
description="Grid line width in pixels; None uses Vega-Lite's default.",
|
|
513
|
+
)
|
|
514
|
+
color: str | None = Field(
|
|
515
|
+
default=None, description="Grid line color; None uses Vega-Lite's default."
|
|
516
|
+
)
|
|
517
|
+
dash: list[float] | None = Field(
|
|
518
|
+
default=None,
|
|
519
|
+
description="Dash pattern for grid lines; None renders a solid line.",
|
|
520
|
+
)
|
|
521
|
+
# Zero-baseline sub-block. None on child axes means "no override at this level";
|
|
522
|
+
# the cascade fills individual zero.* fields from the parent axis.
|
|
523
|
+
zero: AxisGridZeroStyle | None = (
|
|
524
|
+
Field( # None — no zero-baseline authored at this level
|
|
525
|
+
default=None,
|
|
526
|
+
description="Zero-baseline gridline style; None inherits from parent axis.",
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class AxisDomainStyle(BaseModel):
|
|
532
|
+
model_config = ConfigDict(extra="forbid")
|
|
533
|
+
|
|
534
|
+
# Nullable so the cascade can fill missing values from parent `axis`.
|
|
535
|
+
visible: bool | None = Field(
|
|
536
|
+
default=None,
|
|
537
|
+
description="Show the axis domain line; None inherits from parent axis.",
|
|
538
|
+
)
|
|
539
|
+
width: float | None = Field(
|
|
540
|
+
default=None,
|
|
541
|
+
description="Domain line width in pixels; None uses Vega-Lite's default.",
|
|
542
|
+
)
|
|
543
|
+
color: str | None = Field(
|
|
544
|
+
default=None, description="Domain line color; None uses Vega-Lite's default."
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
class AxisTicksStyle(BaseModel):
|
|
549
|
+
model_config = ConfigDict(extra="forbid")
|
|
550
|
+
|
|
551
|
+
# Nullable so the cascade can fill `visible` from parent `axis`.
|
|
552
|
+
visible: bool | None = Field(
|
|
553
|
+
default=None, description="Show axis ticks; None inherits from parent axis."
|
|
554
|
+
)
|
|
555
|
+
color: str | None = Field(
|
|
556
|
+
default=None, description="Tick color; None uses Vega-Lite's default."
|
|
557
|
+
)
|
|
558
|
+
size: float | None = Field(
|
|
559
|
+
default=None,
|
|
560
|
+
description="Tick length in pixels; None uses Vega-Lite's default.",
|
|
561
|
+
)
|
|
562
|
+
width: float | None = Field(
|
|
563
|
+
default=None,
|
|
564
|
+
description="Tick stroke width in pixels; None uses Vega-Lite's default.",
|
|
565
|
+
)
|
|
566
|
+
count: int | None = Field(
|
|
567
|
+
default=None,
|
|
568
|
+
description=(
|
|
569
|
+
"Target number of quantitative y-axis ticks. When set, the renderer "
|
|
570
|
+
"computes explicit tickValues so VL emits exactly this many ticks — "
|
|
571
|
+
"VL's tickCount is advisory and frequently ignored."
|
|
572
|
+
),
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
class AxisElementStyle(BaseModel):
|
|
577
|
+
"""Axis label or title: font + padding + VL-passthrough."""
|
|
578
|
+
|
|
579
|
+
model_config = ConfigDict(extra="forbid")
|
|
580
|
+
|
|
581
|
+
font: FontStyle = Field(
|
|
582
|
+
default_factory=FontStyle, description="Axis element font style overrides."
|
|
583
|
+
)
|
|
584
|
+
padding: float | None = Field(
|
|
585
|
+
default=None,
|
|
586
|
+
description="Padding between axis labels and ticks in pixels; None inherits from parent axis.",
|
|
587
|
+
)
|
|
588
|
+
max_width: float | None = Field(
|
|
589
|
+
default=None,
|
|
590
|
+
description="Maximum label width in pixels; None uses Vega-Lite's default (180px).",
|
|
591
|
+
)
|
|
592
|
+
angle: float | None = Field(
|
|
593
|
+
default=None,
|
|
594
|
+
description="Label rotation angle in degrees; None uses Vega-Lite's default.",
|
|
595
|
+
)
|
|
596
|
+
align: str | None = Field(
|
|
597
|
+
default=None,
|
|
598
|
+
description="Horizontal text alignment of labels; None uses Vega-Lite's default.",
|
|
599
|
+
)
|
|
600
|
+
baseline: str | None = Field(
|
|
601
|
+
default=None,
|
|
602
|
+
description="Vertical text baseline of labels; None uses Vega-Lite's default.",
|
|
603
|
+
)
|
|
604
|
+
# "smart" → omit labelOverlap from VL spec (gets per-scale adaptive default).
|
|
605
|
+
# "allow" → emit labelOverlap: false (no reduction; labels may overlap).
|
|
606
|
+
# "parity" / "greedy" → pass through as-is.
|
|
607
|
+
# Raw true/false rejected — use named strings.
|
|
608
|
+
overlap: Literal["smart", "parity", "greedy", "allow"] | None = Field(
|
|
609
|
+
default=None,
|
|
610
|
+
description="Label overlap reduction strategy; None uses Vega-Lite's per-scale default.",
|
|
611
|
+
)
|
|
612
|
+
separation: float | None = Field(
|
|
613
|
+
default=None,
|
|
614
|
+
description="Minimum pixel separation between labels; None uses Vega-Lite's default (0px).",
|
|
615
|
+
)
|
|
616
|
+
visible: bool | None = Field(
|
|
617
|
+
default=None,
|
|
618
|
+
description="Show axis labels; None uses Vega-Lite's default (labels shown).",
|
|
619
|
+
)
|
|
620
|
+
# Optional label cadence for temporal axes. None inherits from the parent
|
|
621
|
+
# axis time_unit, except yearweek defaults to month labels.
|
|
622
|
+
time_unit: (
|
|
623
|
+
Literal[
|
|
624
|
+
"auto",
|
|
625
|
+
"year",
|
|
626
|
+
"yearquarter",
|
|
627
|
+
"yearmonth",
|
|
628
|
+
"yearweek",
|
|
629
|
+
"yearmonthdate",
|
|
630
|
+
"monthofyear",
|
|
631
|
+
"dayofweek",
|
|
632
|
+
"dayofmonth",
|
|
633
|
+
"dayofyear",
|
|
634
|
+
"hourofday",
|
|
635
|
+
"none",
|
|
636
|
+
]
|
|
637
|
+
| None
|
|
638
|
+
) = Field(
|
|
639
|
+
default=None,
|
|
640
|
+
description="Label cadence for temporal axes; None inherits from the parent axis time_unit.",
|
|
641
|
+
)
|
|
642
|
+
# When None and the parent axis has a temporal time_unit, the render layer
|
|
643
|
+
# fills in a smart conditional labelExpr — see render/chart/time_unit_detect.py.
|
|
644
|
+
expr: str | None = Field(
|
|
645
|
+
default=None,
|
|
646
|
+
description="Custom Vega expression for label text; None uses smart temporal defaults when applicable.",
|
|
647
|
+
)
|
|
648
|
+
# Positioning knobs — all None means VL per-axis defaults apply.
|
|
649
|
+
bound: bool | float | None = Field(
|
|
650
|
+
default=None,
|
|
651
|
+
description="Hide labels that overflow the axis range; None uses Vega-Lite's default.",
|
|
652
|
+
)
|
|
653
|
+
flush: bool | float | None = Field(
|
|
654
|
+
default=None,
|
|
655
|
+
description="Align first/last label flush with the scale range; None uses Vega-Lite's default.",
|
|
656
|
+
)
|
|
657
|
+
offset: float | None = Field(
|
|
658
|
+
default=None,
|
|
659
|
+
description="Pixel offset of the label from its tick anchor; None uses Vega-Lite's default.",
|
|
660
|
+
)
|
|
661
|
+
line_height: float | None = Field(
|
|
662
|
+
default=None,
|
|
663
|
+
description="Line height for multi-line labels in pixels; None uses Vega-Lite's default.",
|
|
664
|
+
)
|
|
665
|
+
anchor: Literal["start", "middle", "end"] | None = Field(default=None, description="Label anchor position; None uses Vega-Lite's default.") # fmt: skip
|
|
666
|
+
# Descending tilt ladder consulted by overlap="smart" on discrete x-axes.
|
|
667
|
+
# The picker walks this list and chooses the first angle whose label widths
|
|
668
|
+
# fit at the chart's drawable width; fall-through uses the last entry and
|
|
669
|
+
# downgrades overlap to "parity". None means picker is disabled (used on
|
|
670
|
+
# y-axis and any axis the resolver does not target).
|
|
671
|
+
tilt_increments: list[float] | None = Field(
|
|
672
|
+
default=None,
|
|
673
|
+
min_length=1,
|
|
674
|
+
description="Descending tilt angles for smart label overlap on discrete x-axes; None disables the picker.",
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
@field_validator("overlap", mode="before")
|
|
678
|
+
@classmethod
|
|
679
|
+
def _reject_bool_overlap(cls, v: object) -> object:
|
|
680
|
+
if v is True:
|
|
681
|
+
raise ValueError(
|
|
682
|
+
'overlap: true is ambiguous — use "smart" for per-scale adaptive '
|
|
683
|
+
'(the most likely intent) or "parity" for the literal VL true '
|
|
684
|
+
"semantic (drop every other label on any scale)."
|
|
685
|
+
)
|
|
686
|
+
if v is False:
|
|
687
|
+
raise ValueError(
|
|
688
|
+
'overlap: false reads backwards — use "allow" to permit labels '
|
|
689
|
+
"to overlap (no reduction)."
|
|
690
|
+
)
|
|
691
|
+
return v
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
class ScaleBandStyle(BaseModel):
|
|
695
|
+
"""Band mark size constraints (min/max pixel width for band-scale marks)."""
|
|
696
|
+
|
|
697
|
+
model_config = ConfigDict(extra="forbid")
|
|
698
|
+
|
|
699
|
+
min_size: float | None = Field( # None = no min constraint; authored overlay
|
|
700
|
+
default=None,
|
|
701
|
+
description="Minimum band mark width in pixels; None uses Vega-Lite's default.",
|
|
702
|
+
)
|
|
703
|
+
max_size: float | None = Field( # None = no max constraint; authored overlay
|
|
704
|
+
default=None,
|
|
705
|
+
description="Maximum band mark width in pixels; None uses Vega-Lite's default.",
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
class ScaleStyle(BaseModel):
|
|
710
|
+
"""Scale configuration primitive.
|
|
711
|
+
|
|
712
|
+
Used both at chart-level (``style.scale`` — applies to both x and y) and
|
|
713
|
+
per-axis (``style.axis_x.scale``, ``style.axis_y.scale`` — overrides on
|
|
714
|
+
that channel only). Per-axis values win on conflict.
|
|
715
|
+
"""
|
|
716
|
+
|
|
717
|
+
model_config = ConfigDict(extra="forbid")
|
|
718
|
+
|
|
719
|
+
band_padding_inner: float | None = Field(
|
|
720
|
+
default=None,
|
|
721
|
+
description="Inner padding fraction for band scales; None uses Vega-Lite's default.",
|
|
722
|
+
)
|
|
723
|
+
band_padding_outer: float | None = Field(
|
|
724
|
+
default=None,
|
|
725
|
+
description="Outer padding fraction for band scales; None uses Vega-Lite's default.",
|
|
726
|
+
)
|
|
727
|
+
point_padding: float | None = Field(
|
|
728
|
+
default=None,
|
|
729
|
+
description="Padding fraction for point scales; None uses Vega-Lite's default.",
|
|
730
|
+
)
|
|
731
|
+
rect_band_padding_inner: float | None = Field(
|
|
732
|
+
default=None,
|
|
733
|
+
description="Inner padding fraction for rect band scales; None uses Vega-Lite's default.",
|
|
734
|
+
)
|
|
735
|
+
round: bool | None = Field(
|
|
736
|
+
default=None,
|
|
737
|
+
description="Round scale outputs to nearest integer; None uses Vega-Lite's default (no rounding).",
|
|
738
|
+
)
|
|
739
|
+
zero: bool | None = Field(
|
|
740
|
+
default=None,
|
|
741
|
+
description="Force scale to include zero; None uses Vega-Lite's default.",
|
|
742
|
+
)
|
|
743
|
+
clamp: bool | None = Field(
|
|
744
|
+
default=None,
|
|
745
|
+
description="Clamp values to scale domain; None uses Vega-Lite's default (no clamping).",
|
|
746
|
+
)
|
|
747
|
+
continuous_padding: float | None = Field(
|
|
748
|
+
default=None,
|
|
749
|
+
description="Padding in pixels for continuous scales; None uses Vega-Lite's default.",
|
|
750
|
+
)
|
|
751
|
+
band: ScaleBandStyle | None = Field( # None = no band size constraint authored
|
|
752
|
+
default=None,
|
|
753
|
+
description="Band mark size constraints; None uses Vega-Lite's defaults.",
|
|
754
|
+
)
|
|
755
|
+
quantile_count: int | None = Field(
|
|
756
|
+
default=None,
|
|
757
|
+
description="Number of quantile buckets; None uses Vega-Lite's default.",
|
|
758
|
+
)
|
|
759
|
+
quantize_count: int | None = Field(
|
|
760
|
+
default=None,
|
|
761
|
+
description="Number of quantize buckets; None uses Vega-Lite's default.",
|
|
762
|
+
)
|
|
763
|
+
use_unaggregated_domain: bool | None = Field(
|
|
764
|
+
default=None,
|
|
765
|
+
description="Use unaggregated domain for scale extent; None uses Vega-Lite's default.",
|
|
766
|
+
)
|
|
767
|
+
x_reverse: bool | None = Field(
|
|
768
|
+
default=None,
|
|
769
|
+
description="Reverse the x-axis scale direction; None means no reversal.",
|
|
770
|
+
)
|
|
771
|
+
type: str | None = Field(
|
|
772
|
+
default=None,
|
|
773
|
+
description="Scale type override; None lets Vega-Lite infer from field type.",
|
|
774
|
+
)
|
|
775
|
+
domain: list | None = Field(
|
|
776
|
+
default=None,
|
|
777
|
+
description="Explicit scale domain values; None lets Vega-Lite auto-determine from data.",
|
|
778
|
+
)
|
|
779
|
+
nice: bool | None = Field(
|
|
780
|
+
default=None,
|
|
781
|
+
description="Round scale domain to nice values; None uses Vega-Lite's default.",
|
|
782
|
+
)
|
|
783
|
+
# Unified shortcut. Maps to VL ``scale.padding``, which Vega-Lite itself
|
|
784
|
+
# dispatches per scale type: bandPaddingOuter for band, pointPadding for
|
|
785
|
+
# point, continuousPadding for continuous. Note that VL interprets this
|
|
786
|
+
# in pixels for continuous scales but as a fraction (0..1) for band/point.
|
|
787
|
+
padding: float | None = Field(
|
|
788
|
+
default=None,
|
|
789
|
+
description="Unified scale padding shortcut; dispatches to band, point, or continuous padding per scale type.",
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
class RangeStylePatch(BaseModel):
|
|
794
|
+
"""Chart-local palette/range overlay — no theme cascade.
|
|
795
|
+
|
|
796
|
+
Sets VL range.category, diverging, heatmap, ramp per chart.
|
|
797
|
+
Theme-level categorical palette is set via ChartsStyle.palette, not this type.
|
|
798
|
+
All fields optional; None means "not overridden at this level".
|
|
799
|
+
"""
|
|
800
|
+
|
|
801
|
+
model_config = ConfigDict(extra="forbid")
|
|
802
|
+
|
|
803
|
+
category: list[str] | None = Field(
|
|
804
|
+
default=None,
|
|
805
|
+
description="Per-chart categorical color palette (list of CSS color strings).",
|
|
806
|
+
)
|
|
807
|
+
diverging: list[str] | None = Field(
|
|
808
|
+
default=None, description="Per-chart diverging color scale colors."
|
|
809
|
+
)
|
|
810
|
+
heatmap: list[str] | None = Field(
|
|
811
|
+
default=None, description="Per-chart heatmap color scale colors."
|
|
812
|
+
)
|
|
813
|
+
ramp: list[str] | None = Field(
|
|
814
|
+
default=None, description="Per-chart sequential color ramp colors."
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
# RangeStylePatch has no compiled counterpart — it IS the authored-overlay shape.
|
|
819
|
+
# Register it as its own patch so build_patch_model doesn't synthesise a
|
|
820
|
+
# double-suffixed RangeStylePatchPatch when processing CartesianChartStyle fields.
|
|
821
|
+
register_as_own_patch(RangeStylePatch)
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
class TooltipStyle(BaseModel):
|
|
825
|
+
"""Tooltip rendering defaults."""
|
|
826
|
+
|
|
827
|
+
model_config = ConfigDict(extra="forbid")
|
|
828
|
+
|
|
829
|
+
format: str = Field(
|
|
830
|
+
description="Default tooltip value format string; theme always provides this."
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
class AxisStyle(BaseModel):
|
|
835
|
+
model_config = ConfigDict(extra="forbid")
|
|
836
|
+
|
|
837
|
+
grid: AxisGridStyle = Field(
|
|
838
|
+
default_factory=AxisGridStyle, description="Grid line style for this axis."
|
|
839
|
+
)
|
|
840
|
+
domain: AxisDomainStyle = Field(
|
|
841
|
+
default_factory=AxisDomainStyle, description="Domain line style for this axis."
|
|
842
|
+
)
|
|
843
|
+
ticks: AxisTicksStyle = Field(description="Tick mark style for this axis.")
|
|
844
|
+
label: AxisElementStyle = Field(
|
|
845
|
+
default_factory=AxisElementStyle, description="Axis label style."
|
|
846
|
+
)
|
|
847
|
+
title: AxisElementStyle = Field(
|
|
848
|
+
default_factory=AxisElementStyle, description="Axis title style."
|
|
849
|
+
)
|
|
850
|
+
# "auto" → "right" by default; flips to "left" when chart has right-edge
|
|
851
|
+
# series labels (e.g. line endpoint labels). VL passthrough for explicit values.
|
|
852
|
+
orient: Literal["left", "right", "top", "bottom", "auto"] | None = Field(
|
|
853
|
+
default=None,
|
|
854
|
+
description="Axis orientation; auto flips y-axis when endpoint labels are present.",
|
|
855
|
+
)
|
|
856
|
+
# Only populated on axis_y in theme YAML; None on shared/x/quantitative/band
|
|
857
|
+
# axes means "skip this VL property" matching the VL config emit layer.
|
|
858
|
+
categorical_orient: str | None = Field(
|
|
859
|
+
default=None,
|
|
860
|
+
description="Orientation for categorical y-axis (e.g. 'left'); None skips this VL property.",
|
|
861
|
+
)
|
|
862
|
+
band_position: float | None = Field(
|
|
863
|
+
default=None,
|
|
864
|
+
description="Band position within the step (0–1); None uses Vega-Lite's default.",
|
|
865
|
+
)
|
|
866
|
+
offset: float | None = Field(
|
|
867
|
+
default=None,
|
|
868
|
+
description="Pixel offset of the axis from its default position; None means no offset.",
|
|
869
|
+
)
|
|
870
|
+
# Encoding-level passthrough: applied to encoding.y.axis, not config.axis
|
|
871
|
+
format: str | None = Field(
|
|
872
|
+
default=None, description="Tick value format string; None uses auto-format."
|
|
873
|
+
)
|
|
874
|
+
values: list | None = Field(
|
|
875
|
+
default=None,
|
|
876
|
+
description="Explicit tick values; None uses Vega-Lite's auto tick values.",
|
|
877
|
+
)
|
|
878
|
+
# Time-unit bucketing for temporal x axes (auto-detected when None or "auto").
|
|
879
|
+
# "none" disables bucketing (continuous temporal). VL timeUnit values otherwise.
|
|
880
|
+
time_unit: (
|
|
881
|
+
Literal[
|
|
882
|
+
"auto",
|
|
883
|
+
"year",
|
|
884
|
+
"yearquarter",
|
|
885
|
+
"yearmonth",
|
|
886
|
+
"yearweek",
|
|
887
|
+
"yearmonthdate",
|
|
888
|
+
"monthofyear",
|
|
889
|
+
"dayofweek",
|
|
890
|
+
"dayofmonth",
|
|
891
|
+
"dayofyear",
|
|
892
|
+
"hourofday",
|
|
893
|
+
"none",
|
|
894
|
+
]
|
|
895
|
+
| None
|
|
896
|
+
) = Field(
|
|
897
|
+
default=None,
|
|
898
|
+
description="Time-unit bucketing for temporal x-axes; None or 'auto' auto-detects from data.",
|
|
899
|
+
)
|
|
900
|
+
# Per-axis scale overrides — applied to encoding.{x,y}.scale, win over
|
|
901
|
+
# the chart-level ``style.scale`` when both are set.
|
|
902
|
+
scale: ScaleStyle | None = Field(
|
|
903
|
+
default=None, description="Per-axis scale overrides; None means no override."
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
# Authored opt-in scale type for bucketed-time x-axes (axis_x only).
|
|
907
|
+
# None/"auto" = ordinal when time_unit resolves to a bucketed-calendar
|
|
908
|
+
# grain, temporal for continuous date. "temporal" forces the temporal
|
|
909
|
+
# escape-hatch path. "ordinal" forces ordinal. Author always wins.
|
|
910
|
+
type: Literal["auto", "ordinal", "temporal"] | None = Field(
|
|
911
|
+
default=None,
|
|
912
|
+
description="Scale type for bucketed-time x-axes; None/'auto' infers from time_unit grain.",
|
|
913
|
+
)
|
|
914
|
+
# Fill mode for missing time buckets on ordinal bucketed-time x-axes.
|
|
915
|
+
# "null" → synthesized rows carry null measures; line/area breaks at gap.
|
|
916
|
+
# "zero" → synthesized rows carry 0 measures; event-count charts opt in.
|
|
917
|
+
# linear / step-* / curve → fill interior synthetic buckets (Looker-aligned names).
|
|
918
|
+
# Required; theme YAML supplies the default ("null") via axis_x.fill.
|
|
919
|
+
fill: Literal[
|
|
920
|
+
"null",
|
|
921
|
+
"zero",
|
|
922
|
+
"linear",
|
|
923
|
+
"step-after",
|
|
924
|
+
"step-before",
|
|
925
|
+
"step-center",
|
|
926
|
+
"curve",
|
|
927
|
+
] = Field(
|
|
928
|
+
description=(
|
|
929
|
+
"Fill for synthesized missing-bucket rows: null, zero, linear, "
|
|
930
|
+
"step-after / step-before / step-center (Looker step), or curve (smoothstep)."
|
|
931
|
+
),
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
@field_validator("fill", mode="before")
|
|
935
|
+
@classmethod
|
|
936
|
+
def _yaml_null_is_null_fill_mode(cls, value: object) -> object:
|
|
937
|
+
# Theme YAML uses `fill: null` for the null fill mode; YAML parses that as None.
|
|
938
|
+
if value is None:
|
|
939
|
+
return "null"
|
|
940
|
+
return value
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
# Patch type generated here so chart-type styles (Bar, Line, Area, Scatter, Arc)
|
|
944
|
+
# can reference it as a per-chart-type axis sentinel field.
|
|
945
|
+
# TYPE_CHECKING stub mirrors the DataTable/Table pattern — gives mypy a proper
|
|
946
|
+
# class (with accurate field names from AxisStyle) while the runtime uses
|
|
947
|
+
# the dynamically created patch model from build_patch_model().
|
|
948
|
+
if TYPE_CHECKING:
|
|
949
|
+
|
|
950
|
+
class AxisStylePatch(AxisStyle):
|
|
951
|
+
pass
|
|
952
|
+
|
|
953
|
+
else:
|
|
954
|
+
AxisStylePatch = build_patch_model(AxisStyle)
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
# Legend
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
class LegendElementStyle(BaseModel):
|
|
961
|
+
model_config = ConfigDict(extra="forbid")
|
|
962
|
+
|
|
963
|
+
font: FontStyle = Field(
|
|
964
|
+
default_factory=FontStyle,
|
|
965
|
+
description="Legend element font style overrides.",
|
|
966
|
+
)
|
|
967
|
+
padding: float = Field(
|
|
968
|
+
description="Padding between legend symbol and element text in pixels."
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
class LegendStyle(BaseModel):
|
|
973
|
+
model_config = ConfigDict(extra="forbid")
|
|
974
|
+
|
|
975
|
+
orient: str = Field(description="Legend orientation (e.g. 'right', 'bottom').")
|
|
976
|
+
direction: str = Field(
|
|
977
|
+
description="Legend layout direction ('vertical' or 'horizontal')."
|
|
978
|
+
)
|
|
979
|
+
label: LegendElementStyle = Field(description="Legend label style.")
|
|
980
|
+
title: LegendElementStyle = Field(description="Legend title style.")
|
|
981
|
+
disable: bool | None = Field(
|
|
982
|
+
default=None,
|
|
983
|
+
description="Suppress the legend entirely; None means legend is visible.",
|
|
984
|
+
)
|
|
985
|
+
interactive_legend: bool = Field(
|
|
986
|
+
description="Emit a dft_legend point-selection param for single-click series toggle.",
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
# Inference
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
class InferenceStyle(BaseModel):
|
|
994
|
+
model_config = ConfigDict(extra="forbid")
|
|
995
|
+
|
|
996
|
+
# Engine behavior flags — not visual style. Code defaults are canonical;
|
|
997
|
+
# themes may override but most don't need to.
|
|
998
|
+
infer_zero_when_missing: bool = Field(
|
|
999
|
+
default=True, description="Auto-infer zero-baseline when not authored."
|
|
1000
|
+
)
|
|
1001
|
+
infer_fields_when_missing: bool = Field(
|
|
1002
|
+
default=True,
|
|
1003
|
+
description="Auto-infer chart fields (x, y, etc.) when not authored.",
|
|
1004
|
+
)
|
|
1005
|
+
infer_type_when_auto: bool = Field(
|
|
1006
|
+
default=True, description="Auto-infer chart type when type is 'auto'."
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def _coerce_pagination_value(v: Any) -> Any:
|
|
1011
|
+
"""Expand pagination shorthand: int → page_size, bool → enabled."""
|
|
1012
|
+
if isinstance(v, bool):
|
|
1013
|
+
return {"enabled": v}
|
|
1014
|
+
if isinstance(v, int):
|
|
1015
|
+
return {"enabled": True, "page_size": v}
|
|
1016
|
+
return v
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
class PaginationConfig(BaseModel):
|
|
1020
|
+
"""Table pagination configuration.
|
|
1021
|
+
|
|
1022
|
+
Controls client-side row paging for table charts. This is a presentation
|
|
1023
|
+
concern (how many rows to show per page), distinct from query-level ``limit``
|
|
1024
|
+
which controls how many rows are *fetched*.
|
|
1025
|
+
|
|
1026
|
+
Shorthand forms are normalised by a ``field_validator`` on the containing
|
|
1027
|
+
style model:
|
|
1028
|
+
- ``pagination: 25`` → PaginationConfig(enabled=True, page_size=25)
|
|
1029
|
+
- ``pagination: true`` → PaginationConfig(enabled=True)
|
|
1030
|
+
- ``pagination: false`` → PaginationConfig(enabled=False)
|
|
1031
|
+
"""
|
|
1032
|
+
|
|
1033
|
+
model_config = ConfigDict(extra="forbid")
|
|
1034
|
+
|
|
1035
|
+
enabled: bool = Field(
|
|
1036
|
+
default=True, description="Enable client-side pagination for table charts."
|
|
1037
|
+
)
|
|
1038
|
+
page_size: int | None = Field(
|
|
1039
|
+
default=None,
|
|
1040
|
+
description=(
|
|
1041
|
+
"Rows per page. When enabled and None, the renderer auto-fits page size "
|
|
1042
|
+
"to the cell — set explicitly to pin the page size."
|
|
1043
|
+
),
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
@field_validator("page_size")
|
|
1047
|
+
@classmethod
|
|
1048
|
+
def _page_size_positive(cls, v: int | None) -> int | None:
|
|
1049
|
+
if v is not None and v < 1:
|
|
1050
|
+
raise ValueError("page_size must be a positive integer")
|
|
1051
|
+
return v
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
# Per-chart-type styles
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
class EndpointLabelsConfig(BaseModel):
|
|
1058
|
+
"""Endpoint label pane config — shared across line, area, and bar.
|
|
1059
|
+
|
|
1060
|
+
When visible, a separate pane is emitted alongside the main chart with one
|
|
1061
|
+
text mark per series. For line/area and vertical stacked/grouped bar, the
|
|
1062
|
+
pane is hconcat to the right, anchored at the family's "trailing-x" slice
|
|
1063
|
+
(endpoint, segment midpoint, or bar top). For horizontal stacked bar, the
|
|
1064
|
+
pane is vconcat above the chart, with each label centered on its segment
|
|
1065
|
+
midpoint in the top categorical row. Typography is sourced from
|
|
1066
|
+
``style.charts.series_label.font.*``.
|
|
1067
|
+
|
|
1068
|
+
Theme-tunable: all fields live in theme YAML.
|
|
1069
|
+
"""
|
|
1070
|
+
|
|
1071
|
+
model_config = ConfigDict(extra="forbid")
|
|
1072
|
+
|
|
1073
|
+
# Render-visibility toggle. Theme writes false; authors flip to true per chart.
|
|
1074
|
+
visible: bool = Field(
|
|
1075
|
+
description="Show endpoint labels; theme sets false, authors opt in per chart."
|
|
1076
|
+
)
|
|
1077
|
+
# Spacing (px) between the chart pane and the label pane —
|
|
1078
|
+
# passed through as the hconcat or vconcat spacing depending on orientation.
|
|
1079
|
+
label_offset: float = Field(
|
|
1080
|
+
description="Spacing in pixels between the chart pane and the label pane."
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
class TextMarkStyle(BaseModel):
|
|
1085
|
+
model_config = ConfigDict(extra="forbid")
|
|
1086
|
+
|
|
1087
|
+
font: FontStyle = Field(
|
|
1088
|
+
default_factory=FontStyle, description="Text mark font style overrides."
|
|
1089
|
+
)
|
|
1090
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1091
|
+
align: str | None = Field(
|
|
1092
|
+
default=None, description="Horizontal text alignment for text marks."
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
class SeriesLabelStyle(BaseModel):
|
|
1097
|
+
"""Series-label primitive — typography for any text mark that names a
|
|
1098
|
+
data series, regardless of placement.
|
|
1099
|
+
|
|
1100
|
+
Endpoint labels (line/area), direct stack labels (bar-stacked), and slice
|
|
1101
|
+
callouts (pie) all read their typography from this single primitive.
|
|
1102
|
+
Placement is family-specific; typography is shared.
|
|
1103
|
+
|
|
1104
|
+
No ``align`` field: alignment is a placement decision owned by the
|
|
1105
|
+
family-specific renderer (endpoint labels left-anchor in their pane;
|
|
1106
|
+
callouts anchor along their leader line; etc.).
|
|
1107
|
+
"""
|
|
1108
|
+
|
|
1109
|
+
model_config = ConfigDict(extra="forbid")
|
|
1110
|
+
|
|
1111
|
+
# Empty FontStyle starter; cascade fills missing fields from
|
|
1112
|
+
# ``charts.font`` in ``_apply_cascade`` (same pattern as
|
|
1113
|
+
# ``TextMarkStyle.font`` and ``TotalSlotStyle.font``).
|
|
1114
|
+
font: FontStyle = Field(
|
|
1115
|
+
default_factory=FontStyle,
|
|
1116
|
+
description="Series label font style overrides; cascade fills missing fields from charts.font.",
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
class TotalSlotStyle(BaseModel):
|
|
1121
|
+
"""Theme slot for one text element of the donut center total."""
|
|
1122
|
+
|
|
1123
|
+
model_config = ConfigDict(extra="forbid")
|
|
1124
|
+
|
|
1125
|
+
# Empty FontStyle starter; cascade fills missing fields from
|
|
1126
|
+
# ``charts.font`` (same pattern as KpiStyle).
|
|
1127
|
+
font: FontStyle = Field(
|
|
1128
|
+
default_factory=FontStyle,
|
|
1129
|
+
description="Donut center total element font style overrides.",
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
class TotalStyle(BaseModel):
|
|
1134
|
+
"""Donut center total paint: value (the number) and label (the caption)."""
|
|
1135
|
+
|
|
1136
|
+
model_config = ConfigDict(extra="forbid")
|
|
1137
|
+
|
|
1138
|
+
value: TotalSlotStyle = Field(
|
|
1139
|
+
description="Style for the donut center value (the number)."
|
|
1140
|
+
)
|
|
1141
|
+
label: TotalSlotStyle = Field(
|
|
1142
|
+
description="Style for the donut center label (the caption)."
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
class SliceLabelsStyle(BaseModel):
|
|
1147
|
+
"""Pie labels: typography + positioning offsets for per-slice text."""
|
|
1148
|
+
|
|
1149
|
+
model_config = ConfigDict(extra="forbid")
|
|
1150
|
+
|
|
1151
|
+
offset: float = Field(
|
|
1152
|
+
description="Radial offset of slice labels from the arc in pixels."
|
|
1153
|
+
)
|
|
1154
|
+
block_height: float = Field(
|
|
1155
|
+
description="Reserved height per label block in pixels."
|
|
1156
|
+
)
|
|
1157
|
+
line_height: float = Field(
|
|
1158
|
+
description="Line height for multi-line slice labels in pixels."
|
|
1159
|
+
)
|
|
1160
|
+
# Empty FontStyle starter; the resolve cascade fills missing fields
|
|
1161
|
+
# from the parent ``charts.font``. Pattern matches KpiStyle /
|
|
1162
|
+
# LegendStyle.
|
|
1163
|
+
font: FontStyle = Field(
|
|
1164
|
+
default_factory=FontStyle, description="Slice label font style overrides."
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
# =============================================================================
|
|
1169
|
+
# CHART-FAMILY STYLE BASE CLASS HIERARCHY (ADR-015)
|
|
1170
|
+
#
|
|
1171
|
+
# Private bases (leading underscore — never authored directly):
|
|
1172
|
+
# _ChartStyleBase → required geometry/frame fields (cascade source)
|
|
1173
|
+
# _ChartStyleBaseAllOptional → all-optional variant (per-family base);
|
|
1174
|
+
# None on any field = inherit from ChartsStyle
|
|
1175
|
+
# _CartesianChartStyle → bar/line/area/scatter/histogram/heatmap/… (axis_x/y)
|
|
1176
|
+
# _RadialChartStyle → pie/donut (inner_radius, total)
|
|
1177
|
+
# _GeoChartStyle → geoshape/point_map (projection, basemap)
|
|
1178
|
+
# KpiChartStyle, TableChartStyle inherit _ChartStyleBaseAllOptional directly
|
|
1179
|
+
# (no palette, legend, tooltip, or marks pipeline)
|
|
1180
|
+
# =============================================================================
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
class ProjectionStyle(BaseModel):
|
|
1184
|
+
"""Vega-Lite map projection configuration for geo chart families."""
|
|
1185
|
+
|
|
1186
|
+
model_config = ConfigDict(extra="forbid")
|
|
1187
|
+
|
|
1188
|
+
type: str = Field(
|
|
1189
|
+
description="Vega-Lite projection type (e.g. 'mercator', 'albersUsa', 'equalEarth')."
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
class BasemapStyle(BaseModel):
|
|
1194
|
+
"""Background map layer for geo charts (especially point maps).
|
|
1195
|
+
|
|
1196
|
+
``source`` is the topography source identifier. None means no background
|
|
1197
|
+
topo layer — the chart renders points or shapes on a blank canvas.
|
|
1198
|
+
"""
|
|
1199
|
+
|
|
1200
|
+
model_config = ConfigDict(extra="forbid")
|
|
1201
|
+
|
|
1202
|
+
# None = no background layer. Point maps may omit the basemap entirely
|
|
1203
|
+
# when the data projection is self-explanatory (e.g. lat/lng scatter).
|
|
1204
|
+
source: str | None = Field(
|
|
1205
|
+
default=None,
|
|
1206
|
+
description="Background topo source identifier; None means no base layer.",
|
|
1207
|
+
)
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
class _ChartStyleBase(BaseModel):
|
|
1211
|
+
"""Required geometry/frame fields — the cascade source for all chart families.
|
|
1212
|
+
|
|
1213
|
+
``ChartsStyle`` inherits this class to supply the authoritative global values.
|
|
1214
|
+
``_ChartStyleBaseAllOptional`` (generated via ``build_patch_model``) is used as
|
|
1215
|
+
the per-family base, where ``None`` on any field means "inherit from
|
|
1216
|
+
``ChartsStyle`` via the cascade".
|
|
1217
|
+
"""
|
|
1218
|
+
|
|
1219
|
+
model_config = ConfigDict(extra="forbid")
|
|
1220
|
+
|
|
1221
|
+
font: FontStyle = Field(
|
|
1222
|
+
default_factory=FontStyle,
|
|
1223
|
+
description="Chart-level font overrides.",
|
|
1224
|
+
)
|
|
1225
|
+
aspect_ratio: float = Field(description="Chart aspect ratio (width/height).")
|
|
1226
|
+
min_height: float = Field(description="Minimum chart height in pixels.")
|
|
1227
|
+
max_height: float = Field(description="Maximum chart height in pixels.")
|
|
1228
|
+
padding: PaddingStyle = Field(description="Chart inner padding in pixels.")
|
|
1229
|
+
border: BorderStyle = Field(description="Chart card border style.")
|
|
1230
|
+
animation_duration: float = Field(
|
|
1231
|
+
description="Vega-Lite animation duration in milliseconds."
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
# Color palette — accepts an explicit list of stops or a categorical
|
|
1235
|
+
# palette name (e.g. ``"category-10"``). The string form is expanded to
|
|
1236
|
+
# the palette's stops at validation time so the field always materializes
|
|
1237
|
+
# as ``list[str]`` downstream.
|
|
1238
|
+
# ``| str`` is present so generated patch models (which don't inherit the
|
|
1239
|
+
# validator) accept string palette names; the cascade expands them.
|
|
1240
|
+
palette: list[str] | str = Field(
|
|
1241
|
+
description="Ordered list of categorical color stops or a palette name; expanded from a palette name at validation time."
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
@field_validator("palette", mode="before")
|
|
1245
|
+
@classmethod
|
|
1246
|
+
def _expand_palette_name(cls, value: Any) -> Any:
|
|
1247
|
+
if isinstance(value, str):
|
|
1248
|
+
from dataface.core.compile.palette import palette as resolve_palette
|
|
1249
|
+
|
|
1250
|
+
return resolve_palette(value)
|
|
1251
|
+
return value
|
|
1252
|
+
|
|
1253
|
+
# Dash palette for line-family marks. None means "no strokeDash encoding";
|
|
1254
|
+
# themes that want dash-based categorical distinction set this. Cascade-
|
|
1255
|
+
# managed sentinel — the render layer reads None and skips emission.
|
|
1256
|
+
dashes: list[list[int]] | None = Field(
|
|
1257
|
+
default=None,
|
|
1258
|
+
description="Ordered list of Vega-Lite strokeDash arrays for line-family categorical encoding; None disables dash emission.",
|
|
1259
|
+
)
|
|
1260
|
+
|
|
1261
|
+
# Inference
|
|
1262
|
+
inference: InferenceStyle = Field(
|
|
1263
|
+
default_factory=InferenceStyle, description="Engine inference behavior flags."
|
|
1264
|
+
)
|
|
1265
|
+
|
|
1266
|
+
# Legend
|
|
1267
|
+
legend: LegendStyle = Field(description="Chart legend style.")
|
|
1268
|
+
|
|
1269
|
+
# Tooltip
|
|
1270
|
+
tooltip: TooltipStyle = Field(description="Chart tooltip style.")
|
|
1271
|
+
|
|
1272
|
+
# Cascade-managed sentinels — chart-local paint and typography overrides.
|
|
1273
|
+
# None propagates unchanged through the cascade; no theme-level default
|
|
1274
|
+
# exists for these per-chart fields. ChartsStyle inherits these as optional
|
|
1275
|
+
# fields that theme YAML may omit.
|
|
1276
|
+
background: str | None = Field(
|
|
1277
|
+
default=None,
|
|
1278
|
+
description="Chart-local background color override; None inherits from theme.",
|
|
1279
|
+
)
|
|
1280
|
+
# StyleColorConfig enables gradient-scale color mode (style.color: {scale: ...}).
|
|
1281
|
+
# The cascade stores only str values in MergedChartsStyle.color (see style_cascade.py);
|
|
1282
|
+
# StyleColorConfig is consumed ephemerally by normalize_chart_channels in the pipeline.
|
|
1283
|
+
color: str | StyleColorConfig | None = Field(
|
|
1284
|
+
default=None,
|
|
1285
|
+
description="Chart-local static mark color (CSS string) or gradient scale config; None uses the theme palette.",
|
|
1286
|
+
)
|
|
1287
|
+
title: TitleStyle | None = Field(
|
|
1288
|
+
default=None,
|
|
1289
|
+
description="Chart-level title style override; None inherits the theme title style.",
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
# All-optional variant of _ChartStyleBase — per-family styles use this as their base.
|
|
1294
|
+
# None on any field is a cascade sentinel meaning "inherit from ChartsStyle global".
|
|
1295
|
+
# build_patch_model_ext with is_recursive=True converts nested BaseModel fields to
|
|
1296
|
+
# their patch types (e.g. LegendStyle -> LegendStylePatch | None), so per-family
|
|
1297
|
+
# theme overrides like bar.legend.disable: false validate cleanly with partial dicts.
|
|
1298
|
+
if TYPE_CHECKING:
|
|
1299
|
+
|
|
1300
|
+
class _ChartStyleBaseAllOptional(_ChartStyleBase):
|
|
1301
|
+
pass
|
|
1302
|
+
|
|
1303
|
+
else:
|
|
1304
|
+
_ChartStyleBaseAllOptional = build_patch_model_ext(
|
|
1305
|
+
_ChartStyleBase, is_recursive=True
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
class _CartesianChartStyle(_ChartStyleBaseAllOptional):
|
|
1310
|
+
"""Cartesian families: bar, line, area, scatter, histogram, heatmap, boxplot, errorbar/band.
|
|
1311
|
+
|
|
1312
|
+
Shared axis tier — theme YAML populates per-family axis overrides here.
|
|
1313
|
+
"""
|
|
1314
|
+
|
|
1315
|
+
axis: AxisStylePatch | None = Field(
|
|
1316
|
+
default=None,
|
|
1317
|
+
description="Override applied to both x and y axes; None inherits global axis defaults.",
|
|
1318
|
+
)
|
|
1319
|
+
axis_x: AxisStylePatch | None = Field(
|
|
1320
|
+
default=None,
|
|
1321
|
+
description="Per-chart-type x-axis style overrides; None inherits global axis defaults.",
|
|
1322
|
+
)
|
|
1323
|
+
axis_y: AxisStylePatch | None = Field(
|
|
1324
|
+
default=None,
|
|
1325
|
+
description="Per-chart-type y-axis style overrides; None inherits global axis defaults.",
|
|
1326
|
+
)
|
|
1327
|
+
axis_quantitative: AxisStylePatch | None = Field(
|
|
1328
|
+
default=None,
|
|
1329
|
+
description="Per-chart-type quantitative-axis overrides; None inherits global defaults.",
|
|
1330
|
+
)
|
|
1331
|
+
axis_band: AxisStylePatch | None = Field(
|
|
1332
|
+
default=None,
|
|
1333
|
+
description="Per-chart-type categorical (band) axis overrides; None inherits global band defaults.",
|
|
1334
|
+
)
|
|
1335
|
+
number_format: str | None = Field(
|
|
1336
|
+
default=None,
|
|
1337
|
+
description="Default number format for axes and tooltips (D3 format string); None inherits from theme.",
|
|
1338
|
+
)
|
|
1339
|
+
time_format: str | None = Field(
|
|
1340
|
+
default=None,
|
|
1341
|
+
description="Default time format for temporal axes (D3 time format string); None inherits from theme.",
|
|
1342
|
+
)
|
|
1343
|
+
scale: ScaleStyle | None = Field(
|
|
1344
|
+
default=None,
|
|
1345
|
+
description="Chart-type encoding scale overrides applied to both x and y; None means no override.",
|
|
1346
|
+
)
|
|
1347
|
+
range: RangeStylePatch | None = Field(
|
|
1348
|
+
default=None,
|
|
1349
|
+
description="Color range/palette overrides for this chart type; None means no override.",
|
|
1350
|
+
)
|
|
1351
|
+
data_table: DataTableStylePatch | None = Field(
|
|
1352
|
+
default=None,
|
|
1353
|
+
description="Per-chart-type data_table style override; None uses the universal style.charts.data_table.",
|
|
1354
|
+
)
|
|
1355
|
+
|
|
1356
|
+
|
|
1357
|
+
class _RadialChartStyle(_ChartStyleBaseAllOptional):
|
|
1358
|
+
"""Radial families: pie, donut."""
|
|
1359
|
+
|
|
1360
|
+
# Optional sentinel: None means 'no inner radius' (solid pie); float means donut.
|
|
1361
|
+
# The None state is meaningful in the resolved model — NOT a 'theme failed to populate'
|
|
1362
|
+
# fallback. Cascade preserves None through merge so the renderer can branch on solid
|
|
1363
|
+
# vs. donut without re-reading raw YAML.
|
|
1364
|
+
inner_radius: float | None = Field(
|
|
1365
|
+
default=None,
|
|
1366
|
+
ge=0,
|
|
1367
|
+
le=1,
|
|
1368
|
+
description="Hole-to-disk ratio 0–1 (inner radius / outer radius). None = solid pie (no hole).",
|
|
1369
|
+
)
|
|
1370
|
+
total: TotalStyle = Field(description="Donut center total paint (value and label).")
|
|
1371
|
+
|
|
1372
|
+
|
|
1373
|
+
class _GeoChartStyle(_ChartStyleBaseAllOptional):
|
|
1374
|
+
"""Geo families: geoshape (choropleth), point_map.
|
|
1375
|
+
|
|
1376
|
+
``geo_source`` is intentionally absent — it is a chart-root channel field
|
|
1377
|
+
(on ``_GeoChartFields``), not a style concern.
|
|
1378
|
+
``color_scheme`` is choropleth-specific — declared on ``GeoshapeChartStyle`` leaf only.
|
|
1379
|
+
"""
|
|
1380
|
+
|
|
1381
|
+
projection: ProjectionStyle = Field(
|
|
1382
|
+
description="Vega-Lite projection configuration for this geo family."
|
|
1383
|
+
)
|
|
1384
|
+
basemap: BasemapStyle = Field(
|
|
1385
|
+
default_factory=BasemapStyle,
|
|
1386
|
+
description="Background map layer; None source = no topo layer.",
|
|
1387
|
+
)
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
# =============================================================================
|
|
1391
|
+
# MARK STYLE CLASSES (<Mark>MarkStyle)
|
|
1392
|
+
#
|
|
1393
|
+
# One class per VL mark type. Each carries ONLY mark-level properties
|
|
1394
|
+
# (geometry, opacity, stroke) — no chart-level concerns (axis, legend, etc.).
|
|
1395
|
+
#
|
|
1396
|
+
# ALL fields are Optional (None = "not specified in this tier"). These types
|
|
1397
|
+
# serve as cascade-tier containers in the three-tier mark cascade:
|
|
1398
|
+
# tier-1 GlobalMarksStyle → tier-2 <Family>ChartMarksStyle → tier-3 face override
|
|
1399
|
+
# None means "inherit from a lower-numbered tier", not "theme failed to populate".
|
|
1400
|
+
# resolve_mark(global, family) merges tiers using deep_merge (exclude_unset).
|
|
1401
|
+
#
|
|
1402
|
+
# GlobalMarksStyle aggregates all 17 marks; per-family ChartMarksStyle
|
|
1403
|
+
# exposes only the marks that family actually emits.
|
|
1404
|
+
# =============================================================================
|
|
1405
|
+
|
|
1406
|
+
|
|
1407
|
+
class BarMarkStyle(BaseModel):
|
|
1408
|
+
"""Bar mark geometry and stroke. Chart-level bar fields live on BarChartStyle.
|
|
1409
|
+
|
|
1410
|
+
All fields None = "not set at this tier; inherit from global".
|
|
1411
|
+
resolve_mark() merges the final value across tiers.
|
|
1412
|
+
"""
|
|
1413
|
+
|
|
1414
|
+
model_config = ConfigDict(extra="forbid")
|
|
1415
|
+
|
|
1416
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1417
|
+
border: BorderStyle | None = Field(
|
|
1418
|
+
default=None,
|
|
1419
|
+
description="Bar border style (color/width also serve as the bar stroke).",
|
|
1420
|
+
)
|
|
1421
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1422
|
+
padding: float | None = Field(
|
|
1423
|
+
default=None, description="Padding around bar marks in pixels."
|
|
1424
|
+
)
|
|
1425
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1426
|
+
size: float | None = Field(
|
|
1427
|
+
default=None, description="Bar width in pixels (fixed-width mode)."
|
|
1428
|
+
)
|
|
1429
|
+
# Bar width as a fraction of the band step (0..1). Maps to VL
|
|
1430
|
+
# ``mark.width: {band: N}`` and works on temporal/utc x scales so
|
|
1431
|
+
# time-unit bars can preserve gaps while keeping consistent column widths.
|
|
1432
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1433
|
+
band_width: float | None = Field(
|
|
1434
|
+
default=None,
|
|
1435
|
+
description="Bar width as a fraction of the band step (0–1).",
|
|
1436
|
+
)
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
class LineMarkStyle(BaseModel):
|
|
1440
|
+
"""Line mark stroke, interpolation, and halo. Point mark lives on PointMarkStyle.
|
|
1441
|
+
|
|
1442
|
+
All fields None = "not set at this tier; inherit from global".
|
|
1443
|
+
"""
|
|
1444
|
+
|
|
1445
|
+
model_config = ConfigDict(extra="forbid")
|
|
1446
|
+
|
|
1447
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1448
|
+
stroke: StrokeStyle | None = Field(default=None, description="Line stroke style.")
|
|
1449
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1450
|
+
curve: str | None = Field(
|
|
1451
|
+
default=None,
|
|
1452
|
+
description="Line interpolation curve (e.g. 'linear', 'monotone').",
|
|
1453
|
+
)
|
|
1454
|
+
# Halo: a knockout-coloured line drawn behind the foreground line so that
|
|
1455
|
+
# crossings read cleanly. ``halo_multiplier`` of 0 disables it; otherwise
|
|
1456
|
+
# the halo stroke width and (when points are enabled) halo point size are
|
|
1457
|
+
# ``base * halo_multiplier``. Halo colour is the chart's effective background.
|
|
1458
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1459
|
+
halo_multiplier: float | None = Field(
|
|
1460
|
+
default=None,
|
|
1461
|
+
description="Halo stroke width multiplier relative to stroke.width; 0 disables the halo.",
|
|
1462
|
+
)
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
class AreaMarkStyle(BaseModel):
|
|
1466
|
+
"""Area mark fill opacity, stroke, and halo.
|
|
1467
|
+
|
|
1468
|
+
All fields None = "not set at this tier; inherit from global".
|
|
1469
|
+
"""
|
|
1470
|
+
|
|
1471
|
+
model_config = ConfigDict(extra="forbid")
|
|
1472
|
+
|
|
1473
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1474
|
+
opacity: float | None = Field(default=None, description="Area fill opacity (0–1).")
|
|
1475
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1476
|
+
stroke: StrokeStyle | None = Field(
|
|
1477
|
+
default=None, description="Area top-edge stroke style."
|
|
1478
|
+
)
|
|
1479
|
+
# Halo: a knockout-coloured stroke drawn behind the foreground area's top-edge
|
|
1480
|
+
# line so that crossings between series read cleanly.
|
|
1481
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1482
|
+
halo_multiplier: float | None = Field(
|
|
1483
|
+
default=None,
|
|
1484
|
+
description="Halo stroke width multiplier relative to stroke.width; 0 disables the halo.",
|
|
1485
|
+
)
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
class PointMarkStyle(BaseModel):
|
|
1489
|
+
"""Point mark style (data-point markers on line/area/scatter charts).
|
|
1490
|
+
|
|
1491
|
+
All fields are Optional. None = "not specified / inherit from global or VL default".
|
|
1492
|
+
"""
|
|
1493
|
+
|
|
1494
|
+
model_config = ConfigDict(extra="forbid")
|
|
1495
|
+
|
|
1496
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1497
|
+
size: float | None = Field(
|
|
1498
|
+
default=None,
|
|
1499
|
+
description="Point size in square pixels; 0 disables points on line charts.",
|
|
1500
|
+
)
|
|
1501
|
+
# None = inherits from chart encoding color channel (VL default behavior)
|
|
1502
|
+
color: str | None = Field(
|
|
1503
|
+
default=None, description="Point color; None inherits the series color."
|
|
1504
|
+
)
|
|
1505
|
+
# None = VL default shape for point marks
|
|
1506
|
+
shape: str | None = Field(
|
|
1507
|
+
default=None,
|
|
1508
|
+
description="Point shape (e.g. 'circle', 'square'); None uses VL default.",
|
|
1509
|
+
)
|
|
1510
|
+
# None = VL default opacity; scatter theme populates; line renderer forces 1 for halo clarity
|
|
1511
|
+
opacity: float | None = Field(
|
|
1512
|
+
default=None, description="Point opacity 0–1; None uses VL default."
|
|
1513
|
+
)
|
|
1514
|
+
# None = VL default fill; scatter theme populates
|
|
1515
|
+
filled: bool | None = Field(
|
|
1516
|
+
default=None, description="Whether points are filled; None uses VL default."
|
|
1517
|
+
)
|
|
1518
|
+
# None = no fill override; theme sets theme.background so hollow points get
|
|
1519
|
+
# a solid background automatically when filled=false.
|
|
1520
|
+
fill: str | None = Field(
|
|
1521
|
+
default=None,
|
|
1522
|
+
description="Point interior fill color; only applied when filled=false.",
|
|
1523
|
+
)
|
|
1524
|
+
|
|
1525
|
+
|
|
1526
|
+
class SliceMarkStyle(BaseModel):
|
|
1527
|
+
"""Pie/donut slice mark — mark-level paint and layout only.
|
|
1528
|
+
|
|
1529
|
+
No chart geometry (aspect_ratio, inner_radius), no sub-components (total).
|
|
1530
|
+
All fields None = "not set at this tier; inherit from global".
|
|
1531
|
+
"""
|
|
1532
|
+
|
|
1533
|
+
model_config = ConfigDict(extra="forbid")
|
|
1534
|
+
|
|
1535
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1536
|
+
gap: float | None = Field(
|
|
1537
|
+
default=None, description="Angular gap between slices in radians."
|
|
1538
|
+
)
|
|
1539
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1540
|
+
corner_radius: float | None = Field(
|
|
1541
|
+
default=None, description="Corner radius of arc slices in pixels."
|
|
1542
|
+
)
|
|
1543
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1544
|
+
stroke: StrokeStyle | None = Field(
|
|
1545
|
+
default=None, description="Arc slice stroke style."
|
|
1546
|
+
)
|
|
1547
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1548
|
+
labels: SliceLabelsStyle | None = Field(
|
|
1549
|
+
default=None, description="Per-slice label style."
|
|
1550
|
+
)
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
class RuleMarkStyle(BaseModel):
|
|
1554
|
+
"""Rule (reference line) mark opacity and stroke."""
|
|
1555
|
+
|
|
1556
|
+
model_config = ConfigDict(extra="forbid")
|
|
1557
|
+
|
|
1558
|
+
# Cascade-managed sentinels: None until _fill_stroke_color sets stroke.color.
|
|
1559
|
+
opacity: float | None = Field(
|
|
1560
|
+
default=None,
|
|
1561
|
+
description="Mark opacity (0–1); None means not overridden at this level.",
|
|
1562
|
+
)
|
|
1563
|
+
stroke: StrokeStyle | None = Field(
|
|
1564
|
+
default=None,
|
|
1565
|
+
description="Mark stroke style; None means not overridden at this level.",
|
|
1566
|
+
)
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
class RectMarkStyle(BaseModel):
|
|
1570
|
+
"""Rect mark opacity and stroke."""
|
|
1571
|
+
|
|
1572
|
+
model_config = ConfigDict(extra="forbid")
|
|
1573
|
+
|
|
1574
|
+
opacity: float | None = Field(
|
|
1575
|
+
default=None,
|
|
1576
|
+
description="Mark opacity (0–1); None means not overridden at this level.",
|
|
1577
|
+
)
|
|
1578
|
+
stroke: StrokeStyle | None = Field(
|
|
1579
|
+
default=None,
|
|
1580
|
+
description="Mark stroke style; None means not overridden at this level.",
|
|
1581
|
+
)
|
|
1582
|
+
|
|
1583
|
+
|
|
1584
|
+
class CircleMarkStyle(BaseModel):
|
|
1585
|
+
"""Circle mark opacity and stroke."""
|
|
1586
|
+
|
|
1587
|
+
model_config = ConfigDict(extra="forbid")
|
|
1588
|
+
|
|
1589
|
+
opacity: float | None = Field(
|
|
1590
|
+
default=None,
|
|
1591
|
+
description="Mark opacity (0–1); None means not overridden at this level.",
|
|
1592
|
+
)
|
|
1593
|
+
stroke: StrokeStyle | None = Field(
|
|
1594
|
+
default=None,
|
|
1595
|
+
description="Mark stroke style; None means not overridden at this level.",
|
|
1596
|
+
)
|
|
1597
|
+
|
|
1598
|
+
|
|
1599
|
+
class SquareMarkStyle(BaseModel):
|
|
1600
|
+
"""Square mark opacity and stroke."""
|
|
1601
|
+
|
|
1602
|
+
model_config = ConfigDict(extra="forbid")
|
|
1603
|
+
|
|
1604
|
+
opacity: float | None = Field(
|
|
1605
|
+
default=None,
|
|
1606
|
+
description="Mark opacity (0–1); None means not overridden at this level.",
|
|
1607
|
+
)
|
|
1608
|
+
stroke: StrokeStyle | None = Field(
|
|
1609
|
+
default=None,
|
|
1610
|
+
description="Mark stroke style; None means not overridden at this level.",
|
|
1611
|
+
)
|
|
1612
|
+
|
|
1613
|
+
|
|
1614
|
+
class TickMarkStyle(BaseModel):
|
|
1615
|
+
"""Tick mark opacity and stroke."""
|
|
1616
|
+
|
|
1617
|
+
model_config = ConfigDict(extra="forbid")
|
|
1618
|
+
|
|
1619
|
+
opacity: float | None = Field(
|
|
1620
|
+
default=None,
|
|
1621
|
+
description="Mark opacity (0–1); None means not overridden at this level.",
|
|
1622
|
+
)
|
|
1623
|
+
stroke: StrokeStyle | None = Field(
|
|
1624
|
+
default=None,
|
|
1625
|
+
description="Mark stroke style; None means not overridden at this level.",
|
|
1626
|
+
)
|
|
1627
|
+
|
|
1628
|
+
|
|
1629
|
+
class TrailMarkStyle(BaseModel):
|
|
1630
|
+
"""Trail mark opacity and stroke."""
|
|
1631
|
+
|
|
1632
|
+
model_config = ConfigDict(extra="forbid")
|
|
1633
|
+
|
|
1634
|
+
opacity: float | None = Field(
|
|
1635
|
+
default=None,
|
|
1636
|
+
description="Mark opacity (0–1); None means not overridden at this level.",
|
|
1637
|
+
)
|
|
1638
|
+
stroke: StrokeStyle | None = Field(
|
|
1639
|
+
default=None,
|
|
1640
|
+
description="Mark stroke style; None means not overridden at this level.",
|
|
1641
|
+
)
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
class GeoshapeMarkStyle(BaseModel):
|
|
1645
|
+
"""Geoshape (choropleth) mark boundary stroke.
|
|
1646
|
+
|
|
1647
|
+
Cascade tier sentinel: None means "not specified at this tier".
|
|
1648
|
+
"""
|
|
1649
|
+
|
|
1650
|
+
model_config = ConfigDict(extra="forbid")
|
|
1651
|
+
|
|
1652
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1653
|
+
stroke: StrokeStyle | None = Field(
|
|
1654
|
+
default=None, description="Geoshape boundary stroke style."
|
|
1655
|
+
)
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
class ImageMarkStyle(BaseModel):
|
|
1659
|
+
"""Image mark aspect ratio preservation."""
|
|
1660
|
+
|
|
1661
|
+
model_config = ConfigDict(extra="forbid")
|
|
1662
|
+
|
|
1663
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1664
|
+
aspect: bool | None = Field(
|
|
1665
|
+
default=None, description="Preserve image aspect ratio when scaling."
|
|
1666
|
+
)
|
|
1667
|
+
|
|
1668
|
+
|
|
1669
|
+
class BoxplotMarkStyle(BaseModel):
|
|
1670
|
+
"""Boxplot composite mark whisker extent."""
|
|
1671
|
+
|
|
1672
|
+
model_config = ConfigDict(extra="forbid")
|
|
1673
|
+
|
|
1674
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1675
|
+
extent: str | None = Field(
|
|
1676
|
+
default=None,
|
|
1677
|
+
description="Boxplot whisker extent ('min-max' or a k-IQR multiplier as string).",
|
|
1678
|
+
)
|
|
1679
|
+
|
|
1680
|
+
|
|
1681
|
+
class ErrorbarMarkStyle(BaseModel):
|
|
1682
|
+
"""Error bar composite mark tick visibility."""
|
|
1683
|
+
|
|
1684
|
+
model_config = ConfigDict(extra="forbid")
|
|
1685
|
+
|
|
1686
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1687
|
+
ticks: bool | None = Field(
|
|
1688
|
+
default=None,
|
|
1689
|
+
description="Show tick marks at the ends of error bar whiskers.",
|
|
1690
|
+
)
|
|
1691
|
+
|
|
1692
|
+
|
|
1693
|
+
class ErrorbandMarkStyle(BaseModel):
|
|
1694
|
+
"""Error band composite mark fill opacity."""
|
|
1695
|
+
|
|
1696
|
+
model_config = ConfigDict(extra="forbid")
|
|
1697
|
+
|
|
1698
|
+
# Cascade tier sentinel: None means "not specified at this tier".
|
|
1699
|
+
opacity: float | None = Field(
|
|
1700
|
+
default=None, description="Error band fill opacity (0–1)."
|
|
1701
|
+
)
|
|
1702
|
+
|
|
1703
|
+
|
|
1704
|
+
# =============================================================================
|
|
1705
|
+
# GLOBAL MARKS STYLE
|
|
1706
|
+
# =============================================================================
|
|
1707
|
+
|
|
1708
|
+
|
|
1709
|
+
class GlobalMarksStyle(BaseModel):
|
|
1710
|
+
"""Global mark defaults: one <Mark>MarkStyle per VL mark type.
|
|
1711
|
+
|
|
1712
|
+
Tier 1 of the three-tier mark cascade:
|
|
1713
|
+
GlobalMarksStyle → <Family>ChartMarksStyle → face-local marks patch.
|
|
1714
|
+
|
|
1715
|
+
Keys match VL mark type names. ``text`` is the renamed ``label`` slot.
|
|
1716
|
+
All 17 fields have default_factory so the theme YAML can omit rarely-styled
|
|
1717
|
+
marks (image, boxplot, etc.) without breaking validation.
|
|
1718
|
+
"""
|
|
1719
|
+
|
|
1720
|
+
model_config = ConfigDict(extra="forbid")
|
|
1721
|
+
|
|
1722
|
+
bar: BarMarkStyle = Field(
|
|
1723
|
+
default_factory=BarMarkStyle,
|
|
1724
|
+
description="Global bar mark defaults.",
|
|
1725
|
+
)
|
|
1726
|
+
line: LineMarkStyle = Field(
|
|
1727
|
+
default_factory=LineMarkStyle,
|
|
1728
|
+
description="Global line mark defaults.",
|
|
1729
|
+
)
|
|
1730
|
+
area: AreaMarkStyle = Field(
|
|
1731
|
+
default_factory=AreaMarkStyle,
|
|
1732
|
+
description="Global area mark defaults.",
|
|
1733
|
+
)
|
|
1734
|
+
point: PointMarkStyle = Field(
|
|
1735
|
+
default_factory=PointMarkStyle,
|
|
1736
|
+
description="Global point mark defaults.",
|
|
1737
|
+
)
|
|
1738
|
+
slice: SliceMarkStyle = Field(
|
|
1739
|
+
default_factory=SliceMarkStyle,
|
|
1740
|
+
description="Global slice (arc/pie) mark defaults.",
|
|
1741
|
+
)
|
|
1742
|
+
# `text` is the renamed `label` slot. ChartsStyle rejects the old `label` key.
|
|
1743
|
+
text: TextMarkStyle = Field(
|
|
1744
|
+
default_factory=TextMarkStyle,
|
|
1745
|
+
description="Global text mark defaults.",
|
|
1746
|
+
)
|
|
1747
|
+
rule: RuleMarkStyle = Field(
|
|
1748
|
+
default_factory=RuleMarkStyle,
|
|
1749
|
+
description="Global rule mark defaults.",
|
|
1750
|
+
)
|
|
1751
|
+
rect: RectMarkStyle = Field(
|
|
1752
|
+
default_factory=RectMarkStyle,
|
|
1753
|
+
description="Global rect mark defaults.",
|
|
1754
|
+
)
|
|
1755
|
+
circle: CircleMarkStyle = Field(
|
|
1756
|
+
default_factory=CircleMarkStyle,
|
|
1757
|
+
description="Global circle mark defaults.",
|
|
1758
|
+
)
|
|
1759
|
+
square: SquareMarkStyle = Field(
|
|
1760
|
+
default_factory=SquareMarkStyle,
|
|
1761
|
+
description="Global square mark defaults.",
|
|
1762
|
+
)
|
|
1763
|
+
tick: TickMarkStyle = Field(
|
|
1764
|
+
default_factory=TickMarkStyle,
|
|
1765
|
+
description="Global tick mark defaults.",
|
|
1766
|
+
)
|
|
1767
|
+
trail: TrailMarkStyle = Field(
|
|
1768
|
+
default_factory=TrailMarkStyle,
|
|
1769
|
+
description="Global trail mark defaults.",
|
|
1770
|
+
)
|
|
1771
|
+
geoshape: GeoshapeMarkStyle = Field(
|
|
1772
|
+
default_factory=GeoshapeMarkStyle,
|
|
1773
|
+
description="Global geoshape mark defaults.",
|
|
1774
|
+
)
|
|
1775
|
+
image: ImageMarkStyle = Field(
|
|
1776
|
+
default_factory=ImageMarkStyle,
|
|
1777
|
+
description="Global image mark defaults.",
|
|
1778
|
+
)
|
|
1779
|
+
boxplot: BoxplotMarkStyle = Field(
|
|
1780
|
+
default_factory=BoxplotMarkStyle,
|
|
1781
|
+
description="Global boxplot mark defaults.",
|
|
1782
|
+
)
|
|
1783
|
+
errorbar: ErrorbarMarkStyle = Field(
|
|
1784
|
+
default_factory=ErrorbarMarkStyle,
|
|
1785
|
+
description="Global errorbar mark defaults.",
|
|
1786
|
+
)
|
|
1787
|
+
errorband: ErrorbandMarkStyle = Field(
|
|
1788
|
+
default_factory=ErrorbandMarkStyle,
|
|
1789
|
+
description="Global errorband mark defaults.",
|
|
1790
|
+
)
|
|
1791
|
+
|
|
1792
|
+
|
|
1793
|
+
# =============================================================================
|
|
1794
|
+
# PER-FAMILY MARKS CONTAINERS (<Family>ChartMarksStyle)
|
|
1795
|
+
#
|
|
1796
|
+
# Each exposes ONLY the mark types that the family actually emits.
|
|
1797
|
+
# extra="forbid" ensures typos or wrong-family marks are caught at schema time.
|
|
1798
|
+
# All fields default to None (theme may omit family-level mark overrides).
|
|
1799
|
+
# =============================================================================
|
|
1800
|
+
|
|
1801
|
+
|
|
1802
|
+
class BarChartMarksStyle(BaseModel):
|
|
1803
|
+
"""Bar-family mark overrides. Only bar and text (endpoint labels) are valid."""
|
|
1804
|
+
|
|
1805
|
+
model_config = ConfigDict(extra="forbid")
|
|
1806
|
+
|
|
1807
|
+
bar: BarMarkStyle | None = Field(
|
|
1808
|
+
default=None,
|
|
1809
|
+
description="Bar mark overrides for bar charts; None inherits global.",
|
|
1810
|
+
)
|
|
1811
|
+
text: TextMarkStyle | None = Field(
|
|
1812
|
+
default=None,
|
|
1813
|
+
description="Text mark overrides for bar endpoint labels; None inherits global.",
|
|
1814
|
+
)
|
|
1815
|
+
|
|
1816
|
+
|
|
1817
|
+
class LineChartMarksStyle(BaseModel):
|
|
1818
|
+
"""Line-family mark overrides."""
|
|
1819
|
+
|
|
1820
|
+
model_config = ConfigDict(extra="forbid")
|
|
1821
|
+
|
|
1822
|
+
line: LineMarkStyle | None = Field(
|
|
1823
|
+
default=None, description="Line mark overrides; None inherits global."
|
|
1824
|
+
)
|
|
1825
|
+
point: PointMarkStyle | None = Field(
|
|
1826
|
+
default=None, description="Point mark overrides; None inherits global."
|
|
1827
|
+
)
|
|
1828
|
+
text: TextMarkStyle | None = Field(
|
|
1829
|
+
default=None, description="Text mark overrides; None inherits global."
|
|
1830
|
+
)
|
|
1831
|
+
rule: RuleMarkStyle | None = Field(
|
|
1832
|
+
default=None,
|
|
1833
|
+
description="Rule mark overrides; None inherits global.",
|
|
1834
|
+
)
|
|
1835
|
+
|
|
1836
|
+
|
|
1837
|
+
class AreaChartMarksStyle(BaseModel):
|
|
1838
|
+
"""Area-family mark overrides."""
|
|
1839
|
+
|
|
1840
|
+
model_config = ConfigDict(extra="forbid")
|
|
1841
|
+
|
|
1842
|
+
area: AreaMarkStyle | None = Field(
|
|
1843
|
+
default=None, description="Area mark overrides; None inherits global."
|
|
1844
|
+
)
|
|
1845
|
+
line: LineMarkStyle | None = Field(
|
|
1846
|
+
default=None, description="Top-edge line mark overrides; None inherits global."
|
|
1847
|
+
)
|
|
1848
|
+
point: PointMarkStyle | None = Field(
|
|
1849
|
+
default=None, description="Point mark overrides; None inherits global."
|
|
1850
|
+
)
|
|
1851
|
+
text: TextMarkStyle | None = Field(
|
|
1852
|
+
default=None, description="Text mark overrides; None inherits global."
|
|
1853
|
+
)
|
|
1854
|
+
|
|
1855
|
+
|
|
1856
|
+
class ScatterChartMarksStyle(BaseModel):
|
|
1857
|
+
"""Scatter-family mark overrides."""
|
|
1858
|
+
|
|
1859
|
+
model_config = ConfigDict(extra="forbid")
|
|
1860
|
+
|
|
1861
|
+
point: PointMarkStyle | None = Field(
|
|
1862
|
+
default=None, description="Point mark overrides; None inherits global."
|
|
1863
|
+
)
|
|
1864
|
+
text: TextMarkStyle | None = Field(
|
|
1865
|
+
default=None, description="Text mark overrides; None inherits global."
|
|
1866
|
+
)
|
|
1867
|
+
|
|
1868
|
+
|
|
1869
|
+
class PieChartMarksStyle(BaseModel):
|
|
1870
|
+
"""Pie/donut-family mark overrides."""
|
|
1871
|
+
|
|
1872
|
+
model_config = ConfigDict(extra="forbid")
|
|
1873
|
+
|
|
1874
|
+
slice: SliceMarkStyle | None = Field(
|
|
1875
|
+
default=None, description="Slice mark overrides; None inherits global."
|
|
1876
|
+
)
|
|
1877
|
+
text: TextMarkStyle | None = Field(
|
|
1878
|
+
default=None,
|
|
1879
|
+
description="Text mark overrides; None inherits global.",
|
|
1880
|
+
)
|
|
1881
|
+
|
|
1882
|
+
|
|
1883
|
+
class HistogramChartMarksStyle(BaseModel):
|
|
1884
|
+
"""Histogram-family mark overrides."""
|
|
1885
|
+
|
|
1886
|
+
model_config = ConfigDict(extra="forbid")
|
|
1887
|
+
|
|
1888
|
+
bar: BarMarkStyle | None = Field(
|
|
1889
|
+
default=None, description="Bar mark overrides; None inherits global."
|
|
1890
|
+
)
|
|
1891
|
+
rule: RuleMarkStyle | None = Field(
|
|
1892
|
+
default=None,
|
|
1893
|
+
description="Rule mark overrides; None inherits global.",
|
|
1894
|
+
)
|
|
1895
|
+
|
|
1896
|
+
|
|
1897
|
+
class HeatmapChartMarksStyle(BaseModel):
|
|
1898
|
+
"""Heatmap-family mark overrides."""
|
|
1899
|
+
|
|
1900
|
+
model_config = ConfigDict(extra="forbid")
|
|
1901
|
+
|
|
1902
|
+
rect: RectMarkStyle | None = Field(
|
|
1903
|
+
default=None, description="Rect mark overrides; None inherits global."
|
|
1904
|
+
)
|
|
1905
|
+
text: TextMarkStyle | None = Field(
|
|
1906
|
+
default=None,
|
|
1907
|
+
description="Text mark overrides; None inherits global.",
|
|
1908
|
+
)
|
|
1909
|
+
|
|
1910
|
+
|
|
1911
|
+
class GeoshapeChartMarksStyle(BaseModel):
|
|
1912
|
+
"""Geoshape-family mark overrides."""
|
|
1913
|
+
|
|
1914
|
+
model_config = ConfigDict(extra="forbid")
|
|
1915
|
+
|
|
1916
|
+
geoshape: GeoshapeMarkStyle | None = Field(
|
|
1917
|
+
default=None, description="Geoshape mark overrides; None inherits global."
|
|
1918
|
+
)
|
|
1919
|
+
|
|
1920
|
+
|
|
1921
|
+
class PointMapChartMarksStyle(BaseModel):
|
|
1922
|
+
"""Point map-family mark overrides."""
|
|
1923
|
+
|
|
1924
|
+
model_config = ConfigDict(extra="forbid")
|
|
1925
|
+
|
|
1926
|
+
point: PointMarkStyle | None = Field(
|
|
1927
|
+
default=None, description="Point mark overrides; None inherits global."
|
|
1928
|
+
)
|
|
1929
|
+
|
|
1930
|
+
|
|
1931
|
+
class BoxplotChartMarksStyle(BaseModel):
|
|
1932
|
+
"""Boxplot-family mark overrides."""
|
|
1933
|
+
|
|
1934
|
+
model_config = ConfigDict(extra="forbid")
|
|
1935
|
+
|
|
1936
|
+
boxplot: BoxplotMarkStyle | None = Field(
|
|
1937
|
+
default=None, description="Boxplot mark overrides; None inherits global."
|
|
1938
|
+
)
|
|
1939
|
+
|
|
1940
|
+
|
|
1941
|
+
class ErrorbarChartMarksStyle(BaseModel):
|
|
1942
|
+
"""Errorbar-family mark overrides."""
|
|
1943
|
+
|
|
1944
|
+
model_config = ConfigDict(extra="forbid")
|
|
1945
|
+
|
|
1946
|
+
errorbar: ErrorbarMarkStyle | None = Field(
|
|
1947
|
+
default=None, description="Errorbar mark overrides; None inherits global."
|
|
1948
|
+
)
|
|
1949
|
+
|
|
1950
|
+
|
|
1951
|
+
class ErrorbandChartMarksStyle(BaseModel):
|
|
1952
|
+
"""Errorband-family mark overrides."""
|
|
1953
|
+
|
|
1954
|
+
model_config = ConfigDict(extra="forbid")
|
|
1955
|
+
|
|
1956
|
+
errorband: ErrorbandMarkStyle | None = Field(
|
|
1957
|
+
default=None, description="Errorband mark overrides; None inherits global."
|
|
1958
|
+
)
|
|
1959
|
+
|
|
1960
|
+
|
|
1961
|
+
# =============================================================================
|
|
1962
|
+
# PER-FAMILY CHART STYLE LEAVES (<Family>ChartStyle)
|
|
1963
|
+
# =============================================================================
|
|
1964
|
+
|
|
1965
|
+
|
|
1966
|
+
class BarChartStyle(_CartesianChartStyle):
|
|
1967
|
+
"""Bar chart style: chart-level fields + marks sub-block."""
|
|
1968
|
+
|
|
1969
|
+
# Cascade-managed sentinels — None means "not overridden at this tier".
|
|
1970
|
+
orientation: Literal["horizontal", "vertical", "auto"] | None = Field(
|
|
1971
|
+
default=None,
|
|
1972
|
+
description="Preferred bar orientation; None uses the renderer default (vertical).",
|
|
1973
|
+
)
|
|
1974
|
+
stack: bool | Literal["zero", "normalize", "center"] | None = Field(
|
|
1975
|
+
default=None,
|
|
1976
|
+
description="Default stack mode for bar charts; None uses the renderer default (VL stacks by default).",
|
|
1977
|
+
)
|
|
1978
|
+
stack_order: Literal["value", "data", "alphabetical"] | None = Field(
|
|
1979
|
+
default=None,
|
|
1980
|
+
description=(
|
|
1981
|
+
"Z-order of stacked segments. None/'value' puts the largest aggregate at baseline. "
|
|
1982
|
+
"'data' follows SQL row order (orientation-stable not guaranteed). "
|
|
1983
|
+
"'alphabetical' sorts by color field name. Ignored when stacking is off or no color."
|
|
1984
|
+
),
|
|
1985
|
+
)
|
|
1986
|
+
# Endpoint labels: same opt-in feature as line/area. When enabled on a
|
|
1987
|
+
# multi-series stacked or grouped column chart, a right-side label pane
|
|
1988
|
+
# replaces the categorical legend with direct segment/bar labels.
|
|
1989
|
+
endpoint_labels: EndpointLabelsConfig = Field(
|
|
1990
|
+
description="Endpoint label pane configuration for bar charts."
|
|
1991
|
+
)
|
|
1992
|
+
marks: BarChartMarksStyle = Field(
|
|
1993
|
+
default_factory=BarChartMarksStyle,
|
|
1994
|
+
description="Bar-family mark overrides.",
|
|
1995
|
+
)
|
|
1996
|
+
|
|
1997
|
+
|
|
1998
|
+
class LineChartStyle(_CartesianChartStyle):
|
|
1999
|
+
"""Line chart style: chart-level fields + marks sub-block."""
|
|
2000
|
+
|
|
2001
|
+
endpoint_labels: EndpointLabelsConfig = Field(
|
|
2002
|
+
description="Endpoint label pane configuration for line charts."
|
|
2003
|
+
)
|
|
2004
|
+
marks: LineChartMarksStyle = Field(
|
|
2005
|
+
default_factory=LineChartMarksStyle,
|
|
2006
|
+
description="Line-family mark overrides.",
|
|
2007
|
+
)
|
|
2008
|
+
|
|
2009
|
+
|
|
2010
|
+
class AreaChartStyle(_CartesianChartStyle):
|
|
2011
|
+
"""Area chart style: chart-level fields + marks sub-block."""
|
|
2012
|
+
|
|
2013
|
+
# Stack default for the area family. ``False`` disables stacking
|
|
2014
|
+
# (overlapping silhouettes); a string mode is passed through to VL.
|
|
2015
|
+
# Used when a chart doesn't author ``chart.stack`` explicitly.
|
|
2016
|
+
stack: bool | Literal["zero", "normalize", "center"] = Field(
|
|
2017
|
+
description="Default stack mode for area charts; False overlaps silhouettes."
|
|
2018
|
+
)
|
|
2019
|
+
endpoint_labels: EndpointLabelsConfig = Field(
|
|
2020
|
+
description="Endpoint label pane configuration for area charts."
|
|
2021
|
+
)
|
|
2022
|
+
marks: AreaChartMarksStyle = Field(
|
|
2023
|
+
default_factory=AreaChartMarksStyle,
|
|
2024
|
+
description="Area-family mark overrides.",
|
|
2025
|
+
)
|
|
2026
|
+
|
|
2027
|
+
|
|
2028
|
+
class ScatterChartStyle(_CartesianChartStyle):
|
|
2029
|
+
"""Scatter chart style: chart-level fields + marks sub-block."""
|
|
2030
|
+
|
|
2031
|
+
marks: ScatterChartMarksStyle = Field(
|
|
2032
|
+
default_factory=ScatterChartMarksStyle,
|
|
2033
|
+
description="Scatter-family mark overrides.",
|
|
2034
|
+
)
|
|
2035
|
+
|
|
2036
|
+
|
|
2037
|
+
class PieChartStyle(_RadialChartStyle):
|
|
2038
|
+
"""Pie/donut chart style: geometry + total (flat) + marks sub-block.
|
|
2039
|
+
|
|
2040
|
+
``total`` stays flat at this level per ADR-005 (compositional slot, not a VL mark).
|
|
2041
|
+
``inner_radius`` is on _RadialChartStyle (cascade sentinel: None = solid pie).
|
|
2042
|
+
"""
|
|
2043
|
+
|
|
2044
|
+
aspect_ratio: float = Field(
|
|
2045
|
+
description="Aspect ratio (width/height) of the pie chart viewport."
|
|
2046
|
+
)
|
|
2047
|
+
marks: PieChartMarksStyle = Field(
|
|
2048
|
+
default_factory=PieChartMarksStyle,
|
|
2049
|
+
description="Pie-family mark overrides.",
|
|
2050
|
+
)
|
|
2051
|
+
|
|
2052
|
+
|
|
2053
|
+
class HistogramChartStyle(_CartesianChartStyle):
|
|
2054
|
+
"""Histogram chart style."""
|
|
2055
|
+
|
|
2056
|
+
bin_maxbins: int = Field(description="Maximum number of bins for auto-binning.")
|
|
2057
|
+
marks: HistogramChartMarksStyle = Field(
|
|
2058
|
+
default_factory=HistogramChartMarksStyle,
|
|
2059
|
+
description="Histogram-family mark overrides.",
|
|
2060
|
+
)
|
|
2061
|
+
|
|
2062
|
+
|
|
2063
|
+
class HeatmapChartStyle(_CartesianChartStyle):
|
|
2064
|
+
"""Heatmap chart style."""
|
|
2065
|
+
|
|
2066
|
+
cell_padding: float = Field(description="Padding between heatmap cells in pixels.")
|
|
2067
|
+
color_scheme: str = Field(
|
|
2068
|
+
description="Color scheme name for heatmap gradient (e.g. 'blues', 'viridis')."
|
|
2069
|
+
)
|
|
2070
|
+
marks: HeatmapChartMarksStyle = Field(
|
|
2071
|
+
default_factory=HeatmapChartMarksStyle,
|
|
2072
|
+
description="Heatmap-family mark overrides.",
|
|
2073
|
+
)
|
|
2074
|
+
|
|
2075
|
+
|
|
2076
|
+
class GeoshapeChartStyle(_GeoChartStyle):
|
|
2077
|
+
"""Geoshape (choropleth) chart style.
|
|
2078
|
+
|
|
2079
|
+
``color_scheme`` is choropleth-specific; only this leaf, not _GeoChartStyle base.
|
|
2080
|
+
"""
|
|
2081
|
+
|
|
2082
|
+
color_scheme: str = Field(description="Color scheme name for choropleth gradient.")
|
|
2083
|
+
marks: GeoshapeChartMarksStyle = Field(
|
|
2084
|
+
default_factory=GeoshapeChartMarksStyle,
|
|
2085
|
+
description="Geoshape-family mark overrides.",
|
|
2086
|
+
)
|
|
2087
|
+
|
|
2088
|
+
|
|
2089
|
+
class PointMapChartStyle(_GeoChartStyle):
|
|
2090
|
+
"""Point map chart style."""
|
|
2091
|
+
|
|
2092
|
+
marks: PointMapChartMarksStyle = Field(
|
|
2093
|
+
default_factory=PointMapChartMarksStyle,
|
|
2094
|
+
description="Point-map-family mark overrides.",
|
|
2095
|
+
)
|
|
2096
|
+
|
|
2097
|
+
|
|
2098
|
+
class BoxplotChartStyle(_CartesianChartStyle):
|
|
2099
|
+
"""Boxplot chart style."""
|
|
2100
|
+
|
|
2101
|
+
marks: BoxplotChartMarksStyle = Field(
|
|
2102
|
+
default_factory=BoxplotChartMarksStyle,
|
|
2103
|
+
description="Boxplot-family mark overrides.",
|
|
2104
|
+
)
|
|
2105
|
+
|
|
2106
|
+
|
|
2107
|
+
class ErrorbarChartStyle(_CartesianChartStyle):
|
|
2108
|
+
"""Error bar chart style."""
|
|
2109
|
+
|
|
2110
|
+
marks: ErrorbarChartMarksStyle = Field(
|
|
2111
|
+
default_factory=ErrorbarChartMarksStyle,
|
|
2112
|
+
description="Errorbar-family mark overrides.",
|
|
2113
|
+
)
|
|
2114
|
+
|
|
2115
|
+
|
|
2116
|
+
class ErrorbandChartStyle(_CartesianChartStyle):
|
|
2117
|
+
"""Error band chart style."""
|
|
2118
|
+
|
|
2119
|
+
marks: ErrorbandChartMarksStyle = Field(
|
|
2120
|
+
default_factory=ErrorbandChartMarksStyle,
|
|
2121
|
+
description="Errorband-family mark overrides.",
|
|
2122
|
+
)
|
|
2123
|
+
|
|
2124
|
+
|
|
2125
|
+
class ChartsStyle(_ChartStyleBase):
|
|
2126
|
+
"""Registry of all chart-type styles plus shared chart configuration.
|
|
2127
|
+
|
|
2128
|
+
Inherits ``_ChartStyleBase`` — geometry/frame fields (aspect_ratio, min_height,
|
|
2129
|
+
max_height, padding, border, animation_duration, font) are populated by theme
|
|
2130
|
+
YAML at the global ``charts:`` level and serve as the cascade source for
|
|
2131
|
+
per-family defaults. ``height`` and ``width`` are NOT in the style cascade —
|
|
2132
|
+
they live only at chart root in the authored YAML.
|
|
2133
|
+
|
|
2134
|
+
Per-family styles inherit ``_ChartStyleBaseAllOptional`` (all-optional variant);
|
|
2135
|
+
the cascade fills their ``None`` sentinels from the values here.
|
|
2136
|
+
|
|
2137
|
+
The ``marks:`` block holds global mark defaults (tier-1 of the three-tier mark
|
|
2138
|
+
cascade). ADR-015: legacy top-level VL mark fields (circle, square, label, tick,
|
|
2139
|
+
rule, trail, rect, geoshape, image, boxplot, errorbar, errorband, map) are removed.
|
|
2140
|
+
Use ``marks.circle``, ``marks.text``, etc. instead.
|
|
2141
|
+
"""
|
|
2142
|
+
|
|
2143
|
+
model_config = ConfigDict(extra="forbid")
|
|
2144
|
+
|
|
2145
|
+
@model_validator(mode="before")
|
|
2146
|
+
@classmethod
|
|
2147
|
+
def _reject_legacy_mark_keys(cls, data: Any) -> Any:
|
|
2148
|
+
"""Reject keys renamed/moved in ADR-015 with actionable hints."""
|
|
2149
|
+
if not isinstance(data, dict):
|
|
2150
|
+
return data
|
|
2151
|
+
hints: list[str] = []
|
|
2152
|
+
if "label" in data:
|
|
2153
|
+
hints.append("'label' was renamed to 'text' under marks: → marks.text.*")
|
|
2154
|
+
if "map" in data:
|
|
2155
|
+
hints.append("'map' was renamed to 'geoshape' (the chart family)")
|
|
2156
|
+
if hints:
|
|
2157
|
+
raise ValueError(
|
|
2158
|
+
"ChartsStyle: renamed/moved keys detected. " + "; ".join(hints)
|
|
2159
|
+
)
|
|
2160
|
+
return data
|
|
2161
|
+
|
|
2162
|
+
# Axis sections
|
|
2163
|
+
axis: AxisStyle = Field(
|
|
2164
|
+
description="Shared axis style applied to all axes before per-axis overrides."
|
|
2165
|
+
)
|
|
2166
|
+
axis_x: AxisStyle = Field(
|
|
2167
|
+
description="X-axis style overrides applied after the shared axis."
|
|
2168
|
+
)
|
|
2169
|
+
axis_y: AxisStyle = Field(
|
|
2170
|
+
description="Y-axis style overrides applied after the shared axis."
|
|
2171
|
+
)
|
|
2172
|
+
axis_quantitative: AxisStyle = Field(
|
|
2173
|
+
description="Quantitative axis style overrides applied after axis_x/axis_y."
|
|
2174
|
+
)
|
|
2175
|
+
|
|
2176
|
+
# View / autosize
|
|
2177
|
+
view: ViewStyle = Field(description="Vega-Lite view dimensions and border.")
|
|
2178
|
+
autosize: AutosizeStyle = Field(description="Vega-Lite autosize configuration.")
|
|
2179
|
+
|
|
2180
|
+
# Global mark defaults — tier-1 of the three-tier mark cascade
|
|
2181
|
+
marks: GlobalMarksStyle = Field(
|
|
2182
|
+
default_factory=GlobalMarksStyle,
|
|
2183
|
+
description="Global mark defaults (tier-1 of the marks cascade).",
|
|
2184
|
+
)
|
|
2185
|
+
|
|
2186
|
+
# Primary chart families
|
|
2187
|
+
bar: BarChartStyle = Field(description="Bar chart style.")
|
|
2188
|
+
line: LineChartStyle = Field(description="Line chart style.")
|
|
2189
|
+
area: AreaChartStyle = Field(description="Area chart style.")
|
|
2190
|
+
scatter: ScatterChartStyle = Field(description="Scatter chart style.")
|
|
2191
|
+
histogram: HistogramChartStyle = Field(description="Histogram chart style.")
|
|
2192
|
+
heatmap: HeatmapChartStyle = Field(description="Heatmap chart style.")
|
|
2193
|
+
geoshape: GeoshapeChartStyle = Field(
|
|
2194
|
+
description="Geoshape (choropleth) chart style."
|
|
2195
|
+
)
|
|
2196
|
+
point_map: PointMapChartStyle = Field(description="Point map chart style.")
|
|
2197
|
+
boxplot: BoxplotChartStyle = Field(description="Boxplot chart style.")
|
|
2198
|
+
errorbar: ErrorbarChartStyle = Field(description="Error bar chart style.")
|
|
2199
|
+
errorband: ErrorbandChartStyle = Field(description="Error band chart style.")
|
|
2200
|
+
pie: PieChartStyle = Field(description="Pie/donut chart style.")
|
|
2201
|
+
|
|
2202
|
+
# Series-label primitive — typography for series-naming text marks
|
|
2203
|
+
# (endpoint labels, direct stack labels, slice callouts).
|
|
2204
|
+
series_label: SeriesLabelStyle = Field(
|
|
2205
|
+
description="Shared series-label typography for endpoint labels, stack labels, and slice callouts."
|
|
2206
|
+
)
|
|
2207
|
+
|
|
2208
|
+
# Non-Vega chart types (required — theme pipeline always populates)
|
|
2209
|
+
kpi: KpiChartStyle = Field(description="KPI card chart style.")
|
|
2210
|
+
table: TableChartStyle = Field(description="Table chart style.")
|
|
2211
|
+
spark_bar: SparkBarChartStyle = Field(
|
|
2212
|
+
description="Spark_bar (full-chart horizontal bar) style."
|
|
2213
|
+
)
|
|
2214
|
+
|
|
2215
|
+
# Attached data_table primitive (per-chart strip; populated by the theme cascade).
|
|
2216
|
+
data_table: DataTableStyle = Field(description="Attached data_table strip style.")
|
|
2217
|
+
|
|
2218
|
+
# Callout chart-family style (type: callout and runtime chart-error fallback)
|
|
2219
|
+
callout: CalloutChartStyle = Field(
|
|
2220
|
+
description="Callout chart-family style for type:callout and runtime fallback cards."
|
|
2221
|
+
)
|
|
2222
|
+
|
|
2223
|
+
|
|
2224
|
+
# KPI
|
|
2225
|
+
|
|
2226
|
+
|
|
2227
|
+
class SubtitleStyle(BaseModel):
|
|
2228
|
+
"""Subtitle font style for chart subtitles (Table, SparkBar).
|
|
2229
|
+
|
|
2230
|
+
Theme YAML populates font.size directly — no delta/floor math.
|
|
2231
|
+
Values live in stark.yaml style.charts.*.subtitle.
|
|
2232
|
+
"""
|
|
2233
|
+
|
|
2234
|
+
model_config = ConfigDict(extra="forbid")
|
|
2235
|
+
|
|
2236
|
+
# FontStyle is all-Optional (it's a patch type); default_factory=FontStyle
|
|
2237
|
+
# creates an empty overlay that the theme YAML fills with font.size.
|
|
2238
|
+
# Same justified exception as KpiValueStyle.font.
|
|
2239
|
+
font: FontStyle = Field(
|
|
2240
|
+
default_factory=FontStyle,
|
|
2241
|
+
description="Subtitle font style; theme populates font.size directly.",
|
|
2242
|
+
)
|
|
2243
|
+
|
|
2244
|
+
|
|
2245
|
+
class KpiValueStyle(BaseModel):
|
|
2246
|
+
"""KPI headline value slot: font. Theme populates font.size directly."""
|
|
2247
|
+
|
|
2248
|
+
model_config = ConfigDict(extra="forbid")
|
|
2249
|
+
|
|
2250
|
+
font: FontStyle = Field(
|
|
2251
|
+
default_factory=FontStyle,
|
|
2252
|
+
description="KPI value font style; cascades from kpi.font.",
|
|
2253
|
+
)
|
|
2254
|
+
|
|
2255
|
+
|
|
2256
|
+
class KpiSlotStyle(BaseModel):
|
|
2257
|
+
"""KPI per-slot style for label, affix, and glyph slots."""
|
|
2258
|
+
|
|
2259
|
+
model_config = ConfigDict(extra="forbid")
|
|
2260
|
+
|
|
2261
|
+
font: FontStyle = Field(
|
|
2262
|
+
default_factory=FontStyle,
|
|
2263
|
+
description="Slot font style; cascades from kpi.font.",
|
|
2264
|
+
)
|
|
2265
|
+
# Authored glyph character (e.g. '▲', '▼', '●'). None = no glyph rendered.
|
|
2266
|
+
# Only meaningful on the `glyph` slot — ignored on label/affix slots.
|
|
2267
|
+
character: str | None = Field(
|
|
2268
|
+
default=None,
|
|
2269
|
+
description="Glyph character to render (e.g. '▲'). None = no glyph.",
|
|
2270
|
+
)
|
|
2271
|
+
|
|
2272
|
+
|
|
2273
|
+
class KpiTonesStyle(BaseModel):
|
|
2274
|
+
"""Semantic tone palette for KPI value/glyph and (eventually) tables.
|
|
2275
|
+
|
|
2276
|
+
Colors are theme tokens — the renderer reads them through this model
|
|
2277
|
+
rather than hardcoding hexes, so themes can rebrand the semantic
|
|
2278
|
+
vocabulary without touching the renderer.
|
|
2279
|
+
|
|
2280
|
+
v1: three tones only — positive, negative, warning.
|
|
2281
|
+
KPIs without tone use default chrome colors.
|
|
2282
|
+
"""
|
|
2283
|
+
|
|
2284
|
+
model_config = ConfigDict(extra="forbid")
|
|
2285
|
+
|
|
2286
|
+
positive: str = Field(description="Color for positive/good tone indicators.")
|
|
2287
|
+
negative: str = Field(description="Color for negative/bad tone indicators.")
|
|
2288
|
+
warning: str = Field(description="Color for warning/caution tone indicators.")
|
|
2289
|
+
|
|
2290
|
+
|
|
2291
|
+
class KpiChartStyle(_ChartStyleBaseAllOptional):
|
|
2292
|
+
"""Produced by cascade from theme YAML.
|
|
2293
|
+
|
|
2294
|
+
Four per-slot font blocks (value, label, affix, glyph) plus a parent
|
|
2295
|
+
``font`` that cascades into every slot and doubles as the support-row
|
|
2296
|
+
font. Concrete pixel values live in ``stark.yaml``.
|
|
2297
|
+
|
|
2298
|
+
* ``font`` — parent cascade base; also the support row (small text under value)
|
|
2299
|
+
* ``value.font`` — headline number typography; size/weight from theme (authoritative)
|
|
2300
|
+
* ``label.font`` — card label typography; cascades from ``kpi.font``
|
|
2301
|
+
* ``affix.font`` — currency/percent affixes; cascades from ``kpi.font``
|
|
2302
|
+
* ``glyph.font`` — indicator glyphs (▲▼●); cascades from ``kpi.font``
|
|
2303
|
+
* ``content_padding`` — per-side inner inset for the value/label/support text block
|
|
2304
|
+
"""
|
|
2305
|
+
|
|
2306
|
+
model_config = ConfigDict(extra="forbid")
|
|
2307
|
+
|
|
2308
|
+
value: KpiValueStyle = Field(description="KPI headline value slot.")
|
|
2309
|
+
label: KpiSlotStyle = Field(description="KPI card label slot.")
|
|
2310
|
+
affix: KpiSlotStyle = Field(description="Currency/percent affix slot.")
|
|
2311
|
+
glyph: KpiSlotStyle = Field(
|
|
2312
|
+
# Indicator glyphs (▲▼●) sit next to the value as markers, not second
|
|
2313
|
+
# headlines — drawn at body/narrow tier so a triangle's visual mass
|
|
2314
|
+
# doesn't compete with the digits.
|
|
2315
|
+
description="Indicator glyph (▲▼●) slot.",
|
|
2316
|
+
)
|
|
2317
|
+
min_card_width: float = Field(description="Minimum KPI card width in pixels.")
|
|
2318
|
+
default_width: float = Field(description="Default KPI card width in pixels.")
|
|
2319
|
+
default_height: float = Field(description="Default KPI card height in pixels.")
|
|
2320
|
+
border: BorderStyle = Field(description="KPI card border style.")
|
|
2321
|
+
content_padding: SpacingValues = Field(
|
|
2322
|
+
description="Inner inset (top/right/bottom/left) for KPI card content in pixels."
|
|
2323
|
+
)
|
|
2324
|
+
tones: KpiTonesStyle = Field(
|
|
2325
|
+
description="Semantic tone color palette for KPI values and glyphs."
|
|
2326
|
+
)
|
|
2327
|
+
# Semantic tone selector (positive/negative/warning). None = no tone applied.
|
|
2328
|
+
# Moved here from chart root (ADR-001: paint → style:).
|
|
2329
|
+
tone: str | None = Field(
|
|
2330
|
+
default=None,
|
|
2331
|
+
description="Semantic tone selector (positive | negative | warning). None = default chrome.",
|
|
2332
|
+
)
|
|
2333
|
+
|
|
2334
|
+
|
|
2335
|
+
# Table
|
|
2336
|
+
|
|
2337
|
+
|
|
2338
|
+
class TableColumnsStyle(BaseModel):
|
|
2339
|
+
model_config = ConfigDict(extra="forbid")
|
|
2340
|
+
|
|
2341
|
+
default_width: float = Field(description="Default column width in pixels.")
|
|
2342
|
+
cell_padding: float = Field(
|
|
2343
|
+
description="Horizontal padding inside table cells in pixels."
|
|
2344
|
+
)
|
|
2345
|
+
width_similarity_threshold: float = Field(
|
|
2346
|
+
ge=0.0,
|
|
2347
|
+
le=1.0,
|
|
2348
|
+
description=(
|
|
2349
|
+
"Auto-width columns whose min/max ratio >= this threshold are snapped "
|
|
2350
|
+
"to a shared width before budget allocation. 1.0 disables clustering; "
|
|
2351
|
+
"0.0 forces all auto-columns to equal width."
|
|
2352
|
+
),
|
|
2353
|
+
)
|
|
2354
|
+
|
|
2355
|
+
|
|
2356
|
+
class TableHeaderStyle(BaseModel):
|
|
2357
|
+
model_config = ConfigDict(extra="forbid")
|
|
2358
|
+
|
|
2359
|
+
visible: bool = Field(
|
|
2360
|
+
description=(
|
|
2361
|
+
"Show the header row (column labels + rule). Set to False on "
|
|
2362
|
+
"series-keyed tables where column meanings are obvious from context "
|
|
2363
|
+
"(e.g. donut-attached tables: swatch / share / name / value). "
|
|
2364
|
+
"Theme YAML supplies the UX default (true) via _base.yaml."
|
|
2365
|
+
),
|
|
2366
|
+
)
|
|
2367
|
+
height: float = Field(description="Header row height in pixels.")
|
|
2368
|
+
font: FontStyle = Field(
|
|
2369
|
+
default_factory=FontStyle, description="Header font style overrides."
|
|
2370
|
+
)
|
|
2371
|
+
font_compact: FontStyle | None = Field(
|
|
2372
|
+
default=None,
|
|
2373
|
+
description="Compact-tier font overrides applied when body size ≤11px; None means no compact override.",
|
|
2374
|
+
) # None = no compact-tier override; header uses font at all body sizes
|
|
2375
|
+
# None = no header fill. Header rule alone separates header from body.
|
|
2376
|
+
# Themes may set a fill (cream, dark) when paper-warmth or
|
|
2377
|
+
# contrast demands it.
|
|
2378
|
+
background: str | None = Field(
|
|
2379
|
+
default=None,
|
|
2380
|
+
description="Header background color; None means no fill (rule alone separates header from body).",
|
|
2381
|
+
)
|
|
2382
|
+
overflow: str = Field(
|
|
2383
|
+
description="Header text overflow mode (clip, truncate, wrap)."
|
|
2384
|
+
)
|
|
2385
|
+
rule: RuleStyle = Field(description="Header bottom rule style.")
|
|
2386
|
+
|
|
2387
|
+
|
|
2388
|
+
class TableRowRoleStyle(BaseModel):
|
|
2389
|
+
model_config = ConfigDict(extra="forbid")
|
|
2390
|
+
|
|
2391
|
+
rule_width: float = Field(
|
|
2392
|
+
description="Rule width above rows with this role in pixels."
|
|
2393
|
+
)
|
|
2394
|
+
font: FontStyle | None = Field(
|
|
2395
|
+
default=None,
|
|
2396
|
+
description="Per-role font style override; None uses the default row font.",
|
|
2397
|
+
)
|
|
2398
|
+
background: str | None = Field(
|
|
2399
|
+
default=None,
|
|
2400
|
+
description="Per-role row background color; None means no override.",
|
|
2401
|
+
)
|
|
2402
|
+
|
|
2403
|
+
|
|
2404
|
+
class TableRowRolesStyle(BaseModel):
|
|
2405
|
+
model_config = ConfigDict(extra="forbid")
|
|
2406
|
+
|
|
2407
|
+
summary: TableRowRoleStyle = Field(description="Style for summary role rows.")
|
|
2408
|
+
total: TableRowRoleStyle = Field(description="Style for total role rows.")
|
|
2409
|
+
|
|
2410
|
+
|
|
2411
|
+
class TableRowStripeStyle(BaseModel):
|
|
2412
|
+
"""Alternating row stripe style."""
|
|
2413
|
+
|
|
2414
|
+
model_config = ConfigDict(extra="forbid")
|
|
2415
|
+
|
|
2416
|
+
color: str | None = Field( # None = no alternating row fill
|
|
2417
|
+
default=None,
|
|
2418
|
+
description="Alternating stripe background color; None means no alternating row fill.",
|
|
2419
|
+
)
|
|
2420
|
+
|
|
2421
|
+
|
|
2422
|
+
class TableRowStyle(BaseModel):
|
|
2423
|
+
model_config = ConfigDict(extra="forbid")
|
|
2424
|
+
|
|
2425
|
+
height: float = Field(description="Body row height in pixels.")
|
|
2426
|
+
stripe: TableRowStripeStyle | None = Field( # None = no alternating row fill
|
|
2427
|
+
default=None,
|
|
2428
|
+
description="Alternating row stripe style; None means no stripe.",
|
|
2429
|
+
)
|
|
2430
|
+
rule: RuleStyle = Field(description="Row bottom rule style.")
|
|
2431
|
+
role: str | None = Field(
|
|
2432
|
+
default=None,
|
|
2433
|
+
description="Default row role assignment; None means plain body row.",
|
|
2434
|
+
)
|
|
2435
|
+
roles: TableRowRolesStyle = Field(
|
|
2436
|
+
description="Per-role style overrides for summary and total rows."
|
|
2437
|
+
)
|
|
2438
|
+
|
|
2439
|
+
|
|
2440
|
+
class TableTitleStyle(BaseModel):
|
|
2441
|
+
model_config = ConfigDict(extra="forbid")
|
|
2442
|
+
|
|
2443
|
+
height: float = Field(description="Table title row height in pixels.")
|
|
2444
|
+
font: FontStyle = Field(
|
|
2445
|
+
default_factory=FontStyle, description="Table title font style overrides."
|
|
2446
|
+
)
|
|
2447
|
+
|
|
2448
|
+
|
|
2449
|
+
class TableEdgeStyle(BaseModel):
|
|
2450
|
+
"""more_rows or empty_state edge-case UI."""
|
|
2451
|
+
|
|
2452
|
+
model_config = ConfigDict(extra="forbid")
|
|
2453
|
+
|
|
2454
|
+
font: FontStyle = Field(
|
|
2455
|
+
default_factory=FontStyle,
|
|
2456
|
+
description="Edge-case UI (more_rows / empty_state) font style overrides.",
|
|
2457
|
+
)
|
|
2458
|
+
|
|
2459
|
+
|
|
2460
|
+
class SparkEmptyStyle(BaseModel):
|
|
2461
|
+
model_config = ConfigDict(extra="forbid")
|
|
2462
|
+
inset_x: float = Field(
|
|
2463
|
+
description="Horizontal inset for the empty sparkline placeholder in pixels."
|
|
2464
|
+
)
|
|
2465
|
+
stroke: StrokeStyle = Field(description="Empty-state placeholder stroke style.")
|
|
2466
|
+
|
|
2467
|
+
@model_validator(mode="after")
|
|
2468
|
+
def _require_stroke_fields(self) -> SparkEmptyStyle:
|
|
2469
|
+
s = self.stroke
|
|
2470
|
+
missing = [
|
|
2471
|
+
f
|
|
2472
|
+
for f, v in [
|
|
2473
|
+
("color", s.color),
|
|
2474
|
+
("width", s.width),
|
|
2475
|
+
("dasharray", s.dasharray),
|
|
2476
|
+
]
|
|
2477
|
+
if v is None
|
|
2478
|
+
]
|
|
2479
|
+
if missing:
|
|
2480
|
+
raise ValueError(
|
|
2481
|
+
f"spark.empty.stroke.{missing[0]} is required (renderer reads color/width/dasharray)"
|
|
2482
|
+
)
|
|
2483
|
+
return self
|
|
2484
|
+
|
|
2485
|
+
|
|
2486
|
+
class SparkSingleValueStyle(BaseModel):
|
|
2487
|
+
model_config = ConfigDict(extra="forbid")
|
|
2488
|
+
inset_x: float = Field(
|
|
2489
|
+
description="Horizontal inset for the single-value sparkline in pixels."
|
|
2490
|
+
)
|
|
2491
|
+
marker_radius: float = Field(
|
|
2492
|
+
description="Radius of the single-value marker circle in pixels."
|
|
2493
|
+
)
|
|
2494
|
+
|
|
2495
|
+
|
|
2496
|
+
class SparkColumnsStyle(BaseModel):
|
|
2497
|
+
"""Inline `spark.type: columns` (multi-value vertical bars) defaults."""
|
|
2498
|
+
|
|
2499
|
+
model_config = ConfigDict(extra="forbid")
|
|
2500
|
+
gap: float = Field(description="Gap between column bars in pixels.")
|
|
2501
|
+
padding: float = Field(
|
|
2502
|
+
description="Horizontal outer padding of the columns sparkline in pixels."
|
|
2503
|
+
)
|
|
2504
|
+
min_bar_height: float = Field(description="Minimum rendered bar height in pixels.")
|
|
2505
|
+
border: BorderStyle = Field(description="Column bar border style.")
|
|
2506
|
+
|
|
2507
|
+
|
|
2508
|
+
class SparkBarLabelStyle(BaseModel):
|
|
2509
|
+
model_config = ConfigDict(extra="forbid")
|
|
2510
|
+
inset_x: float = Field(
|
|
2511
|
+
description="Horizontal inset for the spark bar label in pixels."
|
|
2512
|
+
)
|
|
2513
|
+
fill: str = Field(description="Label text fill color.")
|
|
2514
|
+
fill_opacity: float = Field(description="Label text fill opacity (0–1).")
|
|
2515
|
+
min_size: float = Field(
|
|
2516
|
+
description="Minimum bar fill width required to show the label in pixels."
|
|
2517
|
+
)
|
|
2518
|
+
height_offset: float = Field(
|
|
2519
|
+
description="Vertical offset of the label from its bar top in pixels."
|
|
2520
|
+
)
|
|
2521
|
+
|
|
2522
|
+
|
|
2523
|
+
class SparkBarCellStyle(BaseModel):
|
|
2524
|
+
"""Inline `spark.type: bar` and `bar-normalize` (single horizontal bar) defaults.
|
|
2525
|
+
|
|
2526
|
+
The variant decides whether the background track is drawn:
|
|
2527
|
+
`bar` paints only the fill; `bar-normalize` paints a track + fill.
|
|
2528
|
+
Style sub-keys are shared between both variants.
|
|
2529
|
+
"""
|
|
2530
|
+
|
|
2531
|
+
model_config = ConfigDict(extra="forbid")
|
|
2532
|
+
background: str | None = Field(
|
|
2533
|
+
default=None,
|
|
2534
|
+
description="Track background color; cascade-managed (inherits from style.muted).",
|
|
2535
|
+
)
|
|
2536
|
+
color: str | None = Field(
|
|
2537
|
+
default=None,
|
|
2538
|
+
description="Bar fill color; cascade-managed (inherits from style.accent).",
|
|
2539
|
+
)
|
|
2540
|
+
default_max: float = Field(
|
|
2541
|
+
description="Default maximum value for bar scale when no explicit max is authored."
|
|
2542
|
+
)
|
|
2543
|
+
border: BorderStyle = Field(description="Spark bar border style.")
|
|
2544
|
+
font: FontStyle = Field(
|
|
2545
|
+
default_factory=FontStyle,
|
|
2546
|
+
description="Spark bar cell font style overrides.",
|
|
2547
|
+
)
|
|
2548
|
+
label: SparkBarLabelStyle = Field(description="Spark bar inline label style.")
|
|
2549
|
+
|
|
2550
|
+
|
|
2551
|
+
class SparkAreaStyle(BaseModel):
|
|
2552
|
+
model_config = ConfigDict(extra="forbid")
|
|
2553
|
+
fill_opacity: float = Field(description="Spark area fill opacity (0–1).")
|
|
2554
|
+
|
|
2555
|
+
|
|
2556
|
+
class SparkStyle(BaseModel):
|
|
2557
|
+
"""Inline sparkline defaults (inside table cells)."""
|
|
2558
|
+
|
|
2559
|
+
model_config = ConfigDict(extra="forbid")
|
|
2560
|
+
|
|
2561
|
+
color: str | None = Field(
|
|
2562
|
+
default=None,
|
|
2563
|
+
description="Sparkline line/point color; cascade-managed (inherits from style.accent).",
|
|
2564
|
+
)
|
|
2565
|
+
padding: SpacingValues = Field(
|
|
2566
|
+
description="Cell padding around the sparkline in pixels."
|
|
2567
|
+
)
|
|
2568
|
+
empty: SparkEmptyStyle = Field(description="Style for empty/no-data sparklines.")
|
|
2569
|
+
single_value: SparkSingleValueStyle = Field(
|
|
2570
|
+
description="Style for single-data-point sparklines."
|
|
2571
|
+
)
|
|
2572
|
+
columns: SparkColumnsStyle = Field(description="Style for column-type sparklines.")
|
|
2573
|
+
bar: SparkBarCellStyle = Field(
|
|
2574
|
+
description="Style for bar/bar-normalize sparklines."
|
|
2575
|
+
)
|
|
2576
|
+
area: SparkAreaStyle = Field(description="Style for area sparklines.")
|
|
2577
|
+
|
|
2578
|
+
|
|
2579
|
+
class TableRowNumbersStyle(BaseModel):
|
|
2580
|
+
"""Leading row-number column (style.table.row_numbers).
|
|
2581
|
+
|
|
2582
|
+
When ``show=True`` the renderer injects a synthetic column at index 0
|
|
2583
|
+
that displays the absolute 1-based row index in the original unpaginated
|
|
2584
|
+
dataset. Pagination is continuous: page 2 of a 10-per-page table starts
|
|
2585
|
+
at 11. The column is transparent to ``style.columns`` (not listed), to
|
|
2586
|
+
conditional_formatting rules keyed by column name, and to data-pipeline
|
|
2587
|
+
transforms — it exists only in the chart layer.
|
|
2588
|
+
"""
|
|
2589
|
+
|
|
2590
|
+
model_config = ConfigDict(extra="forbid")
|
|
2591
|
+
|
|
2592
|
+
# Feature toggle — False is the opt-out default; not a visual theme value.
|
|
2593
|
+
visible: bool = Field(
|
|
2594
|
+
default=False, description="Show a leading row-number column; false by default."
|
|
2595
|
+
)
|
|
2596
|
+
# Fixed column header label — not a theme style value.
|
|
2597
|
+
header: str = Field(
|
|
2598
|
+
default="#", description="Header label for the row-number column."
|
|
2599
|
+
)
|
|
2600
|
+
# Fixed alignment for the row-number column — not a theme style value.
|
|
2601
|
+
align: Literal["left", "right"] = Field(
|
|
2602
|
+
default="right", description="Text alignment for the row-number column."
|
|
2603
|
+
)
|
|
2604
|
+
|
|
2605
|
+
|
|
2606
|
+
class PaginatorStyle(BaseModel):
|
|
2607
|
+
"""Visual style for the paginator control (chevrons + page numbers).
|
|
2608
|
+
|
|
2609
|
+
Distinct from PaginationConfig which controls *behaviour* (enabled,
|
|
2610
|
+
page_size). PaginatorStyle owns the look: colour ramp across the three
|
|
2611
|
+
item states (active, inactive, disabled), font sizing, weight contrast
|
|
2612
|
+
between the current page and its neighbours, and the slot width that
|
|
2613
|
+
drives the hit target.
|
|
2614
|
+
"""
|
|
2615
|
+
|
|
2616
|
+
model_config = ConfigDict(extra="forbid")
|
|
2617
|
+
|
|
2618
|
+
color_active: str = Field(
|
|
2619
|
+
description="Current page and live chevron colour (theme body ink)."
|
|
2620
|
+
)
|
|
2621
|
+
color_inactive: str = Field(
|
|
2622
|
+
description="Other pages and ellipsis colour (theme secondary text)."
|
|
2623
|
+
)
|
|
2624
|
+
color_disabled: str = Field(
|
|
2625
|
+
description="Chevron colour when at first/last page (signals disabled by tone)."
|
|
2626
|
+
)
|
|
2627
|
+
font: FontStyle = Field(
|
|
2628
|
+
default_factory=FontStyle,
|
|
2629
|
+
description="Paginator font overrides (size, family).",
|
|
2630
|
+
)
|
|
2631
|
+
weight_active: int = Field(
|
|
2632
|
+
description="Font weight for the current (selected) page number."
|
|
2633
|
+
)
|
|
2634
|
+
weight_inactive: int = Field(
|
|
2635
|
+
description="Font weight for other pages and ellipsis."
|
|
2636
|
+
)
|
|
2637
|
+
weight_chevron: int = Field(
|
|
2638
|
+
description=(
|
|
2639
|
+
"Font weight for the prev/next chevrons (live and disabled). "
|
|
2640
|
+
"Usually heavier than weight_active so the chevrons read as "
|
|
2641
|
+
"interactive affordances against the lighter page numbers."
|
|
2642
|
+
)
|
|
2643
|
+
)
|
|
2644
|
+
item_width: float = Field(
|
|
2645
|
+
description="Per-item slot width in pixels (drives layout step)."
|
|
2646
|
+
)
|
|
2647
|
+
|
|
2648
|
+
|
|
2649
|
+
class TableRuleStyle(BaseModel):
|
|
2650
|
+
"""Table rule color override block."""
|
|
2651
|
+
|
|
2652
|
+
model_config = ConfigDict(extra="forbid")
|
|
2653
|
+
|
|
2654
|
+
color: str | None = Field( # None = use theme default
|
|
2655
|
+
default=None, description="Table rule color; None uses the theme default."
|
|
2656
|
+
)
|
|
2657
|
+
|
|
2658
|
+
|
|
2659
|
+
class TableColumnDefaultsConfig(BaseModel):
|
|
2660
|
+
"""Table-level defaults applied to every column unless overridden per-column.
|
|
2661
|
+
|
|
2662
|
+
Only fields that are meaningful as uniform defaults are included.
|
|
2663
|
+
Data-dependent fields (link, header_link, spark, scale, glyph, glyph_color,
|
|
2664
|
+
header_overflow) are excluded — they depend on per-column data shape or
|
|
2665
|
+
interaction intent.
|
|
2666
|
+
"""
|
|
2667
|
+
|
|
2668
|
+
model_config = ConfigDict(extra="forbid")
|
|
2669
|
+
|
|
2670
|
+
label: str | None = Field(
|
|
2671
|
+
default=None,
|
|
2672
|
+
description="Override column header label text.",
|
|
2673
|
+
)
|
|
2674
|
+
width: int | str | None = Field(
|
|
2675
|
+
default=None,
|
|
2676
|
+
description="Override column width in pixels (integer) or a CSS width string.",
|
|
2677
|
+
)
|
|
2678
|
+
align: Literal["left", "center", "right"] | None = Field(
|
|
2679
|
+
default=None,
|
|
2680
|
+
description="Override cell text alignment (left, center, or right).",
|
|
2681
|
+
)
|
|
2682
|
+
format: str | FormatConfig | None = Field(
|
|
2683
|
+
default=None,
|
|
2684
|
+
description="Override cell value format (D3 format string or format config object).",
|
|
2685
|
+
)
|
|
2686
|
+
background: str | None = Field(
|
|
2687
|
+
default=None, description="Override cell background color (CSS color string)."
|
|
2688
|
+
)
|
|
2689
|
+
font: FontStyle | None = Field(
|
|
2690
|
+
default=None,
|
|
2691
|
+
description="Override cell font style (size, weight, color, family).",
|
|
2692
|
+
)
|
|
2693
|
+
|
|
2694
|
+
|
|
2695
|
+
class TableChartStyle(_ChartStyleBaseAllOptional):
|
|
2696
|
+
model_config = ConfigDict(extra="forbid")
|
|
2697
|
+
|
|
2698
|
+
# Color overrides (None = inherit from theme)
|
|
2699
|
+
background: str | None = Field(
|
|
2700
|
+
default=None, description="Table background color; None inherits from theme."
|
|
2701
|
+
)
|
|
2702
|
+
# str → text color; StyleColorConfig → gradient scale for the color channel.
|
|
2703
|
+
color: str | StyleColorConfig | None = Field(
|
|
2704
|
+
default=None,
|
|
2705
|
+
description="Table text color override (CSS string) or gradient scale config; None uses the theme default.",
|
|
2706
|
+
)
|
|
2707
|
+
rule: TableRuleStyle | None = Field( # None = no rule color override
|
|
2708
|
+
default=None,
|
|
2709
|
+
description="Table rule style overrides (color). None = no override; inherits from theme.",
|
|
2710
|
+
)
|
|
2711
|
+
|
|
2712
|
+
outer_padding: float = Field(description="Outer table padding in pixels.")
|
|
2713
|
+
bottom_padding: float = Field(
|
|
2714
|
+
description="Extra bottom padding below the last row in pixels."
|
|
2715
|
+
)
|
|
2716
|
+
# Layout/structural column settings (width budget, cell padding, clustering).
|
|
2717
|
+
# Renamed from 'columns' to avoid collision with the per-column display config.
|
|
2718
|
+
column_layout: TableColumnsStyle = Field(
|
|
2719
|
+
description="Default column width and cell padding settings."
|
|
2720
|
+
)
|
|
2721
|
+
header: TableHeaderStyle = Field(description="Table header row style.")
|
|
2722
|
+
row: TableRowStyle = Field(description="Table body row style.")
|
|
2723
|
+
row_numbers: TableRowNumbersStyle = Field(
|
|
2724
|
+
default_factory=TableRowNumbersStyle,
|
|
2725
|
+
description="Leading row-number column configuration.",
|
|
2726
|
+
)
|
|
2727
|
+
title_row: TableTitleStyle = Field(description="Table title row style.")
|
|
2728
|
+
border: BorderStyle = Field(description="Table outer border style.")
|
|
2729
|
+
wrap: bool = Field(
|
|
2730
|
+
description="Allow cell text wrapping; false clips to single line."
|
|
2731
|
+
)
|
|
2732
|
+
pagination: PaginationConfig = Field(
|
|
2733
|
+
description="Client-side pagination defaults for table charts."
|
|
2734
|
+
)
|
|
2735
|
+
# Cascade-managed sentinels — None means "not overridden at this tier".
|
|
2736
|
+
column_defaults: TableColumnDefaultsConfig | None = Field(
|
|
2737
|
+
default=None,
|
|
2738
|
+
description="Table-level column defaults applied to every column; None means no defaults authored.",
|
|
2739
|
+
)
|
|
2740
|
+
columns: dict[str, Any] | None = Field(
|
|
2741
|
+
default=None,
|
|
2742
|
+
description="Per-column display configuration keyed by column name (label, format, width, etc.).",
|
|
2743
|
+
)
|
|
2744
|
+
header_overflow: Literal["clip", "truncate", "wrap-two", "wrap"] | None = Field(
|
|
2745
|
+
default=None,
|
|
2746
|
+
description="Table column header text overflow mode; None inherits from theme.",
|
|
2747
|
+
)
|
|
2748
|
+
paginator: PaginatorStyle = Field(
|
|
2749
|
+
description="Visual style for the paginator control (chevrons + page numbers)."
|
|
2750
|
+
)
|
|
2751
|
+
symbol_mode: str = Field(
|
|
2752
|
+
description=(
|
|
2753
|
+
"Where to show currency-prefix and magnitude/unit suffix symbols in a "
|
|
2754
|
+
"numeric column. 'all' shows the full formatted value on every row; "
|
|
2755
|
+
"'anchors' shows it only on the first data row and summary/total rows, "
|
|
2756
|
+
"stripping prefix and suffix from plain middle rows so the anchors "
|
|
2757
|
+
"guide the reader at the top and bottom of the value field."
|
|
2758
|
+
)
|
|
2759
|
+
)
|
|
2760
|
+
more_rows: TableEdgeStyle = Field(
|
|
2761
|
+
description="Style for the 'more rows' edge-case indicator."
|
|
2762
|
+
)
|
|
2763
|
+
empty_state: TableEdgeStyle = Field(
|
|
2764
|
+
description="Style for the empty-state (no data) indicator."
|
|
2765
|
+
)
|
|
2766
|
+
spark: SparkStyle = Field(description="Inline sparkline defaults for table cells.")
|
|
2767
|
+
|
|
2768
|
+
# SVG layout constants (all-config per ADR-002)
|
|
2769
|
+
text_baseline_offset: float = Field(
|
|
2770
|
+
description="Vertical offset to align SVG text baseline with cell grid in pixels."
|
|
2771
|
+
)
|
|
2772
|
+
title_baseline_offset: float = Field(
|
|
2773
|
+
description="Vertical offset to align SVG title baseline in pixels."
|
|
2774
|
+
)
|
|
2775
|
+
title_subtitle_gap: float = Field(
|
|
2776
|
+
description="Gap between table title and subtitle lines in pixels."
|
|
2777
|
+
)
|
|
2778
|
+
subtitle: SubtitleStyle = Field(description="Table subtitle font-sizing constants.")
|
|
2779
|
+
|
|
2780
|
+
|
|
2781
|
+
# ── data_table (attached chart primitive) ───────────────────────────────────
|
|
2782
|
+
# ADR-006: no column-header row; `divider:` styles the strip-top rule only.
|
|
2783
|
+
# Styleable leaves (spec §4.3): font, divider, row, label, number_align,
|
|
2784
|
+
# padding_top, padding_bottom. Defaults are conservative — themes tighten.
|
|
2785
|
+
|
|
2786
|
+
|
|
2787
|
+
class DataTableRowPaddingStyle(BaseModel):
|
|
2788
|
+
model_config = ConfigDict(extra="forbid")
|
|
2789
|
+
|
|
2790
|
+
vertical: float = Field(
|
|
2791
|
+
description="Vertical (top/bottom) padding inside data_table rows in pixels."
|
|
2792
|
+
)
|
|
2793
|
+
horizontal: float = Field(
|
|
2794
|
+
description="Horizontal (left/right) padding inside data_table rows in pixels."
|
|
2795
|
+
)
|
|
2796
|
+
|
|
2797
|
+
|
|
2798
|
+
class DataTableRowStyle(BaseModel):
|
|
2799
|
+
model_config = ConfigDict(extra="forbid")
|
|
2800
|
+
|
|
2801
|
+
padding: DataTableRowPaddingStyle = Field(description="Row padding style.")
|
|
2802
|
+
rule: RuleStyle = Field(description="Row bottom rule style.")
|
|
2803
|
+
|
|
2804
|
+
|
|
2805
|
+
class DataTableLabelStyle(BaseModel):
|
|
2806
|
+
"""Row label styling.
|
|
2807
|
+
|
|
2808
|
+
The row label's side (left-gutter or right-gutter) is a pure function of
|
|
2809
|
+
the chart's resolved ``axis_y.orient`` — there is no authorable
|
|
2810
|
+
``position`` or ``align`` override. The compiler derives both from the
|
|
2811
|
+
axis orientation at render time:
|
|
2812
|
+
|
|
2813
|
+
- ``axis_y.orient == "right"`` → label at ``spec_width + axis_y.label.padding``,
|
|
2814
|
+
``mark.align = "left"`` (text-anchor start, extends rightward).
|
|
2815
|
+
- ``axis_y.orient == "left"`` → label at ``-axis_y.label.padding``,
|
|
2816
|
+
``mark.align = "right"`` (text-anchor end, extends leftward).
|
|
2817
|
+
|
|
2818
|
+
``extra="forbid"`` rejects any YAML that still authors the deleted
|
|
2819
|
+
``position`` or ``align`` fields with a ``ValidationError`` at compile time.
|
|
2820
|
+
"""
|
|
2821
|
+
|
|
2822
|
+
model_config = ConfigDict(extra="forbid")
|
|
2823
|
+
|
|
2824
|
+
font: FontStyle = Field(
|
|
2825
|
+
default_factory=FontStyle, description="Row label font style overrides."
|
|
2826
|
+
)
|
|
2827
|
+
|
|
2828
|
+
|
|
2829
|
+
class DataTableStyle(BaseModel):
|
|
2830
|
+
"""Attached data_table style. Lives at style.charts.data_table.*.
|
|
2831
|
+
|
|
2832
|
+
Also nestable under per-chart-type blocks (bar.data_table, line.data_table,
|
|
2833
|
+
area.data_table) for per-chart-type overrides per ADR-002.
|
|
2834
|
+
"""
|
|
2835
|
+
|
|
2836
|
+
model_config = ConfigDict(extra="forbid")
|
|
2837
|
+
|
|
2838
|
+
font: FontStyle = Field(
|
|
2839
|
+
default_factory=FontStyle, description="Data_table font style overrides."
|
|
2840
|
+
)
|
|
2841
|
+
divider: RuleStyle = Field(
|
|
2842
|
+
description=(
|
|
2843
|
+
"Rule at the boundary between the chart plot and the data strip. "
|
|
2844
|
+
"For position='bottom': rule sits above the strip (below the axis). "
|
|
2845
|
+
"For position='top': rule sits below the strip rows (above the plot top)."
|
|
2846
|
+
)
|
|
2847
|
+
)
|
|
2848
|
+
row: DataTableRowStyle = Field(description="Data_table row padding and rule style.")
|
|
2849
|
+
label: DataTableLabelStyle = Field(description="Row label (series name) style.")
|
|
2850
|
+
number_align: Literal["left", "right", "decimal"] = Field(
|
|
2851
|
+
description="Alignment of numeric values in data_table cells."
|
|
2852
|
+
)
|
|
2853
|
+
padding_top: float = Field(
|
|
2854
|
+
description=(
|
|
2855
|
+
"Padding above the topmost strip row in pixels. "
|
|
2856
|
+
"For position='bottom': gap between axis labels and the first row. "
|
|
2857
|
+
"For position='top': space above the topmost row (outer edge of strip)."
|
|
2858
|
+
)
|
|
2859
|
+
)
|
|
2860
|
+
padding_bottom: float = Field(
|
|
2861
|
+
description=(
|
|
2862
|
+
"Padding below the last strip row in pixels. "
|
|
2863
|
+
"For position='bottom': space below the last row. "
|
|
2864
|
+
"For position='top': gap between the last row and the plot top edge."
|
|
2865
|
+
)
|
|
2866
|
+
)
|
|
2867
|
+
# Number of x-axis label lines to reserve above the strip. Vega emits an
|
|
2868
|
+
# array label expression (e.g. ['Jan', '2024']) at year boundaries, so the
|
|
2869
|
+
# strip must reserve space for 2 lines even on months that emit a single
|
|
2870
|
+
# line — otherwise the strip overlaps labels at year ticks. Set to 1 if the
|
|
2871
|
+
# theme is known to emit only single-line labels (e.g. year-only cadence).
|
|
2872
|
+
label_max_lines: int = Field(
|
|
2873
|
+
description=(
|
|
2874
|
+
"Number of x-axis label lines to reserve in the axis gap "
|
|
2875
|
+
"(only used for position='bottom'; ignored for position='top'). "
|
|
2876
|
+
"Typically 1 or 2."
|
|
2877
|
+
)
|
|
2878
|
+
)
|
|
2879
|
+
position: Literal["top", "bottom"] = Field(
|
|
2880
|
+
description=(
|
|
2881
|
+
"Strip placement relative to the chart plot. "
|
|
2882
|
+
"'top' places the strip above the plot (no x-axis gap needed); "
|
|
2883
|
+
"'bottom' places it below with the x-axis between plot and strip."
|
|
2884
|
+
)
|
|
2885
|
+
)
|
|
2886
|
+
|
|
2887
|
+
|
|
2888
|
+
if TYPE_CHECKING:
|
|
2889
|
+
|
|
2890
|
+
class DataTableStylePatch(DataTableStyle):
|
|
2891
|
+
pass
|
|
2892
|
+
|
|
2893
|
+
else:
|
|
2894
|
+
DataTableStylePatch = build_patch_model(DataTableStyle)
|
|
2895
|
+
|
|
2896
|
+
|
|
2897
|
+
# Table charts use row-count sizing; aspect-ratio fields from the base class
|
|
2898
|
+
# are meaningless and excluded to prevent silent no-ops.
|
|
2899
|
+
_TABLE_CHART_STYLE_SIZING: frozenset[str] = frozenset(
|
|
2900
|
+
{"aspect_ratio", "min_height", "max_height"}
|
|
2901
|
+
)
|
|
2902
|
+
|
|
2903
|
+
# Patch type for chart-local table style overrides.
|
|
2904
|
+
# At runtime `build_patch_model_ext(TableChartStyle, exclude=_TABLE_CHART_STYLE_SIZING)`
|
|
2905
|
+
# returns a Pydantic class structurally equivalent to `TableChartStyle` but with every
|
|
2906
|
+
# field made Optional (and nested models recursed the same way), minus the sizing
|
|
2907
|
+
# fields that don't apply to tables. Mypy can't follow the dynamic `create_model`
|
|
2908
|
+
# call, so under TYPE_CHECKING we declare the patch as a subclass of the compiled
|
|
2909
|
+
# class. That gives callers accurate field names, nested-model types, and completion;
|
|
2910
|
+
# every field on `TableChartStyle` already has a default, so required-vs-optional
|
|
2911
|
+
# drift between the static stub and the runtime patch is moot.
|
|
2912
|
+
# `no-redef` is required because mypy sees the class definition under
|
|
2913
|
+
# TYPE_CHECKING and the runtime assignment as two declarations of the same
|
|
2914
|
+
# symbol — that's intentional, not accidental rebinding.
|
|
2915
|
+
if TYPE_CHECKING:
|
|
2916
|
+
|
|
2917
|
+
class TableChartStylePatch(TableChartStyle):
|
|
2918
|
+
pass
|
|
2919
|
+
|
|
2920
|
+
else:
|
|
2921
|
+
TableChartStylePatch = build_patch_model_ext(
|
|
2922
|
+
TableChartStyle, exclude=_TABLE_CHART_STYLE_SIZING
|
|
2923
|
+
)
|
|
2924
|
+
|
|
2925
|
+
|
|
2926
|
+
# Spark bar (full chart, not inline)
|
|
2927
|
+
|
|
2928
|
+
|
|
2929
|
+
class SparkBarBarStyle(BaseModel):
|
|
2930
|
+
"""Bar geometry sub-block for SparkBarChartStyle."""
|
|
2931
|
+
|
|
2932
|
+
model_config = ConfigDict(extra="forbid")
|
|
2933
|
+
|
|
2934
|
+
height: float = Field(description="Height of each bar in pixels.")
|
|
2935
|
+
padding: float = Field(description="Vertical padding between bars in pixels.")
|
|
2936
|
+
# Cascade-managed sentinels: filled from style.accent / style.muted in token cascade.
|
|
2937
|
+
color: str | None = Field(
|
|
2938
|
+
default=None,
|
|
2939
|
+
description="Bar fill color; cascade-managed (inherits from style.accent).",
|
|
2940
|
+
)
|
|
2941
|
+
background: str | None = Field(
|
|
2942
|
+
default=None,
|
|
2943
|
+
description="Bar track background color; cascade-managed (inherits from style.muted).",
|
|
2944
|
+
)
|
|
2945
|
+
|
|
2946
|
+
|
|
2947
|
+
class SparkBarChartLabelStyle(BaseModel):
|
|
2948
|
+
"""Category-label sub-block for SparkBarChartStyle.
|
|
2949
|
+
|
|
2950
|
+
Not to be confused with SparkBarLabelStyle, which is the in-cell bar
|
|
2951
|
+
sparkline label inside table cells.
|
|
2952
|
+
"""
|
|
2953
|
+
|
|
2954
|
+
model_config = ConfigDict(extra="forbid")
|
|
2955
|
+
|
|
2956
|
+
visible: bool = Field(description="Show series label text next to bars.")
|
|
2957
|
+
width: float = Field(description="Reserved width for bar label text in pixels.")
|
|
2958
|
+
|
|
2959
|
+
|
|
2960
|
+
class SparkBarCountStyle(BaseModel):
|
|
2961
|
+
"""Count-value sub-block for SparkBarChartStyle."""
|
|
2962
|
+
|
|
2963
|
+
model_config = ConfigDict(extra="forbid")
|
|
2964
|
+
|
|
2965
|
+
visible: bool = Field(description="Show count/value text next to bars.")
|
|
2966
|
+
width: float = Field(description="Reserved width for bar count text in pixels.")
|
|
2967
|
+
|
|
2968
|
+
|
|
2969
|
+
class SparkBarChartStyle(BaseModel):
|
|
2970
|
+
"""Produced by cascade from theme YAML."""
|
|
2971
|
+
|
|
2972
|
+
model_config = ConfigDict(extra="forbid")
|
|
2973
|
+
|
|
2974
|
+
bar: SparkBarBarStyle = Field(description="Bar geometry (height, padding, color).")
|
|
2975
|
+
label: SparkBarChartLabelStyle = Field(
|
|
2976
|
+
description="Category-label column (visibility, reserved width)."
|
|
2977
|
+
)
|
|
2978
|
+
count: SparkBarCountStyle = Field(
|
|
2979
|
+
description="Count-value column (visibility, reserved width)."
|
|
2980
|
+
)
|
|
2981
|
+
max_bars: int = Field(description="Maximum number of bars to render.")
|
|
2982
|
+
font: FontStyle = Field(
|
|
2983
|
+
default_factory=FontStyle, description="Spark_bar font style overrides."
|
|
2984
|
+
)
|
|
2985
|
+
border: BorderStyle = Field(description="Spark_bar outer border style.")
|
|
2986
|
+
subtitle: SubtitleStyle = Field(
|
|
2987
|
+
description="Spark_bar subtitle font-sizing constants."
|
|
2988
|
+
)
|
|
2989
|
+
|
|
2990
|
+
|
|
2991
|
+
# View / autosize
|
|
2992
|
+
|
|
2993
|
+
|
|
2994
|
+
class ViewStyle(BaseModel):
|
|
2995
|
+
model_config = ConfigDict(extra="forbid")
|
|
2996
|
+
|
|
2997
|
+
stroke: str | None = Field(
|
|
2998
|
+
description="Plot area border stroke color; None means no border."
|
|
2999
|
+
)
|
|
3000
|
+
continuous_width: float = Field(
|
|
3001
|
+
description="Default plot width for continuous (quantitative) scales in pixels."
|
|
3002
|
+
)
|
|
3003
|
+
continuous_height: float = Field(
|
|
3004
|
+
description="Default plot height for continuous (quantitative) scales in pixels."
|
|
3005
|
+
)
|
|
3006
|
+
discrete_width: float | None = Field(
|
|
3007
|
+
default=None,
|
|
3008
|
+
description="Default plot width for discrete (ordinal/nominal) scales in pixels; None means auto.",
|
|
3009
|
+
)
|
|
3010
|
+
discrete_height: float | None = Field(
|
|
3011
|
+
default=None,
|
|
3012
|
+
description="Default plot height for discrete (ordinal/nominal) scales in pixels; None means auto.",
|
|
3013
|
+
)
|
|
3014
|
+
|
|
3015
|
+
|
|
3016
|
+
class AutosizeStyle(BaseModel):
|
|
3017
|
+
model_config = ConfigDict(extra="forbid")
|
|
3018
|
+
|
|
3019
|
+
type: str = Field(
|
|
3020
|
+
description="Vega-Lite autosize type (e.g. 'fit', 'fit-x', 'pad')."
|
|
3021
|
+
)
|
|
3022
|
+
contains: str = Field(
|
|
3023
|
+
description="What the autosize dimensions include ('content' or 'padding')."
|
|
3024
|
+
)
|
|
3025
|
+
resize: bool = Field(description="Recompute autosize on each render cycle.")
|
|
3026
|
+
|
|
3027
|
+
|
|
3028
|
+
# Layout
|
|
3029
|
+
|
|
3030
|
+
|
|
3031
|
+
class LayoutGapStyle(BaseModel):
|
|
3032
|
+
model_config = ConfigDict(extra="forbid")
|
|
3033
|
+
|
|
3034
|
+
gap: float = Field(description="Gap between layout items in pixels.")
|
|
3035
|
+
|
|
3036
|
+
|
|
3037
|
+
class GridLayoutStyle(BaseModel):
|
|
3038
|
+
model_config = ConfigDict(extra="forbid")
|
|
3039
|
+
|
|
3040
|
+
columns: int = Field(description="Number of columns in the grid layout.")
|
|
3041
|
+
gap: float = Field(description="Gap between grid cells in pixels.")
|
|
3042
|
+
|
|
3043
|
+
|
|
3044
|
+
class TabsStyle(BaseModel):
|
|
3045
|
+
model_config = ConfigDict(extra="forbid")
|
|
3046
|
+
|
|
3047
|
+
bar_height: float = Field(description="Tab bar height in pixels.")
|
|
3048
|
+
border: BorderStyle = Field(description="Tab bar border style.")
|
|
3049
|
+
font: FontStyle = Field(
|
|
3050
|
+
default_factory=FontStyle, description="Tab label font style overrides."
|
|
3051
|
+
)
|
|
3052
|
+
active_weight: str = Field(description="Font weight for the active tab label.")
|
|
3053
|
+
inactive_weight: str = Field(description="Font weight for inactive tab labels.")
|
|
3054
|
+
# SVG layout constants
|
|
3055
|
+
title_baseline_offset: float = Field(
|
|
3056
|
+
description="Vertical offset to align SVG tab label baseline in pixels."
|
|
3057
|
+
)
|
|
3058
|
+
|
|
3059
|
+
|
|
3060
|
+
class DetailsArrowFontStyle(BaseModel):
|
|
3061
|
+
"""Font style for the expand/collapse arrow glyph."""
|
|
3062
|
+
|
|
3063
|
+
model_config = ConfigDict(extra="forbid")
|
|
3064
|
+
|
|
3065
|
+
size: float = Field(description="Font size of the arrow glyph in pixels.")
|
|
3066
|
+
|
|
3067
|
+
|
|
3068
|
+
class DetailsArrowStyle(BaseModel):
|
|
3069
|
+
"""Layout and font style for the expand/collapse arrow chevron."""
|
|
3070
|
+
|
|
3071
|
+
model_config = ConfigDict(extra="forbid")
|
|
3072
|
+
|
|
3073
|
+
x: float = Field(description="X position of the arrow in pixels.")
|
|
3074
|
+
font: DetailsArrowFontStyle = Field(description="Arrow glyph font style.")
|
|
3075
|
+
|
|
3076
|
+
|
|
3077
|
+
class DetailsStyle(BaseModel):
|
|
3078
|
+
model_config = ConfigDict(extra="forbid")
|
|
3079
|
+
|
|
3080
|
+
summary_height: float = Field(
|
|
3081
|
+
description="Height of the details summary (collapsed) row in pixels."
|
|
3082
|
+
)
|
|
3083
|
+
border: BorderStyle = Field(description="Details element border style.")
|
|
3084
|
+
font: FontStyle = Field(
|
|
3085
|
+
default_factory=FontStyle,
|
|
3086
|
+
description="Details summary font style overrides.",
|
|
3087
|
+
)
|
|
3088
|
+
# SVG layout constants
|
|
3089
|
+
arrow: DetailsArrowStyle = Field(
|
|
3090
|
+
description="Expand/collapse arrow glyph layout and font style."
|
|
3091
|
+
)
|
|
3092
|
+
label_x: float = Field(
|
|
3093
|
+
description="X position of the details summary label text in pixels."
|
|
3094
|
+
)
|
|
3095
|
+
text_baseline_offset: float = Field(
|
|
3096
|
+
description="Vertical offset to align SVG details text baseline in pixels."
|
|
3097
|
+
)
|
|
3098
|
+
content_y_offset: float = Field(
|
|
3099
|
+
description="Y offset of the expanded details content area in pixels."
|
|
3100
|
+
)
|
|
3101
|
+
|
|
3102
|
+
|
|
3103
|
+
class LayoutStyle(BaseModel):
|
|
3104
|
+
model_config = ConfigDict(extra="forbid")
|
|
3105
|
+
|
|
3106
|
+
rows: LayoutGapStyle = Field(description="Row layout gap configuration.")
|
|
3107
|
+
cols: LayoutGapStyle = Field(description="Column layout gap configuration.")
|
|
3108
|
+
grid: GridLayoutStyle = Field(description="Grid layout configuration.")
|
|
3109
|
+
tabs: TabsStyle = Field(description="Tabs layout style.")
|
|
3110
|
+
details: DetailsStyle = Field(description="Details (accordion) layout style.")
|
|
3111
|
+
|
|
3112
|
+
|
|
3113
|
+
# Variables chrome
|
|
3114
|
+
|
|
3115
|
+
|
|
3116
|
+
class InputWidths(BaseModel):
|
|
3117
|
+
model_config = ConfigDict(extra="forbid")
|
|
3118
|
+
|
|
3119
|
+
text: float = Field(description="Default width for text inputs in pixels.")
|
|
3120
|
+
number: float = Field(description="Default width for number inputs in pixels.")
|
|
3121
|
+
range: float = Field(description="Default width for range inputs in pixels.")
|
|
3122
|
+
slider_value_min: float = Field(
|
|
3123
|
+
description="Minimum width for slider value display in pixels."
|
|
3124
|
+
)
|
|
3125
|
+
checkbox: float = Field(description="Default width for checkbox inputs in pixels.")
|
|
3126
|
+
daterange: float = Field(
|
|
3127
|
+
description="Default width for daterange chip triggers in pixels."
|
|
3128
|
+
)
|
|
3129
|
+
|
|
3130
|
+
|
|
3131
|
+
class RangeDefaults(BaseModel):
|
|
3132
|
+
model_config = ConfigDict(extra="forbid")
|
|
3133
|
+
|
|
3134
|
+
default_min: float = Field(description="Default minimum value for range inputs.")
|
|
3135
|
+
default_max: float = Field(description="Default maximum value for range inputs.")
|
|
3136
|
+
default_step: float = Field(description="Default step size for range inputs.")
|
|
3137
|
+
|
|
3138
|
+
|
|
3139
|
+
class InputStyle(BaseModel):
|
|
3140
|
+
model_config = ConfigDict(extra="forbid")
|
|
3141
|
+
|
|
3142
|
+
height: float = Field(description="Input control height in pixels.")
|
|
3143
|
+
border: BorderStyle = Field(description="Input border style.")
|
|
3144
|
+
focus_color: str | None = Field(
|
|
3145
|
+
default=None,
|
|
3146
|
+
description="Input focus ring color; cascade-managed (inherits from style.accent).",
|
|
3147
|
+
)
|
|
3148
|
+
background: str = Field(description="Input background color.")
|
|
3149
|
+
padding: SpacingValues = Field(description="Input inner padding in pixels.")
|
|
3150
|
+
widths: InputWidths = Field(description="Per-input-type default widths.")
|
|
3151
|
+
range: RangeDefaults = Field(description="Range input default min/max/step values.")
|
|
3152
|
+
|
|
3153
|
+
|
|
3154
|
+
class PageStyle(BaseModel):
|
|
3155
|
+
"""Page-level (outer HTML canvas) styling.
|
|
3156
|
+
|
|
3157
|
+
Distinct from the top-level `background` which is the working-surface color
|
|
3158
|
+
(board/card fills). `page.background` is the off-white canvas behind the face.
|
|
3159
|
+
"""
|
|
3160
|
+
|
|
3161
|
+
model_config = ConfigDict(extra="forbid")
|
|
3162
|
+
|
|
3163
|
+
background: str = Field(
|
|
3164
|
+
description="Page canvas background color (behind the face board)."
|
|
3165
|
+
)
|
|
3166
|
+
|
|
3167
|
+
|
|
3168
|
+
class VariablesLabelStyle(BaseModel):
|
|
3169
|
+
"""Per-label font substyle for variable controls. Cascades from variables.font."""
|
|
3170
|
+
|
|
3171
|
+
model_config = ConfigDict(extra="forbid")
|
|
3172
|
+
|
|
3173
|
+
# Cascade-starter: _apply_cascade fills from variables.font, then root.
|
|
3174
|
+
font: FontStyle = Field(
|
|
3175
|
+
default_factory=FontStyle,
|
|
3176
|
+
description="Variable label font style overrides.",
|
|
3177
|
+
)
|
|
3178
|
+
|
|
3179
|
+
|
|
3180
|
+
class VariablesValueStyle(BaseModel):
|
|
3181
|
+
"""Per-value font substyle for variable controls. Cascades from variables.font."""
|
|
3182
|
+
|
|
3183
|
+
model_config = ConfigDict(extra="forbid")
|
|
3184
|
+
|
|
3185
|
+
# Cascade-starter: _apply_cascade fills from variables.font, then root.
|
|
3186
|
+
font: FontStyle = Field(
|
|
3187
|
+
default_factory=FontStyle,
|
|
3188
|
+
description="Variable value font style overrides.",
|
|
3189
|
+
)
|
|
3190
|
+
# OpenType numeric feature for slider, number, and date-range value displays.
|
|
3191
|
+
numeric_variant: Literal["normal", "tabular-nums"] = Field(
|
|
3192
|
+
description="OpenType numeric feature for value displays ('normal' or 'tabular-nums')."
|
|
3193
|
+
)
|
|
3194
|
+
|
|
3195
|
+
|
|
3196
|
+
class VariablesPlaceholderStyle(BaseModel):
|
|
3197
|
+
"""Per-placeholder font substyle for variable controls.
|
|
3198
|
+
|
|
3199
|
+
Placeholder = unselected/hint text in variable inputs (e.g. "-- All --" in
|
|
3200
|
+
a select before the user picks a value). Reads lighter than the selected-value
|
|
3201
|
+
text so the strip is scannable. Cascades from variables.font like value/label.
|
|
3202
|
+
"""
|
|
3203
|
+
|
|
3204
|
+
model_config = ConfigDict(extra="forbid")
|
|
3205
|
+
|
|
3206
|
+
# Cascade-starter: _apply_cascade fills from variables.font, then root.
|
|
3207
|
+
font: FontStyle = Field(
|
|
3208
|
+
default_factory=FontStyle,
|
|
3209
|
+
description="Variable placeholder-text font style overrides.",
|
|
3210
|
+
)
|
|
3211
|
+
|
|
3212
|
+
|
|
3213
|
+
class VariablesStyle(BaseModel):
|
|
3214
|
+
"""Variable controls chrome styling.
|
|
3215
|
+
|
|
3216
|
+
Note: the old chart_themes system had separate `variables.background`
|
|
3217
|
+
(container bg) and `variables.input_background` (input bg) fields that were
|
|
3218
|
+
sometimes distinct (e.g., cream). In the unified model both collapse
|
|
3219
|
+
to `input.background` — by design, the container and input share the same
|
|
3220
|
+
background. If distinct container vs. input backgrounds are needed in future,
|
|
3221
|
+
add `container_background: str | None` here.
|
|
3222
|
+
"""
|
|
3223
|
+
|
|
3224
|
+
model_config = ConfigDict(extra="forbid")
|
|
3225
|
+
|
|
3226
|
+
visible: bool = Field(description="Show the variables control panel.")
|
|
3227
|
+
position: Literal["top", "bottom", "title-inline"] = Field(
|
|
3228
|
+
description="Variables strip placement: stacked under the title (top/bottom) "
|
|
3229
|
+
"or on one horizontal band with the face title (title-inline)."
|
|
3230
|
+
)
|
|
3231
|
+
gap: float = Field(description="Gap between variable controls in pixels.")
|
|
3232
|
+
label_position: str = Field(
|
|
3233
|
+
description="Position of labels relative to their input controls (e.g. 'left', 'top')."
|
|
3234
|
+
)
|
|
3235
|
+
title_inline_title_max_width: float = Field(
|
|
3236
|
+
description=(
|
|
3237
|
+
"When position is title-inline: max title column width in px. "
|
|
3238
|
+
"0 means no cap (title uses remaining width after reserving space for variables)."
|
|
3239
|
+
)
|
|
3240
|
+
)
|
|
3241
|
+
font: FontStyle = Field(
|
|
3242
|
+
default_factory=FontStyle,
|
|
3243
|
+
description="Variables panel base font style overrides.",
|
|
3244
|
+
)
|
|
3245
|
+
label: VariablesLabelStyle = Field(description="Variable label typography.")
|
|
3246
|
+
value: VariablesValueStyle = Field(description="Variable value typography.")
|
|
3247
|
+
placeholder: VariablesPlaceholderStyle = Field(
|
|
3248
|
+
description="Style for unselected/hint text in variable inputs."
|
|
3249
|
+
)
|
|
3250
|
+
line_height: float = Field(
|
|
3251
|
+
description="Line height for variable control text in pixels."
|
|
3252
|
+
)
|
|
3253
|
+
padding: float = Field(
|
|
3254
|
+
description="Outer padding around the variables panel in pixels."
|
|
3255
|
+
)
|
|
3256
|
+
container_height: float = Field(
|
|
3257
|
+
description="Height of the variables panel container in pixels."
|
|
3258
|
+
)
|
|
3259
|
+
container_padding: float = Field(
|
|
3260
|
+
description="Inner padding of the variables panel container in pixels."
|
|
3261
|
+
)
|
|
3262
|
+
border: BorderStyle = Field(description="Variables panel border style.")
|
|
3263
|
+
control_gap: float = Field(
|
|
3264
|
+
description="Gap between label and input within a single control in pixels."
|
|
3265
|
+
)
|
|
3266
|
+
input: InputStyle = Field(description="Input control style.")
|
|
3267
|
+
popover_rail_background: str = Field(
|
|
3268
|
+
description=(
|
|
3269
|
+
"Background color for the preset rail (left column) inside the "
|
|
3270
|
+
"daterange chip's popover. Typically the theme's `surface-subtle` "
|
|
3271
|
+
"step — gray-50 on white-canvas themes, a cream-toned light on "
|
|
3272
|
+
"warm-canvas themes. Distinct from the popover card itself, which "
|
|
3273
|
+
"is always white-equivalent."
|
|
3274
|
+
)
|
|
3275
|
+
)
|
|
3276
|
+
|
|
3277
|
+
|
|
3278
|
+
# =============================================================================
|
|
3279
|
+
# Style ROOT
|
|
3280
|
+
# =============================================================================
|
|
3281
|
+
|
|
3282
|
+
|
|
3283
|
+
class Style(BaseModel):
|
|
3284
|
+
"""Authoritative compiled style. Built from a single theme YAML file.
|
|
3285
|
+
|
|
3286
|
+
All sections present with defaults matching themes/stark.yaml.
|
|
3287
|
+
This is the 'compiled from theme' layer — not yet resolved/cascaded.
|
|
3288
|
+
"""
|
|
3289
|
+
|
|
3290
|
+
model_config = ConfigDict(extra="forbid")
|
|
3291
|
+
|
|
3292
|
+
board: BoardStyle = Field(description="Face-level structural board dimensions.")
|
|
3293
|
+
background: str = Field(
|
|
3294
|
+
description="Working-surface background color (board and card fills)."
|
|
3295
|
+
)
|
|
3296
|
+
# Semantic color tokens — set once at root level, cascade to all UI chrome.
|
|
3297
|
+
# accent: spark.color, spark.bar.color, spark_bar.bar.color, focus_color
|
|
3298
|
+
# muted: spark.bar.background, spark_bar.bar.background
|
|
3299
|
+
# font.color: tick.stroke.color, rule.stroke.color
|
|
3300
|
+
accent: str = Field(
|
|
3301
|
+
description="Accent color token cascaded to sparklines, bars, and focus rings."
|
|
3302
|
+
)
|
|
3303
|
+
muted: str = Field(
|
|
3304
|
+
description="Muted color token cascaded to spark backgrounds and bar tracks."
|
|
3305
|
+
)
|
|
3306
|
+
font: RootFontStyle = Field(
|
|
3307
|
+
description="Root font configuration including emoji mode."
|
|
3308
|
+
)
|
|
3309
|
+
border: BorderStyle = Field(
|
|
3310
|
+
description="Default border style cascaded to all chart cards."
|
|
3311
|
+
)
|
|
3312
|
+
box_shadow: str | None = Field(
|
|
3313
|
+
default=None,
|
|
3314
|
+
description="CSS box shadow for chart cards; None means no shadow.",
|
|
3315
|
+
)
|
|
3316
|
+
opacity: float = Field(description="Default mark opacity (0–1).")
|
|
3317
|
+
title: TitleStyle = Field(description="Board and face title style.")
|
|
3318
|
+
text: TextStyle = Field(description="Markdown and plain text content style.")
|
|
3319
|
+
placeholder: PlaceholderStyle = Field(
|
|
3320
|
+
description="Placeholder overlay style for empty charts."
|
|
3321
|
+
)
|
|
3322
|
+
charts: ChartsStyle = Field(
|
|
3323
|
+
description="Root of all chart-type styles and shared chart configuration."
|
|
3324
|
+
)
|
|
3325
|
+
layout: LayoutStyle = Field(
|
|
3326
|
+
description="Layout container styles (rows, cols, grid, tabs, details)."
|
|
3327
|
+
)
|
|
3328
|
+
variables: VariablesStyle = Field(description="Variable controls chrome style.")
|
|
3329
|
+
page: PageStyle = Field(description="Page-level canvas style (behind the board).")
|
|
3330
|
+
# Cascade-managed sentinel — None means "no aliases at this cascade level" (not empty).
|
|
3331
|
+
# The default theme always supplies this; face YAML patches may omit it (→ None).
|
|
3332
|
+
# deep_merge key-wise merges face-level aliases onto theme aliases so face keys win
|
|
3333
|
+
# while unredefined theme keys propagate. None is intentional here — unlike other
|
|
3334
|
+
# Compiled fields the theme populates, this sentinel distinguishes "no override"
|
|
3335
|
+
# from "empty override" across every cascade step, not just the base.
|
|
3336
|
+
formats: dict[str, str] | None = Field(
|
|
3337
|
+
default=None,
|
|
3338
|
+
description="Format alias map; None means no aliases at this cascade level.",
|
|
3339
|
+
)
|
|
3340
|
+
# Theme palette role assignments.
|
|
3341
|
+
# Open dict: role name → palette file name. No enforced enum.
|
|
3342
|
+
# Default theme seeds conventional roles (chrome, negative, positive, warning,
|
|
3343
|
+
# category, sequence, diverge). Child themes may override individual roles.
|
|
3344
|
+
# None = not authored at this cascade level.
|
|
3345
|
+
palettes: dict[str, str] | None = Field(
|
|
3346
|
+
default=None,
|
|
3347
|
+
description=(
|
|
3348
|
+
"Theme palette role assignments: open dict mapping role name to palette file name. "
|
|
3349
|
+
"Default seed: chrome, negative, positive, warning, category, sequence, diverge."
|
|
3350
|
+
),
|
|
3351
|
+
)
|
|
3352
|
+
# Top-level theme role shortcuts.
|
|
3353
|
+
# Optional bare aliases: e.g. ink → chrome.heading. Face authors write the
|
|
3354
|
+
# bare name; resolver dispatches via the dotted palette address.
|
|
3355
|
+
# None = no top-level aliases at this cascade level.
|
|
3356
|
+
roles: dict[str, str] | None = Field(
|
|
3357
|
+
default=None,
|
|
3358
|
+
description=(
|
|
3359
|
+
"Optional top-level theme role aliases: bare name → role.alias. "
|
|
3360
|
+
"e.g. ink: chrome.heading"
|
|
3361
|
+
),
|
|
3362
|
+
)
|
|
3363
|
+
# Not theme-populated; per-face authored CSS-chrome. None = no padding override.
|
|
3364
|
+
padding: SpacingValues | None = Field(
|
|
3365
|
+
default=None,
|
|
3366
|
+
description="Per-face padding override (CSS shorthand or structured).",
|
|
3367
|
+
)
|
|
3368
|
+
# Not theme-populated; per-face authored CSS-chrome. None = no margin override.
|
|
3369
|
+
margin: SpacingValues | None = Field(
|
|
3370
|
+
default=None,
|
|
3371
|
+
description="Per-face margin override (CSS shorthand or structured).",
|
|
3372
|
+
)
|
|
3373
|
+
# Not theme-populated; per-face gap override in pixels. None = use layout gap token.
|
|
3374
|
+
gap: float | None = Field(
|
|
3375
|
+
default=None, description="Per-face gap between layout items in pixels."
|
|
3376
|
+
)
|
|
3377
|
+
# Not theme-populated; per-face text color override. None = inherit from theme.
|
|
3378
|
+
color: str | None = Field(
|
|
3379
|
+
default=None, description="Per-face text color override as a CSS color string."
|
|
3380
|
+
)
|
|
3381
|
+
|
|
3382
|
+
|
|
3383
|
+
# =============================================================================
|
|
3384
|
+
# FORWARD-REF RESOLUTION
|
|
3385
|
+
#
|
|
3386
|
+
# ChartsStyle references ViewStyle, AutosizeStyle, KpiChartStyle, TableChartStyle,
|
|
3387
|
+
# SparkBarChartStyle, and DataTableStyle — all defined after ChartsStyle in this
|
|
3388
|
+
# file. With `from __future__ import annotations`, pydantic stores these as
|
|
3389
|
+
# ForwardRefs until model_rebuild() resolves them. Rebuilding here (after all
|
|
3390
|
+
# classes are defined) ensures that build_patch_model() in authored.py sees
|
|
3391
|
+
# resolved annotations when it is called at module-import time.
|
|
3392
|
+
# =============================================================================
|
|
3393
|
+
|
|
3394
|
+
ChartsStyle.model_rebuild()
|
|
3395
|
+
|
|
3396
|
+
|
|
3397
|
+
# =============================================================================
|
|
3398
|
+
# PRE-GENERATED PATCH TYPES
|
|
3399
|
+
# =============================================================================
|