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.
Files changed (455) hide show
  1. d3_format/__init__.py +14 -0
  2. d3_format/errors.py +19 -0
  3. d3_format/format.py +551 -0
  4. d3_format/spec.py +159 -0
  5. dataface/DATAFACE_SYNTAX.md +1135 -0
  6. dataface/__init__.py +93 -0
  7. dataface/_docs_site.py +20 -0
  8. dataface/_install_hint.py +26 -0
  9. dataface/agent_api/__init__.py +79 -0
  10. dataface/agent_api/_init_templates/__init__.py +0 -0
  11. dataface/agent_api/_init_templates/agents_dft_snippet.md +26 -0
  12. dataface/agent_api/_init_templates/dataface.yml +15 -0
  13. dataface/agent_api/_init_templates/faces-dataface.yml +144 -0
  14. dataface/agent_api/_init_templates/index.md +24 -0
  15. dataface/agent_api/_paths.py +118 -0
  16. dataface/agent_api/_project_agents_md.py +43 -0
  17. dataface/agent_api/_session_store.py +486 -0
  18. dataface/agent_api/_state.py +28 -0
  19. dataface/agent_api/chat.py +221 -0
  20. dataface/agent_api/dashboards.py +257 -0
  21. dataface/agent_api/describe.py +366 -0
  22. dataface/agent_api/describe_query.py +120 -0
  23. dataface/agent_api/docs/__init__.py +25 -0
  24. dataface/agent_api/docs/_loader.py +292 -0
  25. dataface/agent_api/docs/yaml-reference.md +2757 -0
  26. dataface/agent_api/file_refs.py +118 -0
  27. dataface/agent_api/init.py +126 -0
  28. dataface/agent_api/inspect.py +128 -0
  29. dataface/agent_api/mcp_install.py +170 -0
  30. dataface/agent_api/query.py +274 -0
  31. dataface/agent_api/schema.py +658 -0
  32. dataface/agent_api/schema_search.py +284 -0
  33. dataface/agent_api/search.py +270 -0
  34. dataface/agent_api/skill_install.py +141 -0
  35. dataface/agent_api/skill_render.py +90 -0
  36. dataface/agent_api/skills.py +293 -0
  37. dataface/agent_api/surface_aliases.yaml +128 -0
  38. dataface/agent_api/validate.py +175 -0
  39. dataface/agent_api/validate_query.py +84 -0
  40. dataface/ai/__init__.py +39 -0
  41. dataface/ai/agent.py +139 -0
  42. dataface/ai/context.py +45 -0
  43. dataface/ai/events.py +62 -0
  44. dataface/ai/external_mcp.py +610 -0
  45. dataface/ai/generate_sql.py +96 -0
  46. dataface/ai/llm.py +403 -0
  47. dataface/ai/mcp/__init__.py +51 -0
  48. dataface/ai/mcp/server.py +289 -0
  49. dataface/ai/memories.py +85 -0
  50. dataface/ai/prompts.py +177 -0
  51. dataface/ai/schema_context.py +138 -0
  52. dataface/ai/skills/before-after-comparison/SKILL.md +102 -0
  53. dataface/ai/skills/before-after-comparison/examples/before-after-comparison.yml +24 -0
  54. dataface/ai/skills/dashboard-build/SKILL.md +212 -0
  55. dataface/ai/skills/dashboard-build/examples/_smoke.yml +15 -0
  56. dataface/ai/skills/dashboard-design/SKILL.md +182 -0
  57. dataface/ai/skills/dashboard-review/SKILL.md +113 -0
  58. dataface/ai/skills/dashboard-structural-review/SKILL.md +173 -0
  59. dataface/ai/skills/dashboard-visual-review/SKILL.md +139 -0
  60. dataface/ai/skills/dataface-mcp-setup/SKILL.md +177 -0
  61. dataface/ai/skills/dataface-troubleshooting/SKILL.md +225 -0
  62. dataface/ai/skills/drill-down-link/SKILL.md +112 -0
  63. dataface/ai/skills/drill-down-link/examples/drill-down-link.yml +27 -0
  64. dataface/ai/skills/faceted-small-multiples/SKILL.md +116 -0
  65. dataface/ai/skills/faceted-small-multiples/examples/faceted-small-multiples.yml +33 -0
  66. dataface/ai/skills/filter-bar-with-variables/SKILL.md +105 -0
  67. dataface/ai/skills/filter-bar-with-variables/examples/filter-bar-with-variables.yml +49 -0
  68. dataface/ai/skills/kpi-row/SKILL.md +101 -0
  69. dataface/ai/skills/kpi-row/examples/kpi-row.yml +55 -0
  70. dataface/ai/skills/report-design/SKILL.md +184 -0
  71. dataface/ai/skills/single-metric-bignum/SKILL.md +90 -0
  72. dataface/ai/skills/single-metric-bignum/examples/single-metric-bignum.yml +27 -0
  73. dataface/ai/skills/table-heavy-ops-dashboard/SKILL.md +114 -0
  74. dataface/ai/skills/table-heavy-ops-dashboard/examples/table-heavy-ops-dashboard.yml +48 -0
  75. dataface/ai/skills/time-series-trend/SKILL.md +93 -0
  76. dataface/ai/skills/time-series-trend/examples/time-series-trend.yml +26 -0
  77. dataface/ai/skills/top-n-with-detail/SKILL.md +98 -0
  78. dataface/ai/skills/top-n-with-detail/examples/top-n-with-detail.yml +45 -0
  79. dataface/ai/skills/two-by-two-grid-overview/SKILL.md +78 -0
  80. dataface/ai/skills/two-by-two-grid-overview/examples/two-by-two-grid-overview.yml +64 -0
  81. dataface/ai/tool_schemas.py +132 -0
  82. dataface/ai/tools/__init__.py +312 -0
  83. dataface/ai/yaml_utils.py +57 -0
  84. dataface/cli/__init__.py +3 -0
  85. dataface/cli/_console.py +48 -0
  86. dataface/cli/_error_format.py +83 -0
  87. dataface/cli/_extras.py +190 -0
  88. dataface/cli/_json_output.py +8 -0
  89. dataface/cli/_parsing.py +17 -0
  90. dataface/cli/_version_info.py +56 -0
  91. dataface/cli/commands/__init__.py +3 -0
  92. dataface/cli/commands/_agent_input.py +205 -0
  93. dataface/cli/commands/_agent_server.py +115 -0
  94. dataface/cli/commands/chat.py +645 -0
  95. dataface/cli/commands/describe.py +107 -0
  96. dataface/cli/commands/docs.py +131 -0
  97. dataface/cli/commands/extension.py +179 -0
  98. dataface/cli/commands/init.py +240 -0
  99. dataface/cli/commands/inspect.py +94 -0
  100. dataface/cli/commands/mcp_init.py +167 -0
  101. dataface/cli/commands/query.py +386 -0
  102. dataface/cli/commands/render.py +291 -0
  103. dataface/cli/commands/schema.py +411 -0
  104. dataface/cli/commands/search.py +49 -0
  105. dataface/cli/commands/serve.py +114 -0
  106. dataface/cli/commands/skills.py +133 -0
  107. dataface/cli/commands/skills_init.py +161 -0
  108. dataface/cli/commands/validate.py +63 -0
  109. dataface/cli/main.py +1501 -0
  110. dataface/core/__init__.py +75 -0
  111. dataface/core/compile/__init__.py +244 -0
  112. dataface/core/compile/_jinja_helpers.py +78 -0
  113. dataface/core/compile/channel.py +222 -0
  114. dataface/core/compile/chart_focus.py +101 -0
  115. dataface/core/compile/chart_resolved.py +169 -0
  116. dataface/core/compile/chart_type_detection.py +489 -0
  117. dataface/core/compile/chart_update.py +261 -0
  118. dataface/core/compile/colors.py +64 -0
  119. dataface/core/compile/compiler.py +904 -0
  120. dataface/core/compile/config.py +823 -0
  121. dataface/core/compile/custom_chart_types.py +208 -0
  122. dataface/core/compile/data_table_attachment.py +1287 -0
  123. dataface/core/compile/detect.py +110 -0
  124. dataface/core/compile/errors.py +302 -0
  125. dataface/core/compile/filter_injection.py +319 -0
  126. dataface/core/compile/introspection.py +527 -0
  127. dataface/core/compile/jinja.py +511 -0
  128. dataface/core/compile/labels_env.py +52 -0
  129. dataface/core/compile/markdown.py +154 -0
  130. dataface/core/compile/meta.py +388 -0
  131. dataface/core/compile/models/__init__.py +0 -0
  132. dataface/core/compile/models/chart/__init__.py +0 -0
  133. dataface/core/compile/models/chart/authored.py +2137 -0
  134. dataface/core/compile/models/chart/compiled.py +398 -0
  135. dataface/core/compile/models/config.py +347 -0
  136. dataface/core/compile/models/face/__init__.py +0 -0
  137. dataface/core/compile/models/face/authored.py +659 -0
  138. dataface/core/compile/models/face/compiled.py +522 -0
  139. dataface/core/compile/models/factories.py +201 -0
  140. dataface/core/compile/models/markers.py +40 -0
  141. dataface/core/compile/models/palette.py +36 -0
  142. dataface/core/compile/models/primitives.py +415 -0
  143. dataface/core/compile/models/query/__init__.py +0 -0
  144. dataface/core/compile/models/query/authored.py +246 -0
  145. dataface/core/compile/models/query/compiled.py +710 -0
  146. dataface/core/compile/models/refs.py +137 -0
  147. dataface/core/compile/models/source.py +611 -0
  148. dataface/core/compile/models/style/__init__.py +0 -0
  149. dataface/core/compile/models/style/authored.py +481 -0
  150. dataface/core/compile/models/style/compiled.py +3399 -0
  151. dataface/core/compile/models/style/merged.py +1682 -0
  152. dataface/core/compile/models/theme.py +362 -0
  153. dataface/core/compile/models/variable/__init__.py +0 -0
  154. dataface/core/compile/models/variable/authored.py +254 -0
  155. dataface/core/compile/models/vega_lite/__init__.py +0 -0
  156. dataface/core/compile/models/vega_lite/config.py +510 -0
  157. dataface/core/compile/models/vega_lite/contracts.py +171 -0
  158. dataface/core/compile/normalize_charts.py +494 -0
  159. dataface/core/compile/normalize_layout.py +1000 -0
  160. dataface/core/compile/normalize_queries.py +297 -0
  161. dataface/core/compile/normalize_variables.py +489 -0
  162. dataface/core/compile/normalizer.py +543 -0
  163. dataface/core/compile/palette.py +1100 -0
  164. dataface/core/compile/parameterized.py +658 -0
  165. dataface/core/compile/parser.py +228 -0
  166. dataface/core/compile/schema.py +20 -0
  167. dataface/core/compile/schema_renderers/__init__.py +0 -0
  168. dataface/core/compile/schema_renderers/json_schema.py +163 -0
  169. dataface/core/compile/schema_renderers/prompt.py +152 -0
  170. dataface/core/compile/schema_renderers/vscode_schema.py +301 -0
  171. dataface/core/compile/sizing.py +2126 -0
  172. dataface/core/compile/sources.py +518 -0
  173. dataface/core/compile/sql_authoring_lint.py +56 -0
  174. dataface/core/compile/style_cascade.py +471 -0
  175. dataface/core/compile/typography.py +299 -0
  176. dataface/core/compile/validator.py +301 -0
  177. dataface/core/compile/variables.py +53 -0
  178. dataface/core/compile/vega_config.py +98 -0
  179. dataface/core/compile/vega_lite/__init__.py +6 -0
  180. dataface/core/compile/vega_lite/validation.py +95 -0
  181. dataface/core/compile/yaml_error_formatter.py +838 -0
  182. dataface/core/connections.py +38 -0
  183. dataface/core/dashboard.py +358 -0
  184. dataface/core/defaults/default_config.yml +101 -0
  185. dataface/core/defaults/palettes/categorical/category-10-dark.yml +32 -0
  186. dataface/core/defaults/palettes/categorical/category-10-light.yml +43 -0
  187. dataface/core/defaults/palettes/categorical/category-10.yml +31 -0
  188. dataface/core/defaults/palettes/categorical/category-6-tonal-blue.yml +22 -0
  189. dataface/core/defaults/palettes/categorical/category-6-tonal-brown.yml +29 -0
  190. dataface/core/defaults/palettes/categorical/category-6-tonal-green.yml +20 -0
  191. dataface/core/defaults/palettes/categorical/category-6-tonal-orange.yml +21 -0
  192. dataface/core/defaults/palettes/categorical/category-6-tonal-purple.yml +20 -0
  193. dataface/core/defaults/palettes/categorical/editorial-10-dark.yml +32 -0
  194. dataface/core/defaults/palettes/categorical/editorial-10.yml +40 -0
  195. dataface/core/defaults/palettes/categorical/hero-6.yml +17 -0
  196. dataface/core/defaults/palettes/categorical/single-blue.yml +11 -0
  197. dataface/core/defaults/palettes/categorical/tableau.yml +20 -0
  198. dataface/core/defaults/palettes/data/xkcd_colors.json +3803 -0
  199. dataface/core/defaults/palettes/diverging/blue-red.yml +25 -0
  200. dataface/core/defaults/palettes/diverging/coolwarm.yml +24 -0
  201. dataface/core/defaults/palettes/diverging/crimson-green.yml +23 -0
  202. dataface/core/defaults/palettes/diverging/orange-teal.yml +23 -0
  203. dataface/core/defaults/palettes/diverging/sunset.yml +24 -0
  204. dataface/core/defaults/palettes/scaffold/dft-creams.yml +38 -0
  205. dataface/core/defaults/palettes/scaffold/dft-grays.yml +53 -0
  206. dataface/core/defaults/palettes/sequential/amber.yml +22 -0
  207. dataface/core/defaults/palettes/sequential/blue.yml +22 -0
  208. dataface/core/defaults/palettes/sequential/brown.yml +22 -0
  209. dataface/core/defaults/palettes/sequential/gray.yml +22 -0
  210. dataface/core/defaults/palettes/sequential/green.yml +22 -0
  211. dataface/core/defaults/palettes/sequential/purple.yml +22 -0
  212. dataface/core/defaults/palettes/sequential/rust.yml +22 -0
  213. dataface/core/defaults/palettes/sequential/teal.yml +22 -0
  214. dataface/core/defaults/palettes/tone/negative.yml +32 -0
  215. dataface/core/defaults/palettes/tone/positive.yml +22 -0
  216. dataface/core/defaults/palettes/tone/warning.yml +22 -0
  217. dataface/core/defaults/themes/_base.yaml +786 -0
  218. dataface/core/defaults/themes/bi.yaml +16 -0
  219. dataface/core/defaults/themes/carbong100.yaml +41 -0
  220. dataface/core/defaults/themes/cream.yaml +122 -0
  221. dataface/core/defaults/themes/dark.yaml +40 -0
  222. dataface/core/defaults/themes/diagnostics-title-angle-extreme.yaml +9 -0
  223. dataface/core/defaults/themes/diagnostics-title-baseline-extreme.yaml +9 -0
  224. dataface/core/defaults/themes/diagnostics-title-baseline.yaml +24 -0
  225. dataface/core/defaults/themes/diagnostics-title-center.yaml +8 -0
  226. dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +24 -0
  227. dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +25 -0
  228. dataface/core/defaults/themes/diagnostics-title-left.yaml +8 -0
  229. dataface/core/defaults/themes/diagnostics-title-offset-extreme.yaml +9 -0
  230. dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +24 -0
  231. dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +24 -0
  232. dataface/core/defaults/themes/editorial.yaml +147 -0
  233. dataface/core/defaults/themes/light.yaml +30 -0
  234. dataface/core/defaults/themes/looker.yaml +17 -0
  235. dataface/core/defaults/themes/stark.yaml +134 -0
  236. dataface/core/errors/__init__.py +67 -0
  237. dataface/core/errors/codes_compile.py +56 -0
  238. dataface/core/errors/codes_execute.py +177 -0
  239. dataface/core/errors/codes_render.py +106 -0
  240. dataface/core/errors/codes_unknown.py +15 -0
  241. dataface/core/errors/hints.py +74 -0
  242. dataface/core/errors/registry.py +42 -0
  243. dataface/core/errors/structured.py +92 -0
  244. dataface/core/execute/__init__.py +91 -0
  245. dataface/core/execute/adapters/__init__.py +49 -0
  246. dataface/core/execute/adapters/adapter_registry.py +400 -0
  247. dataface/core/execute/adapters/base.py +245 -0
  248. dataface/core/execute/adapters/csv_adapter.py +239 -0
  249. dataface/core/execute/adapters/dbt_adapter.py +283 -0
  250. dataface/core/execute/adapters/dbt_adapter_factory.py +212 -0
  251. dataface/core/execute/adapters/dbt_macro_loader.py +95 -0
  252. dataface/core/execute/adapters/dbt_utils.py +150 -0
  253. dataface/core/execute/adapters/http_adapter.py +224 -0
  254. dataface/core/execute/adapters/metricflow_adapter.py +94 -0
  255. dataface/core/execute/adapters/schema_resolver_adapter.py +144 -0
  256. dataface/core/execute/adapters/sql_adapter.py +710 -0
  257. dataface/core/execute/adapters/values_adapter.py +58 -0
  258. dataface/core/execute/batch.py +744 -0
  259. dataface/core/execute/cache_backend.py +135 -0
  260. dataface/core/execute/cache_keys.py +66 -0
  261. dataface/core/execute/dbt_jinja.py +21 -0
  262. dataface/core/execute/dialects/__init__.py +121 -0
  263. dataface/core/execute/dialects/athena.py +75 -0
  264. dataface/core/execute/dialects/base.py +302 -0
  265. dataface/core/execute/dialects/bigquery.py +38 -0
  266. dataface/core/execute/dialects/databricks.py +68 -0
  267. dataface/core/execute/dialects/duckdb.py +35 -0
  268. dataface/core/execute/dialects/mysql.py +68 -0
  269. dataface/core/execute/dialects/postgres.py +39 -0
  270. dataface/core/execute/dialects/redshift.py +12 -0
  271. dataface/core/execute/dialects/snowflake.py +51 -0
  272. dataface/core/execute/dialects/sqlserver.py +92 -0
  273. dataface/core/execute/duckdb_cache.py +712 -0
  274. dataface/core/execute/duckdb_config.py +26 -0
  275. dataface/core/execute/errors.py +213 -0
  276. dataface/core/execute/executor.py +1249 -0
  277. dataface/core/execute/parallel.py +162 -0
  278. dataface/core/execute/setup_sql.py +58 -0
  279. dataface/core/execute/source_registry.py +72 -0
  280. dataface/core/execute/source_resolver.py +255 -0
  281. dataface/core/execute/sql_guard.py +387 -0
  282. dataface/core/execute/sql_literals.py +199 -0
  283. dataface/core/fonts.py +52 -0
  284. dataface/core/inspect/__init__.py +32 -0
  285. dataface/core/inspect/cache_factory.py +98 -0
  286. dataface/core/inspect/db_types.py +162 -0
  287. dataface/core/inspect/dbt_schema.py +96 -0
  288. dataface/core/inspect/defaults.yml +37 -0
  289. dataface/core/inspect/fanout_risk.py +109 -0
  290. dataface/core/inspect/manifest_utils.py +77 -0
  291. dataface/core/inspect/partials/categorical.yml +40 -0
  292. dataface/core/inspect/partials/date.yml +40 -0
  293. dataface/core/inspect/partials/numeric.yml +55 -0
  294. dataface/core/inspect/partition_types.py +38 -0
  295. dataface/core/inspect/query_validator.py +975 -0
  296. dataface/core/inspect/renderer.py +354 -0
  297. dataface/core/inspect/resolver.py +808 -0
  298. dataface/core/inspect/search.py +461 -0
  299. dataface/core/inspect/sources/__init__.py +32 -0
  300. dataface/core/inspect/sources/dbt.py +738 -0
  301. dataface/core/inspect/sources/duckdb_utils.py +66 -0
  302. dataface/core/inspect/templates/__init__.py +1 -0
  303. dataface/core/inspect/templates/categorical_column.yml +196 -0
  304. dataface/core/inspect/templates/charts.yml +109 -0
  305. dataface/core/inspect/templates/date_column.yml +248 -0
  306. dataface/core/inspect/templates/model.yml +138 -0
  307. dataface/core/inspect/templates/numeric_column.yml +261 -0
  308. dataface/core/inspect/templates/quality.yml +80 -0
  309. dataface/core/inspect/templates/string_column.yml +263 -0
  310. dataface/core/project_roots.py +165 -0
  311. dataface/core/render/__init__.py +87 -0
  312. dataface/core/render/board_links.py +176 -0
  313. dataface/core/render/chart/__init__.py +27 -0
  314. dataface/core/render/chart/arc_attached_table.py +251 -0
  315. dataface/core/render/chart/artifacts.py +16 -0
  316. dataface/core/render/chart/callout.py +225 -0
  317. dataface/core/render/chart/decisions.py +358 -0
  318. dataface/core/render/chart/geo.py +700 -0
  319. dataface/core/render/chart/kpi.py +916 -0
  320. dataface/core/render/chart/labels.py +76 -0
  321. dataface/core/render/chart/pipeline.py +818 -0
  322. dataface/core/render/chart/presentation.py +36 -0
  323. dataface/core/render/chart/profile.py +3438 -0
  324. dataface/core/render/chart/render_single.py +347 -0
  325. dataface/core/render/chart/renderers.py +193 -0
  326. dataface/core/render/chart/rendering.py +565 -0
  327. dataface/core/render/chart/serialization.py +90 -0
  328. dataface/core/render/chart/spark.py +496 -0
  329. dataface/core/render/chart/spark_bar.py +370 -0
  330. dataface/core/render/chart/spec_builders.py +154 -0
  331. dataface/core/render/chart/standard_renderer.py +2645 -0
  332. dataface/core/render/chart/table.py +2957 -0
  333. dataface/core/render/chart/table_support.py +1452 -0
  334. dataface/core/render/chart/tick_values.py +66 -0
  335. dataface/core/render/chart/time_unit_detect.py +809 -0
  336. dataface/core/render/chart/title_overflow.py +157 -0
  337. dataface/core/render/chart/type_inference.py +122 -0
  338. dataface/core/render/chart/validation.py +99 -0
  339. dataface/core/render/chart/vega_lite.py +125 -0
  340. dataface/core/render/chart/vega_lite_types.py +268 -0
  341. dataface/core/render/chart/vl_field_maps.py +346 -0
  342. dataface/core/render/chart_interactivity.py +24 -0
  343. dataface/core/render/control_registry.py +287 -0
  344. dataface/core/render/converters/__init__.py +24 -0
  345. dataface/core/render/converters/chart.py +276 -0
  346. dataface/core/render/converters/html.py +98 -0
  347. dataface/core/render/converters/pdf.py +40 -0
  348. dataface/core/render/converters/png.py +41 -0
  349. dataface/core/render/errors.py +144 -0
  350. dataface/core/render/face_api.py +160 -0
  351. dataface/core/render/faces.py +1194 -0
  352. dataface/core/render/font_measurement.py +48 -0
  353. dataface/core/render/font_support.py +197 -0
  354. dataface/core/render/fonts/DFTSansTabular-Regular.ttf +0 -0
  355. dataface/core/render/fonts/DFTSansTabular-Regular.woff2 +0 -0
  356. dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.ttf +0 -0
  357. dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.ttf +0 -0
  358. dataface/core/render/fonts/InterVariable.ttf +0 -0
  359. dataface/core/render/fonts/InterVariable.woff2 +0 -0
  360. dataface/core/render/fonts/NOTO_COLOR_EMOJI_LICENSE.txt +93 -0
  361. dataface/core/render/fonts/NOTO_EMOJI_LICENSE.txt +93 -0
  362. dataface/core/render/fonts/NotoColorEmoji-Regular.ttf +0 -0
  363. dataface/core/render/fonts/NotoColorEmoji-Regular.woff2 +0 -0
  364. dataface/core/render/fonts/NotoEmoji-Regular.ttf +0 -0
  365. dataface/core/render/fonts/NotoEmoji-Regular.woff2 +0 -0
  366. dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -0
  367. dataface/core/render/fonts/SOURCE_SERIF_4_LICENSE.txt +98 -0
  368. dataface/core/render/fonts/SourceCodePro-Regular.ttf +0 -0
  369. dataface/core/render/fonts/SourceSerif4-Regular.ttf +0 -0
  370. dataface/core/render/fonts/_emoji_font_face.css +43 -0
  371. dataface/core/render/fonts/source-serif-4-variable-latin.woff2 +0 -0
  372. dataface/core/render/format_utils.py +329 -0
  373. dataface/core/render/geo_defaults.yml +28 -0
  374. dataface/core/render/json_format.py +146 -0
  375. dataface/core/render/layout_sizing.py +865 -0
  376. dataface/core/render/layouts.py +541 -0
  377. dataface/core/render/markdown_defaults.yml +16 -0
  378. dataface/core/render/missing_vars_prompt.py +79 -0
  379. dataface/core/render/placeholder.py +389 -0
  380. dataface/core/render/render_result.py +14 -0
  381. dataface/core/render/renderer.py +467 -0
  382. dataface/core/render/script_embedding.py +16 -0
  383. dataface/core/render/svg_utils.py +212 -0
  384. dataface/core/render/template_loader.py +69 -0
  385. dataface/core/render/templates/controls/_styles.css +606 -0
  386. dataface/core/render/templates/controls/checkbox.html +16 -0
  387. dataface/core/render/templates/controls/date.html +16 -0
  388. dataface/core/render/templates/controls/number.html +19 -0
  389. dataface/core/render/templates/controls/readonly.html +9 -0
  390. dataface/core/render/templates/controls/select.html +21 -0
  391. dataface/core/render/templates/controls/slider.html +22 -0
  392. dataface/core/render/templates/controls/text.html +16 -0
  393. dataface/core/render/templates/scripts/chart_interactivity.js +191 -0
  394. dataface/core/render/templates/scripts/variables.js +976 -0
  395. dataface/core/render/templates/svg/grid_pattern.svg +3 -0
  396. dataface/core/render/templates/svg/styles.css +51 -0
  397. dataface/core/render/terminal.py +311 -0
  398. dataface/core/render/terminal_charts.py +563 -0
  399. dataface/core/render/terminal_defaults.yml +2 -0
  400. dataface/core/render/terminal_layouts.py +299 -0
  401. dataface/core/render/terminal_text.py +31 -0
  402. dataface/core/render/text/__init__.py +1 -0
  403. dataface/core/render/text/case.py +113 -0
  404. dataface/core/render/text_format.py +129 -0
  405. dataface/core/render/utils.py +106 -0
  406. dataface/core/render/variable_controls.py +946 -0
  407. dataface/core/render/variable_input_refinement.py +140 -0
  408. dataface/core/render/warnings/__init__.py +15 -0
  409. dataface/core/render/warnings/bar_color_1_to_1_with_x.py +80 -0
  410. dataface/core/render/warnings/base.py +44 -0
  411. dataface/core/render/warnings/fanout_risk.py +15 -0
  412. dataface/core/render/warnings/from_query_diagnostic.py +56 -0
  413. dataface/core/render/warnings/missing_join_predicate.py +13 -0
  414. dataface/core/render/warnings/query_parse_error.py +14 -0
  415. dataface/core/render/warnings/query_returned_zero_rows.py +42 -0
  416. dataface/core/render/warnings/reaggregation.py +14 -0
  417. dataface/core/render/warnings/registry.py +45 -0
  418. dataface/core/render/warnings/suppression.py +46 -0
  419. dataface/core/render/warnings/temporal_single_point.py +63 -0
  420. dataface/core/render/warnings/unreferenced_chart.py +15 -0
  421. dataface/core/render/warnings/y_encoding_mostly_null.py +76 -0
  422. dataface/core/render/yaml_format.py +167 -0
  423. dataface/core/resolve_face.py +195 -0
  424. dataface/core/schema/__init__.py +0 -0
  425. dataface/core/schema/guidance.py +151 -0
  426. dataface/core/scoped_paths.py +59 -0
  427. dataface/core/serve/__init__.py +14 -0
  428. dataface/core/serve/bootstrap.py +39 -0
  429. dataface/core/serve/embedded.py +57 -0
  430. dataface/core/serve/port.py +129 -0
  431. dataface/core/serve/server.py +938 -0
  432. dataface/core/serve/templates/__init__.py +0 -0
  433. dataface/core/serve/templates/directory.yml +6 -0
  434. dataface/core/serve/templates/error.html.j2 +217 -0
  435. dataface/core/utils.py +121 -0
  436. dataface/core/validate.py +64 -0
  437. dataface/integrations/__init__.py +0 -0
  438. dataface/integrations/highlighting.py +351 -0
  439. dataface/integrations/markdown.py +537 -0
  440. dataface/py.typed +0 -0
  441. dataface-0.1.2.dist-info/METADATA +375 -0
  442. dataface-0.1.2.dist-info/RECORD +455 -0
  443. dataface-0.1.2.dist-info/WHEEL +4 -0
  444. dataface-0.1.2.dist-info/entry_points.txt +2 -0
  445. dataface-0.1.2.dist-info/licenses/LICENSE +202 -0
  446. mdsvg/__init__.py +168 -0
  447. mdsvg/fonts.py +656 -0
  448. mdsvg/images.py +299 -0
  449. mdsvg/parser.py +629 -0
  450. mdsvg/playground.py +284 -0
  451. mdsvg/py.typed +2 -0
  452. mdsvg/renderer.py +1623 -0
  453. mdsvg/style.py +355 -0
  454. mdsvg/types.py +200 -0
  455. 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]"
@@ -0,0 +1,2 @@
1
+ terminal:
2
+ max_labels: 20