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,27 @@
|
|
|
1
|
+
"""Chart rendering subpackage.
|
|
2
|
+
|
|
3
|
+
Contains all chart-type-specific renderers (KPI, table, spark bar),
|
|
4
|
+
Vega-Lite spec generation, chart orchestration, and auto-enrichment.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataface.core.render.chart.decisions import enrich_chart
|
|
8
|
+
from dataface.core.render.chart.kpi import render_kpi_svg
|
|
9
|
+
from dataface.core.render.chart.rendering import render_chart_item, render_layout_item
|
|
10
|
+
from dataface.core.render.chart.spark import render_spark
|
|
11
|
+
from dataface.core.render.chart.spark_bar import render_spark_bar_svg
|
|
12
|
+
from dataface.core.render.chart.table import render_table_svg
|
|
13
|
+
from dataface.core.render.chart.vega_lite import generate_vega_lite_spec, render_chart
|
|
14
|
+
from dataface.core.render.utils import slug_to_text
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"enrich_chart",
|
|
18
|
+
"generate_vega_lite_spec",
|
|
19
|
+
"render_chart",
|
|
20
|
+
"render_chart_item",
|
|
21
|
+
"render_kpi_svg",
|
|
22
|
+
"render_layout_item",
|
|
23
|
+
"render_spark",
|
|
24
|
+
"render_spark_bar_svg",
|
|
25
|
+
"render_table_svg",
|
|
26
|
+
"slug_to_text",
|
|
27
|
+
]
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Donut/pie attached-table trigger + series extraction.
|
|
2
|
+
|
|
3
|
+
When an arc chart is too small or too crowded to carry direct labels
|
|
4
|
+
cleanly, the renderer swaps to a series-keyed mini-table beneath the
|
|
5
|
+
donut. The trigger is data-and-width aware: at the tiny tier we always
|
|
6
|
+
attach a table; otherwise we count the wedges that would actually carry
|
|
7
|
+
a label (share above WEDGE_LABEL_MIN_SHARE) and compare against a
|
|
8
|
+
per-tier maximum.
|
|
9
|
+
|
|
10
|
+
Decision rule (single source of truth):
|
|
11
|
+
|
|
12
|
+
visible = sum(share > 0.08 for share in shares)
|
|
13
|
+
|
|
14
|
+
if tier == "tiny": attach table
|
|
15
|
+
elif visible == 0: attach table (no labels would render)
|
|
16
|
+
elif visible > tier_max: attach table (too many to fit cleanly)
|
|
17
|
+
else: direct labels with small wedges suppressed
|
|
18
|
+
|
|
19
|
+
Constants are inline here — promote to theme cascade if a future ask
|
|
20
|
+
demands per-theme tuning. The current numbers were locked against visual
|
|
21
|
+
spike evidence (5-series narrow renders cleanly, 8 equal-mass series at
|
|
22
|
+
wide overflow).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from dataface.core.compile.models.chart.authored import TableColumnConfig
|
|
30
|
+
from dataface.core.compile.typography import width_tier
|
|
31
|
+
|
|
32
|
+
# Wedges with share at or below this value get no direct label — the
|
|
33
|
+
# wedge still renders, hover tooltip carries the value. 8% is the
|
|
34
|
+
# observed danger floor: 5-6% wedges next to each other vertically
|
|
35
|
+
# stack their labels at near-cardinal angles; 8% gives an honest buffer.
|
|
36
|
+
WEDGE_LABEL_MIN_SHARE: float = 0.08
|
|
37
|
+
|
|
38
|
+
# Per-tier ceiling on how many direct-label callouts fit comfortably
|
|
39
|
+
# around the disk. Tiny is absent because tiny always uses the table.
|
|
40
|
+
_TIER_MAX_VISIBLE_CALLOUTS: dict[str, int] = {
|
|
41
|
+
"narrow": 3,
|
|
42
|
+
"medium": 4,
|
|
43
|
+
"wide": 5,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def should_attach_table(width: float, shares: list[float]) -> bool:
|
|
48
|
+
"""Return True when the arc chart should render as donut + attached table.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
width: Final card pixel width the donut will be rendered into.
|
|
52
|
+
shares: Per-wedge fractional shares (each in [0, 1], normally summing
|
|
53
|
+
to 1.0). Order doesn't matter — only the count of visible
|
|
54
|
+
callouts matters for the decision.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
True → render donut SVG with no direct labels, plus a swatch-keyed
|
|
58
|
+
table beneath it. False → render donut SVG with direct labels for
|
|
59
|
+
wedges whose share exceeds ``WEDGE_LABEL_MIN_SHARE``.
|
|
60
|
+
|
|
61
|
+
Empty ``shares`` (data is empty or all-zero) returns False — that's
|
|
62
|
+
the placeholder case, handled by the standard renderer's existing
|
|
63
|
+
empty-data path, not by attaching a table.
|
|
64
|
+
"""
|
|
65
|
+
if not shares:
|
|
66
|
+
return False
|
|
67
|
+
tier = width_tier(width)
|
|
68
|
+
if tier == "tiny":
|
|
69
|
+
return True
|
|
70
|
+
visible = sum(1 for s in shares if s > WEDGE_LABEL_MIN_SHARE)
|
|
71
|
+
if visible == 0:
|
|
72
|
+
return True
|
|
73
|
+
return visible > _TIER_MAX_VISIBLE_CALLOUTS[tier]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Pixel gap between donut SVG and attached table SVG. Tuned visually;
|
|
77
|
+
# theme-cascadable if any future caller needs to vary it.
|
|
78
|
+
_DONUT_TABLE_GAP_PX: float = 12.0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def compute_shares(theta_field: str, data: list[dict[str, Any]]) -> list[float]:
|
|
82
|
+
"""Compute per-row fractional shares from theta field values.
|
|
83
|
+
|
|
84
|
+
Returns shares as floats in [0, 1]. Returns an empty list when data
|
|
85
|
+
is empty, total is zero, or any row's theta value can't be coerced
|
|
86
|
+
to a number — callers treat all three cases as "can't decide
|
|
87
|
+
direct-vs-table from data alone" and route to the standard renderer
|
|
88
|
+
(which has its own placeholder + type-coercion handling).
|
|
89
|
+
|
|
90
|
+
Mirrors ``_augment_arc_label_data``'s tolerance for the CSV adapter
|
|
91
|
+
string-numeric shapes ("1,234"): runs ``normalize_data_types`` so a
|
|
92
|
+
row that the standard renderer would accept doesn't crash the
|
|
93
|
+
dispatcher on `float(...)`.
|
|
94
|
+
"""
|
|
95
|
+
if not data:
|
|
96
|
+
return []
|
|
97
|
+
from dataface.core.render.utils import normalize_data_types
|
|
98
|
+
|
|
99
|
+
normalized = normalize_data_types(data)
|
|
100
|
+
try:
|
|
101
|
+
values = [float(row.get(theta_field, 0) or 0) for row in normalized]
|
|
102
|
+
except (TypeError, ValueError):
|
|
103
|
+
return []
|
|
104
|
+
total = sum(values)
|
|
105
|
+
if total <= 0:
|
|
106
|
+
return []
|
|
107
|
+
return [v / total for v in values]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def compose_attached_table_svg(
|
|
111
|
+
donut_svg: str,
|
|
112
|
+
table_svg: str,
|
|
113
|
+
donut_width: float,
|
|
114
|
+
donut_height: float,
|
|
115
|
+
table_width: float,
|
|
116
|
+
table_height: float,
|
|
117
|
+
gap: float = _DONUT_TABLE_GAP_PX,
|
|
118
|
+
) -> tuple[str, float, float]:
|
|
119
|
+
"""Stack a donut SVG above a table SVG into a single outer SVG.
|
|
120
|
+
|
|
121
|
+
Returns ``(outer_svg, outer_width, outer_height)``. Outer width is
|
|
122
|
+
``max(donut_width, table_width)``; outer height = donut + gap + table.
|
|
123
|
+
The donut sits at top-left (y=0); the table is **horizontally
|
|
124
|
+
centered** beneath it. Centering is the right default for the
|
|
125
|
+
donut-attached use case — the table is meant to read as a legend
|
|
126
|
+
beneath the donut, not as a full-width data block.
|
|
127
|
+
"""
|
|
128
|
+
outer_w = max(donut_width, table_width)
|
|
129
|
+
outer_h = donut_height + gap + table_height
|
|
130
|
+
table_y = donut_height + gap
|
|
131
|
+
table_x = (outer_w - table_width) / 2 # centered under the donut
|
|
132
|
+
donut_x = (outer_w - donut_width) / 2 # also center the donut if narrower
|
|
133
|
+
return (
|
|
134
|
+
f'<svg xmlns="http://www.w3.org/2000/svg" '
|
|
135
|
+
f'width="{outer_w}" height="{outer_h}" '
|
|
136
|
+
f'viewBox="0 0 {outer_w} {outer_h}">'
|
|
137
|
+
f'<g transform="translate({donut_x}, 0)">{donut_svg}</g>'
|
|
138
|
+
f'<g transform="translate({table_x}, {table_y})">{table_svg}</g>'
|
|
139
|
+
f"</svg>",
|
|
140
|
+
outer_w,
|
|
141
|
+
outer_h,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _format_value_for_width(value: Any, value_format: Any) -> str:
|
|
146
|
+
"""Render a value to the string the table will display so we can
|
|
147
|
+
measure its pixel width.
|
|
148
|
+
|
|
149
|
+
``value_format`` is whatever the caller pulled off the chart:
|
|
150
|
+
``None``, a d3-format string, or a ``FormatConfig`` (which may carry
|
|
151
|
+
``prefix`` / ``suffix`` on top of a ``spec``). Width measurement
|
|
152
|
+
only needs an upper-bound string; the table renderer formats the
|
|
153
|
+
actual emitted text. Returns the empty string for None / unparseable
|
|
154
|
+
values (zero contribution to column width).
|
|
155
|
+
"""
|
|
156
|
+
from dataface.core.compile.models.primitives import FormatConfig
|
|
157
|
+
|
|
158
|
+
if value is None:
|
|
159
|
+
return ""
|
|
160
|
+
try:
|
|
161
|
+
v = float(value)
|
|
162
|
+
except (TypeError, ValueError):
|
|
163
|
+
return str(value)
|
|
164
|
+
|
|
165
|
+
spec: str | None
|
|
166
|
+
prefix = ""
|
|
167
|
+
suffix = ""
|
|
168
|
+
if isinstance(value_format, FormatConfig):
|
|
169
|
+
spec = value_format.spec
|
|
170
|
+
prefix = value_format.prefix or ""
|
|
171
|
+
suffix = value_format.suffix or ""
|
|
172
|
+
elif isinstance(value_format, str):
|
|
173
|
+
spec = value_format
|
|
174
|
+
else:
|
|
175
|
+
spec = None
|
|
176
|
+
|
|
177
|
+
# Mirror a few common d3 specs for width approximation. Unknown
|
|
178
|
+
# specs fall back to integer-with-commas; the actual rendered text
|
|
179
|
+
# is the table renderer's job.
|
|
180
|
+
if spec == "$,.0f":
|
|
181
|
+
body = f"${v:,.0f}"
|
|
182
|
+
elif spec == ",.0f" or spec is None:
|
|
183
|
+
body = f"{v:,.0f}"
|
|
184
|
+
elif spec == ".0%":
|
|
185
|
+
body = f"{int(round(v * 100))}%"
|
|
186
|
+
else:
|
|
187
|
+
body = f"{v:,.0f}"
|
|
188
|
+
return f"{prefix}{body}{suffix}"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def build_attached_table_columns(
|
|
192
|
+
rows: list[dict[str, Any]],
|
|
193
|
+
*,
|
|
194
|
+
value_format: Any,
|
|
195
|
+
font_size: float,
|
|
196
|
+
font_family: str,
|
|
197
|
+
cell_pad: int = 6,
|
|
198
|
+
) -> tuple[dict[str, TableColumnConfig], float]:
|
|
199
|
+
"""Build per-column ``TableColumnConfig`` objects for the attached table
|
|
200
|
+
plus the total table width (sum of column widths).
|
|
201
|
+
|
|
202
|
+
Sizes each column via the same font measurer the table renderer
|
|
203
|
+
uses, threaded the same ``font_size`` and ``font_family`` the table
|
|
204
|
+
will paint with. Caller pulls both off the resolved table style
|
|
205
|
+
(``tc.font.size`` / ``tc.font.family``) so a theme that retunes
|
|
206
|
+
table type doesn't desync width-sizing from rendering.
|
|
207
|
+
|
|
208
|
+
``value_format`` is the chart's format hint (``str``, ``FormatConfig``,
|
|
209
|
+
or ``None``); accepted as-is and passed through to
|
|
210
|
+
``TableColumnConfig.format``.
|
|
211
|
+
"""
|
|
212
|
+
from dataface.core.render.font_measurement import get_font_measurer
|
|
213
|
+
|
|
214
|
+
if not rows:
|
|
215
|
+
# Empty case shouldn't actually fire (caller's trigger gate
|
|
216
|
+
# rejects empty-shares before we get here) but return a sane
|
|
217
|
+
# default so misuse fails loudly rather than panics.
|
|
218
|
+
return ({}, 0.0)
|
|
219
|
+
|
|
220
|
+
measurer = get_font_measurer(font_family)
|
|
221
|
+
|
|
222
|
+
def _col_width(strings: list[str], floor: int) -> int:
|
|
223
|
+
widest = max(
|
|
224
|
+
(measurer.measure(s, font_size) for s in strings),
|
|
225
|
+
default=0,
|
|
226
|
+
)
|
|
227
|
+
# 3px buffer caps measurer rounding so a value that measures
|
|
228
|
+
# 25.6px doesn't end up in a 25px column that would clip it.
|
|
229
|
+
return max(floor, int(widest) + cell_pad * 2 + 3)
|
|
230
|
+
|
|
231
|
+
swatch_w = 24
|
|
232
|
+
share_strings = [str(r.get("share", "")) for r in rows]
|
|
233
|
+
share_w = _col_width(share_strings, floor=36)
|
|
234
|
+
name_strings = [str(r.get("name", "")) for r in rows]
|
|
235
|
+
name_w = _col_width(name_strings, floor=60)
|
|
236
|
+
value_strings = [
|
|
237
|
+
_format_value_for_width(r.get("value"), value_format) for r in rows
|
|
238
|
+
]
|
|
239
|
+
value_w = _col_width(value_strings, floor=50)
|
|
240
|
+
|
|
241
|
+
columns = {
|
|
242
|
+
"swatch": TableColumnConfig(swatch=True, width=swatch_w),
|
|
243
|
+
"share": TableColumnConfig(align="right", width=share_w),
|
|
244
|
+
"name": TableColumnConfig(width=name_w),
|
|
245
|
+
"value": TableColumnConfig(
|
|
246
|
+
format=value_format,
|
|
247
|
+
align="right",
|
|
248
|
+
width=value_w,
|
|
249
|
+
),
|
|
250
|
+
}
|
|
251
|
+
return columns, float(swatch_w + share_w + name_w + value_w)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Render output types for the chart render pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
ArtifactKind = Literal["vega_spec", "svg", "json"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class RenderArtifact:
|
|
13
|
+
"""Chart-domain render output before transport conversion."""
|
|
14
|
+
|
|
15
|
+
kind: ArtifactKind
|
|
16
|
+
payload: dict[str, Any] | str
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Wrapped SVG renderer for callout charts (type: callout)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import html
|
|
6
|
+
|
|
7
|
+
from dataface.core.compile.chart_resolved import ResolvedChart
|
|
8
|
+
from dataface.core.compile.config import get_config
|
|
9
|
+
from dataface.core.compile.models.style.merged import (
|
|
10
|
+
MergedStyle,
|
|
11
|
+
resolve_style,
|
|
12
|
+
)
|
|
13
|
+
from dataface.core.compile.palette import color as resolve_palette_color
|
|
14
|
+
from dataface.core.render.font_measurement import get_font_measurer
|
|
15
|
+
from mdsvg.fonts import wrap_text_precise
|
|
16
|
+
|
|
17
|
+
_MIN_HEIGHT = 60.0
|
|
18
|
+
_LINE_HEIGHT_RATIO = 1.35
|
|
19
|
+
# Keep cards readable without letting a long stack trace consume the full board.
|
|
20
|
+
_MAX_MESSAGE_LINES = 12
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def render_callout_svg(
|
|
24
|
+
*,
|
|
25
|
+
chart_id: str,
|
|
26
|
+
message: str,
|
|
27
|
+
width: float,
|
|
28
|
+
height: float | None = None,
|
|
29
|
+
title: str | None = None,
|
|
30
|
+
resolved_style: MergedStyle | None = None,
|
|
31
|
+
code: str | None = None,
|
|
32
|
+
hint: str | None = None,
|
|
33
|
+
doc_url: str | None = None,
|
|
34
|
+
tone: str = "negative",
|
|
35
|
+
) -> str:
|
|
36
|
+
"""Render a wrapped inline callout card as SVG.
|
|
37
|
+
|
|
38
|
+
Colors are resolved from ``{tone}.*`` palette roles (bg, border, solid,
|
|
39
|
+
text) at render time so each tone gets the correct theme-defined semantic
|
|
40
|
+
color without storing per-tone style copies.
|
|
41
|
+
"""
|
|
42
|
+
style = resolved_style or resolve_style(get_config().style)
|
|
43
|
+
inline = style.charts.callout
|
|
44
|
+
padding = inline.padding
|
|
45
|
+
section_gap = inline.section_gap
|
|
46
|
+
|
|
47
|
+
background = resolve_palette_color(f"{tone}.bg")
|
|
48
|
+
border_color = resolve_palette_color(f"{tone}.border")
|
|
49
|
+
title_color = resolve_palette_color(f"{tone}.solid")
|
|
50
|
+
message_color = resolve_palette_color(f"{tone}.text")
|
|
51
|
+
|
|
52
|
+
w = max(width, 120.0)
|
|
53
|
+
content_width = max(w - (2 * padding), 40.0)
|
|
54
|
+
|
|
55
|
+
title_rf = inline.title.font # MergedFontStyle — all fields concrete
|
|
56
|
+
message_rf = inline.message.font
|
|
57
|
+
title_text = title or f"Callout: {chart_id}"
|
|
58
|
+
|
|
59
|
+
title_measurer = get_font_measurer(title_rf.family)
|
|
60
|
+
message_measurer = get_font_measurer(message_rf.family)
|
|
61
|
+
title_lines = wrap_text_precise(
|
|
62
|
+
title_text,
|
|
63
|
+
content_width,
|
|
64
|
+
title_rf.size,
|
|
65
|
+
title_measurer,
|
|
66
|
+
max_lines=3,
|
|
67
|
+
ellipsis=True,
|
|
68
|
+
) or [title_text]
|
|
69
|
+
message_lines = wrap_text_precise(
|
|
70
|
+
message,
|
|
71
|
+
content_width,
|
|
72
|
+
message_rf.size,
|
|
73
|
+
message_measurer,
|
|
74
|
+
max_lines=_MAX_MESSAGE_LINES,
|
|
75
|
+
ellipsis=True,
|
|
76
|
+
) or [message]
|
|
77
|
+
|
|
78
|
+
# Optional extra lines: hint and doc_url
|
|
79
|
+
hint_lines: list[str] = []
|
|
80
|
+
if hint:
|
|
81
|
+
hint_lines = wrap_text_precise(
|
|
82
|
+
hint,
|
|
83
|
+
content_width,
|
|
84
|
+
message_rf.size,
|
|
85
|
+
message_measurer,
|
|
86
|
+
max_lines=3,
|
|
87
|
+
ellipsis=True,
|
|
88
|
+
) or [hint]
|
|
89
|
+
|
|
90
|
+
doc_url_lines: list[str] = []
|
|
91
|
+
if doc_url:
|
|
92
|
+
doc_url_lines = wrap_text_precise(
|
|
93
|
+
doc_url,
|
|
94
|
+
content_width,
|
|
95
|
+
message_rf.size * 0.85,
|
|
96
|
+
message_measurer,
|
|
97
|
+
max_lines=4,
|
|
98
|
+
ellipsis=True,
|
|
99
|
+
) or [doc_url]
|
|
100
|
+
|
|
101
|
+
title_line_height = title_rf.size * _LINE_HEIGHT_RATIO
|
|
102
|
+
message_line_height = message_rf.size * _LINE_HEIGHT_RATIO
|
|
103
|
+
doc_url_line_height = message_rf.size * 0.85 * _LINE_HEIGHT_RATIO
|
|
104
|
+
|
|
105
|
+
# Code badge height (small monospace text above title)
|
|
106
|
+
code_badge_height = (title_rf.size * 0.75 * _LINE_HEIGHT_RATIO) if code else 0.0
|
|
107
|
+
title_block_height = len(title_lines) * title_line_height
|
|
108
|
+
message_block_height = len(message_lines) * message_line_height
|
|
109
|
+
hint_block_height = len(hint_lines) * message_line_height if hint_lines else 0.0
|
|
110
|
+
doc_url_block_height = (
|
|
111
|
+
len(doc_url_lines) * doc_url_line_height if doc_url_lines else 0.0
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
natural_height = (
|
|
115
|
+
(2 * padding)
|
|
116
|
+
+ code_badge_height
|
|
117
|
+
+ (section_gap if code else 0.0)
|
|
118
|
+
+ title_block_height
|
|
119
|
+
+ section_gap
|
|
120
|
+
+ message_block_height
|
|
121
|
+
+ (section_gap + hint_block_height if hint_lines else 0.0)
|
|
122
|
+
+ (section_gap + doc_url_block_height if doc_url_lines else 0.0)
|
|
123
|
+
)
|
|
124
|
+
h = max(_MIN_HEIGHT, height or 0.0, natural_height)
|
|
125
|
+
|
|
126
|
+
current_y = padding
|
|
127
|
+
|
|
128
|
+
# Code badge
|
|
129
|
+
code_svg = ""
|
|
130
|
+
if code:
|
|
131
|
+
code_font_size = title_rf.size * 0.75
|
|
132
|
+
code_y = current_y + code_font_size
|
|
133
|
+
code_svg = (
|
|
134
|
+
f'<text x="{padding}" y="{code_y}" '
|
|
135
|
+
f'font-size="{code_font_size}" font-weight="normal" '
|
|
136
|
+
f'font-family="monospace" '
|
|
137
|
+
f'fill="{message_color}">{html.escape(code)}</text>'
|
|
138
|
+
)
|
|
139
|
+
current_y += code_badge_height + section_gap
|
|
140
|
+
|
|
141
|
+
title_start_y = current_y + title_rf.size + inline.title.y_offset
|
|
142
|
+
current_y = (
|
|
143
|
+
title_start_y
|
|
144
|
+
+ title_block_height
|
|
145
|
+
- title_rf.size
|
|
146
|
+
+ section_gap
|
|
147
|
+
+ inline.message.y_offset
|
|
148
|
+
)
|
|
149
|
+
message_start_y = current_y
|
|
150
|
+
|
|
151
|
+
title_lines_svg = "".join(
|
|
152
|
+
f'<text x="{padding}" y="{title_start_y + (index * title_line_height)}" '
|
|
153
|
+
f'font-size="{title_rf.size}" font-weight="{title_rf.weight}" '
|
|
154
|
+
f'font-family="{html.escape(title_rf.family)}" '
|
|
155
|
+
f'fill="{title_color}">{html.escape(line)}</text>'
|
|
156
|
+
for index, line in enumerate(title_lines)
|
|
157
|
+
)
|
|
158
|
+
message_lines_svg = "".join(
|
|
159
|
+
f'<text x="{padding}" y="{message_start_y + (index * message_line_height)}" '
|
|
160
|
+
f'font-size="{message_rf.size}" font-weight="{message_rf.weight}" '
|
|
161
|
+
f'font-family="{html.escape(message_rf.family)}" '
|
|
162
|
+
f'fill="{message_color}">{html.escape(line)}</text>'
|
|
163
|
+
for index, line in enumerate(message_lines)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
hint_svg = ""
|
|
167
|
+
if hint_lines:
|
|
168
|
+
hint_start_y = message_start_y + message_block_height + section_gap
|
|
169
|
+
hint_svg = "".join(
|
|
170
|
+
f'<text x="{padding}" y="{hint_start_y + (i * message_line_height)}" '
|
|
171
|
+
f'font-size="{message_rf.size}" font-weight="normal" font-style="italic" '
|
|
172
|
+
f'font-family="{html.escape(message_rf.family)}" '
|
|
173
|
+
f'fill="{message_color}">{html.escape(line)}</text>'
|
|
174
|
+
for i, line in enumerate(hint_lines)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
doc_svg = ""
|
|
178
|
+
if doc_url_lines:
|
|
179
|
+
doc_url_font_size = message_rf.size * 0.85
|
|
180
|
+
prev_height = message_block_height + (
|
|
181
|
+
section_gap + hint_block_height if hint_lines else 0.0
|
|
182
|
+
)
|
|
183
|
+
doc_start_y = message_start_y + prev_height + section_gap
|
|
184
|
+
doc_svg = "".join(
|
|
185
|
+
f'<text x="{padding}" y="{doc_start_y + (i * doc_url_line_height)}" '
|
|
186
|
+
f'font-size="{doc_url_font_size}" font-weight="normal" '
|
|
187
|
+
f'font-family="{html.escape(message_rf.family)}" '
|
|
188
|
+
f'fill="{message_color}">{html.escape(line)}</text>'
|
|
189
|
+
for i, line in enumerate(doc_url_lines)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
f'<svg xmlns="http://www.w3.org/2000/svg" class="dft-chart-callout" width="{w}" height="{h}" viewBox="0 0 {w} {h}">'
|
|
194
|
+
f'<rect x="0" y="0" width="{w}" height="{h}" '
|
|
195
|
+
f'fill="{background}" stroke="{border_color}" '
|
|
196
|
+
f'stroke-width="{inline.border.width}" rx="{inline.border.radius}"/>'
|
|
197
|
+
f"{code_svg}{title_lines_svg}{message_lines_svg}{hint_svg}{doc_svg}</svg>"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def render_callout_chart_svg(
|
|
202
|
+
chart: ResolvedChart,
|
|
203
|
+
data: list[dict[str, object]],
|
|
204
|
+
width: float | None = None,
|
|
205
|
+
height: float | None = None,
|
|
206
|
+
is_placeholder: bool = False,
|
|
207
|
+
*,
|
|
208
|
+
resolved_style: MergedStyle | None = None,
|
|
209
|
+
face_level: int = 1,
|
|
210
|
+
) -> str:
|
|
211
|
+
"""Render an authored ``type: callout`` chart."""
|
|
212
|
+
_ = data, is_placeholder, face_level
|
|
213
|
+
chart_id = chart.id
|
|
214
|
+
tone = chart.resolved_style.callout.tone or "negative"
|
|
215
|
+
title = chart.title or f"Callout: {chart_id}"
|
|
216
|
+
assert chart.message, "callout charts require a message"
|
|
217
|
+
return render_callout_svg(
|
|
218
|
+
chart_id=chart_id,
|
|
219
|
+
message=chart.message,
|
|
220
|
+
width=width or 320.0,
|
|
221
|
+
height=height,
|
|
222
|
+
title=title,
|
|
223
|
+
resolved_style=resolved_style,
|
|
224
|
+
tone=tone,
|
|
225
|
+
)
|