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,563 @@
|
|
|
1
|
+
"""Terminal chart rendering using Plotext and Rich.
|
|
2
|
+
|
|
3
|
+
Stage: RENDER
|
|
4
|
+
Purpose: Convert Vega-Lite specifications to terminal-friendly charts.
|
|
5
|
+
|
|
6
|
+
This module handles the conversion from Vega-Lite chart specifications
|
|
7
|
+
to terminal output using Plotext for charts and Rich for tables.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from dataface.core.compile.channel import parse_style_channel
|
|
14
|
+
from dataface.core.compile.models.chart.compiled import (
|
|
15
|
+
Chart,
|
|
16
|
+
)
|
|
17
|
+
from dataface.core.render.errors import ChartDataError
|
|
18
|
+
from dataface.core.render.utils import normalize_data_types, slug_to_text
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _terminal_color_field(chart: Chart) -> str | None:
|
|
24
|
+
"""Return str color field for terminal use, or None for non-data-bound forms."""
|
|
25
|
+
color = chart.color
|
|
26
|
+
if color is None:
|
|
27
|
+
return None
|
|
28
|
+
ch = parse_style_channel(color, "color")
|
|
29
|
+
if ch.mode == "series":
|
|
30
|
+
return ch.data_field
|
|
31
|
+
if ch.mode == "literal":
|
|
32
|
+
return None
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"Terminal renderer does not support '{ch.mode}' color channels. "
|
|
35
|
+
"Use a string field name or {value: '#hex'} for a literal color."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _terminal_y_field(chart: Chart) -> str | None:
|
|
40
|
+
"""Return a single y field for terminal renderers that only support one series."""
|
|
41
|
+
if isinstance(chart.y, list):
|
|
42
|
+
if len(chart.y) > 1:
|
|
43
|
+
logger.warning(
|
|
44
|
+
"Terminal chart rendering only supports one y series; using '%s' and dropping %d additional series for chart '%s'.",
|
|
45
|
+
chart.y[0],
|
|
46
|
+
len(chart.y) - 1,
|
|
47
|
+
chart.id,
|
|
48
|
+
)
|
|
49
|
+
return chart.y[0] if chart.y else None
|
|
50
|
+
return chart.y
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def render_chart_terminal(
|
|
54
|
+
chart: Chart,
|
|
55
|
+
data: list[dict[str, Any]],
|
|
56
|
+
width: int | None = None,
|
|
57
|
+
height: int | None = None,
|
|
58
|
+
colors: bool = True,
|
|
59
|
+
) -> str:
|
|
60
|
+
"""Render a chart to terminal output.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
chart: Chart definition
|
|
64
|
+
data: List of dicts containing chart data
|
|
65
|
+
width: Optional terminal width in characters
|
|
66
|
+
height: Optional terminal height in characters
|
|
67
|
+
colors: Whether to use ANSI colors
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Terminal-formatted chart string
|
|
71
|
+
"""
|
|
72
|
+
chart_type: str = chart.type
|
|
73
|
+
|
|
74
|
+
# Map unsupported chart types to supported alternatives
|
|
75
|
+
# This allows all chart types to render in terminal mode
|
|
76
|
+
chart_type = _get_terminal_chart_type(chart_type)
|
|
77
|
+
|
|
78
|
+
# Handle special chart types
|
|
79
|
+
if chart_type == "table":
|
|
80
|
+
return render_table_terminal(chart, data, width=width)
|
|
81
|
+
elif chart_type == "kpi":
|
|
82
|
+
return render_kpi_terminal(chart, data)
|
|
83
|
+
# Normalize data types
|
|
84
|
+
normalized_data = normalize_data_types(data)
|
|
85
|
+
|
|
86
|
+
import plotext as plt
|
|
87
|
+
|
|
88
|
+
# Configure Plotext
|
|
89
|
+
plt.clear_data()
|
|
90
|
+
plt.clear_figure()
|
|
91
|
+
|
|
92
|
+
# Set dimensions (Plotext uses width, height)
|
|
93
|
+
# Use more conservative width to avoid overflow issues
|
|
94
|
+
# Plotext can have issues with very wide terminals, so cap at reasonable size
|
|
95
|
+
max_chart_width = min(
|
|
96
|
+
width - 8 if width else 72, 120
|
|
97
|
+
) # Account for borders and cap width
|
|
98
|
+
chart_height = height - 6 if height else 15 # Account for title and borders
|
|
99
|
+
|
|
100
|
+
if width and height:
|
|
101
|
+
plt.plotsize(max_chart_width, chart_height)
|
|
102
|
+
elif width:
|
|
103
|
+
plt.plotsize(max_chart_width, 15) # Default height if only width specified
|
|
104
|
+
elif height:
|
|
105
|
+
plt.plotsize(72, chart_height) # Default width if only height specified
|
|
106
|
+
else:
|
|
107
|
+
plt.plotsize(72, 15) # Default size
|
|
108
|
+
|
|
109
|
+
# Configure plotext for better terminal compatibility
|
|
110
|
+
# Disable theme to avoid background color issues
|
|
111
|
+
plt.theme("clear")
|
|
112
|
+
# Use simpler color scheme
|
|
113
|
+
if not colors:
|
|
114
|
+
plt.plotsize(max_chart_width if width else 72, chart_height if height else 15)
|
|
115
|
+
|
|
116
|
+
# Set title
|
|
117
|
+
if chart.title:
|
|
118
|
+
plt.title(chart.title)
|
|
119
|
+
|
|
120
|
+
# Render based on chart type
|
|
121
|
+
if chart_type in ("bar", "histogram"):
|
|
122
|
+
return _render_bar_chart_terminal(chart, normalized_data, plt)
|
|
123
|
+
elif chart_type == "line":
|
|
124
|
+
return _render_line_chart_terminal(chart, normalized_data, plt)
|
|
125
|
+
elif chart_type in ("circle", "scatter"):
|
|
126
|
+
return _render_scatter_chart_terminal(chart, normalized_data, plt, colors)
|
|
127
|
+
elif chart_type == "area":
|
|
128
|
+
return _render_area_chart_terminal(chart, normalized_data, plt)
|
|
129
|
+
else:
|
|
130
|
+
# This shouldn't happen since _get_terminal_chart_type maps all types
|
|
131
|
+
# but fallback to bar chart just in case
|
|
132
|
+
return _render_bar_chart_terminal(chart, normalized_data, plt)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _get_terminal_chart_type(chart_type: str) -> str:
|
|
136
|
+
"""Map chart types to terminal-supported equivalents.
|
|
137
|
+
|
|
138
|
+
Terminal mode supports: bar, line, scatter, area, table, kpi
|
|
139
|
+
All other chart types are mapped to their closest visual equivalent.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
chart_type: Original chart type from the chart definition
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Terminal-compatible chart type
|
|
146
|
+
"""
|
|
147
|
+
# Chart type mapping: unsupported -> supported equivalent
|
|
148
|
+
# The goal is to show *something* meaningful rather than an error
|
|
149
|
+
chart_type_map = {
|
|
150
|
+
# Direct support (no mapping needed)
|
|
151
|
+
"bar": "bar",
|
|
152
|
+
"histogram": "bar",
|
|
153
|
+
"line": "line",
|
|
154
|
+
"circle": "scatter",
|
|
155
|
+
"scatter": "scatter",
|
|
156
|
+
"area": "area",
|
|
157
|
+
"table": "table",
|
|
158
|
+
"kpi": "kpi",
|
|
159
|
+
# Pie charts -> bar chart (shows same breakdown, different viz)
|
|
160
|
+
"pie": "bar",
|
|
161
|
+
"arc": "bar",
|
|
162
|
+
# Heatmap -> table (shows the grid data)
|
|
163
|
+
"heatmap": "table",
|
|
164
|
+
# Boxplot -> bar (shows aggregated values)
|
|
165
|
+
"boxplot": "bar",
|
|
166
|
+
# Tick marks -> bar (scatter requires numeric x, tick often has categorical)
|
|
167
|
+
"tick": "bar",
|
|
168
|
+
# Rule (reference lines) -> bar (line requires numeric data)
|
|
169
|
+
"rule": "bar",
|
|
170
|
+
# Rect -> bar
|
|
171
|
+
"rect": "bar",
|
|
172
|
+
# Geoshape/Map -> table (can't render maps in terminal)
|
|
173
|
+
"geoshape": "table",
|
|
174
|
+
"map": "table",
|
|
175
|
+
"point_map": "table",
|
|
176
|
+
"bubble_map": "table",
|
|
177
|
+
# Trail -> line (trail is essentially a line with variable width)
|
|
178
|
+
"trail": "line",
|
|
179
|
+
# Square -> bar (scatter requires numeric x)
|
|
180
|
+
"square": "bar",
|
|
181
|
+
# Image -> table (can't render images in terminal)
|
|
182
|
+
"image": "table",
|
|
183
|
+
# Errorbar -> bar (show the main value)
|
|
184
|
+
"errorbar": "bar",
|
|
185
|
+
# Errorband -> area (show the main trend)
|
|
186
|
+
"errorband": "area",
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return chart_type_map.get(chart_type, "bar") # Default to bar for unknown types
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def render_table_terminal(
|
|
193
|
+
chart: Chart,
|
|
194
|
+
data: list[dict[str, Any]],
|
|
195
|
+
width: int | None = None,
|
|
196
|
+
) -> str:
|
|
197
|
+
"""Render a table chart using Rich.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
chart: Chart definition
|
|
201
|
+
data: Table data
|
|
202
|
+
width: Optional terminal width
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Terminal-formatted table string
|
|
206
|
+
"""
|
|
207
|
+
from rich import box as rich_box
|
|
208
|
+
from rich.console import Console
|
|
209
|
+
from rich.table import Table
|
|
210
|
+
|
|
211
|
+
console = Console(width=width, force_terminal=True, legacy_windows=False)
|
|
212
|
+
|
|
213
|
+
table = Table(
|
|
214
|
+
show_header=True, box=None if width and width < 80 else rich_box.SIMPLE
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if not data:
|
|
218
|
+
return _empty_table_output(chart)
|
|
219
|
+
|
|
220
|
+
# Get columns
|
|
221
|
+
columns = list(data[0].keys())
|
|
222
|
+
|
|
223
|
+
# Add columns
|
|
224
|
+
for col in columns:
|
|
225
|
+
# Format column name
|
|
226
|
+
display_name = slug_to_text(col)
|
|
227
|
+
table.add_column(display_name, overflow="ellipsis", no_wrap=True)
|
|
228
|
+
|
|
229
|
+
# Add rows
|
|
230
|
+
for row in data:
|
|
231
|
+
values = []
|
|
232
|
+
for col in columns:
|
|
233
|
+
value = row.get(col, "")
|
|
234
|
+
# Format value
|
|
235
|
+
if value is None:
|
|
236
|
+
display_value = "—"
|
|
237
|
+
elif isinstance(value, (int, float)):
|
|
238
|
+
if isinstance(value, float) and value == int(value):
|
|
239
|
+
display_value = str(int(value))
|
|
240
|
+
else:
|
|
241
|
+
display_value = (
|
|
242
|
+
f"{value:,.2f}" if isinstance(value, float) else f"{value:,}"
|
|
243
|
+
)
|
|
244
|
+
else:
|
|
245
|
+
display_value = str(value)
|
|
246
|
+
values.append(display_value)
|
|
247
|
+
table.add_row(*values)
|
|
248
|
+
|
|
249
|
+
# Render to string
|
|
250
|
+
with console.capture() as capture:
|
|
251
|
+
console.print(table)
|
|
252
|
+
output = capture.get()
|
|
253
|
+
|
|
254
|
+
# Add title if present
|
|
255
|
+
if chart.title:
|
|
256
|
+
return f"{chart.title}\n{output}"
|
|
257
|
+
return output
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def render_kpi_terminal(chart: Chart, data: list[dict[str, Any]]) -> str:
|
|
261
|
+
"""Render a KPI chart as text.
|
|
262
|
+
|
|
263
|
+
Reads the same `chart.value` contract as the SVG renderer: a column
|
|
264
|
+
reference (string column name). Raises ``ChartDataError`` when the
|
|
265
|
+
column is not present — same behavior as the SVG renderer.
|
|
266
|
+
|
|
267
|
+
The data-shape contract matches the SVG renderer: empty data and
|
|
268
|
+
multi-row results both raise ``ChartDataError`` so the same authoring
|
|
269
|
+
mistake fails the same way regardless of output medium.
|
|
270
|
+
"""
|
|
271
|
+
from dataface.core.render.chart.kpi import _resolve_value # noqa: PLC0415
|
|
272
|
+
|
|
273
|
+
chart_id = getattr(chart, "id", "unknown")
|
|
274
|
+
if not data:
|
|
275
|
+
raise ChartDataError(
|
|
276
|
+
f"KPI chart '{chart_id}' has no data — query returned 0 rows",
|
|
277
|
+
chart_id=chart_id,
|
|
278
|
+
)
|
|
279
|
+
if len(data) > 1:
|
|
280
|
+
raise ChartDataError(
|
|
281
|
+
f"KPI chart '{chart_id}' expects exactly 1 row, got {len(data)}. "
|
|
282
|
+
f"Use a query that returns a single row (e.g. SELECT SUM(...) or LIMIT 1).",
|
|
283
|
+
chart_id=chart_id,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
raw_value = chart.value
|
|
287
|
+
if raw_value is None:
|
|
288
|
+
raise ChartDataError(
|
|
289
|
+
f"KPI chart '{chart_id}' requires a 'value' field (column reference).",
|
|
290
|
+
chart_id=chart_id,
|
|
291
|
+
)
|
|
292
|
+
row = data[0]
|
|
293
|
+
cell, column_name = _resolve_value(raw_value, row, chart_id)
|
|
294
|
+
|
|
295
|
+
if isinstance(cell, (int, float)):
|
|
296
|
+
if isinstance(cell, float) and cell == int(cell):
|
|
297
|
+
display_value = f"{int(cell):,}"
|
|
298
|
+
else:
|
|
299
|
+
display_value = f"{cell:,.2f}"
|
|
300
|
+
else:
|
|
301
|
+
display_value = "" if cell is None else str(cell)
|
|
302
|
+
|
|
303
|
+
# KPI carries its authored text in ``label``; fall back to ``title`` (for
|
|
304
|
+
# error charts and any caller passing a non-KPI shape) and finally the
|
|
305
|
+
# column-name slug.
|
|
306
|
+
title_text = (
|
|
307
|
+
chart.label or chart.title or (slug_to_text(column_name) if column_name else "")
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
from rich.console import Console
|
|
311
|
+
from rich.text import Text
|
|
312
|
+
|
|
313
|
+
console = Console(force_terminal=True, legacy_windows=False)
|
|
314
|
+
text = Text()
|
|
315
|
+
if title_text:
|
|
316
|
+
text.append(title_text, style="bold")
|
|
317
|
+
text.append(": ", style="bold")
|
|
318
|
+
text.append(display_value, style="bold cyan")
|
|
319
|
+
|
|
320
|
+
with console.capture() as capture:
|
|
321
|
+
console.print(text)
|
|
322
|
+
return capture.get()
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _render_bar_chart_terminal(
|
|
326
|
+
chart: Chart,
|
|
327
|
+
data: list[dict[str, Any]],
|
|
328
|
+
plt: Any,
|
|
329
|
+
) -> str:
|
|
330
|
+
"""Render a bar chart using Plotext."""
|
|
331
|
+
x_field = chart.x
|
|
332
|
+
y_field = _terminal_y_field(chart)
|
|
333
|
+
|
|
334
|
+
# If no x/y fields, try to infer from data or render as table
|
|
335
|
+
if not x_field or not y_field:
|
|
336
|
+
# Try to find suitable fields from data
|
|
337
|
+
if data and len(data) > 0:
|
|
338
|
+
keys = list(data[0].keys())
|
|
339
|
+
# Try to find a numeric field for y
|
|
340
|
+
for key in keys:
|
|
341
|
+
try:
|
|
342
|
+
float(data[0].get(key, 0))
|
|
343
|
+
if y_field is None:
|
|
344
|
+
y_field = key
|
|
345
|
+
elif x_field is None:
|
|
346
|
+
x_field = key
|
|
347
|
+
except (ValueError, TypeError):
|
|
348
|
+
if x_field is None:
|
|
349
|
+
x_field = key
|
|
350
|
+
if not x_field or not y_field:
|
|
351
|
+
return _fallback_chart_render(chart, data)
|
|
352
|
+
else:
|
|
353
|
+
return _fallback_chart_render(chart, data)
|
|
354
|
+
|
|
355
|
+
# Extract data with error handling for non-numeric values
|
|
356
|
+
x_data = []
|
|
357
|
+
y_data = []
|
|
358
|
+
for row in data:
|
|
359
|
+
if x_field in row:
|
|
360
|
+
x_data.append(str(row.get(x_field, "")))
|
|
361
|
+
try:
|
|
362
|
+
y_val = float(row.get(y_field, 0))
|
|
363
|
+
y_data.append(y_val)
|
|
364
|
+
except (ValueError, TypeError):
|
|
365
|
+
# Non-numeric y value - skip this row
|
|
366
|
+
x_data.pop() # Remove the x value we just added
|
|
367
|
+
|
|
368
|
+
if not y_data:
|
|
369
|
+
return _fallback_chart_render(chart, data)
|
|
370
|
+
|
|
371
|
+
color_field = _terminal_color_field(chart)
|
|
372
|
+
if color_field and data and color_field in data[0]:
|
|
373
|
+
# Group by color - renders as simple bars (grouped rendering not yet implemented)
|
|
374
|
+
plt.bar(y_data)
|
|
375
|
+
else:
|
|
376
|
+
plt.bar(y_data)
|
|
377
|
+
|
|
378
|
+
# Set labels
|
|
379
|
+
if x_data and len(x_data) == len(y_data):
|
|
380
|
+
# Limit x-axis labels to avoid overflow (from config)
|
|
381
|
+
from dataface.core.compile.config import get_terminal_config
|
|
382
|
+
|
|
383
|
+
max_labels = get_terminal_config().max_labels
|
|
384
|
+
if len(x_data) > max_labels:
|
|
385
|
+
step = len(x_data) // max_labels
|
|
386
|
+
tick_positions = list(range(0, len(x_data), step))
|
|
387
|
+
tick_labels = [x_data[i] for i in tick_positions]
|
|
388
|
+
plt.xticks(tick_positions, tick_labels)
|
|
389
|
+
else:
|
|
390
|
+
plt.xticks(list(range(len(x_data))), x_data)
|
|
391
|
+
|
|
392
|
+
# Build chart and clean up output
|
|
393
|
+
chart_output = plt.build()
|
|
394
|
+
# Remove trailing whitespace and normalize line endings
|
|
395
|
+
lines = chart_output.split("\n")
|
|
396
|
+
cleaned_lines = [line.rstrip() for line in lines]
|
|
397
|
+
return "\n".join(cleaned_lines)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _render_line_chart_terminal(
|
|
401
|
+
chart: Chart,
|
|
402
|
+
data: list[dict[str, Any]],
|
|
403
|
+
plt: Any,
|
|
404
|
+
) -> str:
|
|
405
|
+
"""Render a line chart using Plotext."""
|
|
406
|
+
x_field = chart.x
|
|
407
|
+
y_field = _terminal_y_field(chart)
|
|
408
|
+
|
|
409
|
+
if not x_field or not y_field:
|
|
410
|
+
return _fallback_chart_render(chart, data)
|
|
411
|
+
|
|
412
|
+
# Extract and sort data with error handling
|
|
413
|
+
rows = []
|
|
414
|
+
for row in data:
|
|
415
|
+
try:
|
|
416
|
+
x_val = row.get(x_field)
|
|
417
|
+
y_val = float(row.get(y_field, 0))
|
|
418
|
+
rows.append((x_val, y_val))
|
|
419
|
+
except (ValueError, TypeError):
|
|
420
|
+
# Skip rows with non-numeric y values
|
|
421
|
+
pass
|
|
422
|
+
|
|
423
|
+
if not rows:
|
|
424
|
+
return _fallback_chart_render(chart, data)
|
|
425
|
+
|
|
426
|
+
rows.sort(key=lambda x: (str(x[0]) if x[0] is not None else ""))
|
|
427
|
+
|
|
428
|
+
x_data = [str(row[0]) if row[0] is not None else "" for row in rows]
|
|
429
|
+
y_data = [row[1] for row in rows]
|
|
430
|
+
|
|
431
|
+
color_field = _terminal_color_field(chart)
|
|
432
|
+
if color_field and color_field in data[0]:
|
|
433
|
+
# Group by color
|
|
434
|
+
color_groups: dict[str, list[tuple]] = {}
|
|
435
|
+
for row in data:
|
|
436
|
+
color_val = str(row.get(color_field, ""))
|
|
437
|
+
x_val = row.get(x_field)
|
|
438
|
+
y_val = float(row.get(y_field, 0))
|
|
439
|
+
if color_val not in color_groups:
|
|
440
|
+
color_groups[color_val] = []
|
|
441
|
+
color_groups[color_val].append((x_val, y_val))
|
|
442
|
+
|
|
443
|
+
# Render multiple lines
|
|
444
|
+
for color_val, points in color_groups.items():
|
|
445
|
+
points.sort(key=lambda x: (str(x[0]) if x[0] is not None else ""))
|
|
446
|
+
y_vals = [p[1] for p in points]
|
|
447
|
+
plt.plot(y_vals, label=color_val)
|
|
448
|
+
else:
|
|
449
|
+
plt.plot(y_data)
|
|
450
|
+
|
|
451
|
+
# Set labels
|
|
452
|
+
if x_data:
|
|
453
|
+
# Limit x-axis labels to avoid overflow - plotext can struggle with many labels
|
|
454
|
+
from dataface.core.compile.config import get_terminal_config
|
|
455
|
+
|
|
456
|
+
max_labels = get_terminal_config().max_labels
|
|
457
|
+
if len(x_data) > max_labels:
|
|
458
|
+
# Show every Nth label
|
|
459
|
+
step = len(x_data) // max_labels
|
|
460
|
+
tick_positions = list(range(0, len(x_data), step))
|
|
461
|
+
tick_labels = [x_data[i] for i in tick_positions]
|
|
462
|
+
plt.xticks(tick_positions, tick_labels)
|
|
463
|
+
else:
|
|
464
|
+
plt.xticks(list(range(len(x_data))), x_data)
|
|
465
|
+
|
|
466
|
+
# Build chart and clean up output
|
|
467
|
+
chart_output = plt.build()
|
|
468
|
+
# Remove any problematic control characters but keep ANSI color codes
|
|
469
|
+
# Strip trailing whitespace and normalize line endings
|
|
470
|
+
lines = chart_output.split("\n")
|
|
471
|
+
cleaned_lines = [line.rstrip() for line in lines]
|
|
472
|
+
return "\n".join(cleaned_lines)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _render_scatter_chart_terminal(
|
|
476
|
+
chart: Chart,
|
|
477
|
+
data: list[dict[str, Any]],
|
|
478
|
+
plt: Any,
|
|
479
|
+
colors: bool,
|
|
480
|
+
) -> str:
|
|
481
|
+
"""Render a scatter plot using Plotext."""
|
|
482
|
+
x_field = chart.x
|
|
483
|
+
y_field = _terminal_y_field(chart)
|
|
484
|
+
|
|
485
|
+
if not x_field or not y_field:
|
|
486
|
+
return _fallback_chart_render(chart, data)
|
|
487
|
+
|
|
488
|
+
# Extract data - try to convert to numeric, fall back to bar chart if not possible
|
|
489
|
+
x_data = []
|
|
490
|
+
y_data = []
|
|
491
|
+
for row in data:
|
|
492
|
+
if x_field in row and y_field in row:
|
|
493
|
+
try:
|
|
494
|
+
x_val = float(row.get(x_field, 0))
|
|
495
|
+
y_val = float(row.get(y_field, 0))
|
|
496
|
+
x_data.append(x_val)
|
|
497
|
+
y_data.append(y_val)
|
|
498
|
+
except (ValueError, TypeError):
|
|
499
|
+
# Non-numeric x values - fall back to bar chart
|
|
500
|
+
return _render_bar_chart_terminal(chart, data, plt)
|
|
501
|
+
|
|
502
|
+
if not x_data or not y_data:
|
|
503
|
+
return _fallback_chart_render(chart, data)
|
|
504
|
+
|
|
505
|
+
plt.scatter(x_data, y_data)
|
|
506
|
+
# Build chart and clean up output
|
|
507
|
+
chart_output = plt.build()
|
|
508
|
+
lines = chart_output.split("\n")
|
|
509
|
+
cleaned_lines = [line.rstrip() for line in lines]
|
|
510
|
+
return "\n".join(cleaned_lines)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _render_area_chart_terminal(
|
|
514
|
+
chart: Chart,
|
|
515
|
+
data: list[dict[str, Any]],
|
|
516
|
+
plt: Any,
|
|
517
|
+
) -> str:
|
|
518
|
+
"""Render an area chart using Plotext (as filled line)."""
|
|
519
|
+
x_field = chart.x
|
|
520
|
+
y_field = _terminal_y_field(chart)
|
|
521
|
+
|
|
522
|
+
if not x_field or not y_field:
|
|
523
|
+
return _fallback_chart_render(chart, data)
|
|
524
|
+
|
|
525
|
+
# Extract and sort data with error handling
|
|
526
|
+
rows = []
|
|
527
|
+
for row in data:
|
|
528
|
+
try:
|
|
529
|
+
x_val = row.get(x_field)
|
|
530
|
+
y_val = float(row.get(y_field, 0))
|
|
531
|
+
rows.append((x_val, y_val))
|
|
532
|
+
except (ValueError, TypeError):
|
|
533
|
+
# Skip rows with non-numeric y values
|
|
534
|
+
pass
|
|
535
|
+
|
|
536
|
+
if not rows:
|
|
537
|
+
return _fallback_chart_render(chart, data)
|
|
538
|
+
|
|
539
|
+
rows.sort(key=lambda x: (str(x[0]) if x[0] is not None else ""))
|
|
540
|
+
|
|
541
|
+
x_data = [str(row[0]) if row[0] is not None else "" for row in rows]
|
|
542
|
+
y_data = [row[1] for row in rows]
|
|
543
|
+
|
|
544
|
+
# Plotext doesn't have area charts, use filled line
|
|
545
|
+
plt.plot(y_data, fillx=True)
|
|
546
|
+
if x_data:
|
|
547
|
+
plt.xticks(list(range(len(x_data))), x_data)
|
|
548
|
+
|
|
549
|
+
return str(plt.build())
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _fallback_chart_render(chart: Chart, data: list[dict[str, Any]]) -> str:
|
|
553
|
+
"""Fallback rendering when Plotext is not available."""
|
|
554
|
+
title = chart.title or "Chart"
|
|
555
|
+
if data:
|
|
556
|
+
return f"{title}\n[Chart rendering not available - install plotext]"
|
|
557
|
+
return f"{title}\n[No data]"
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _empty_table_output(chart: Chart) -> str:
|
|
561
|
+
"""Output for empty table."""
|
|
562
|
+
title = chart.title or "Table"
|
|
563
|
+
return f"{title}\n[No data]"
|