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,489 @@
1
+ """Chart type auto-detection based on data structure.
2
+
3
+ Stage: RENDER (Runtime)
4
+ Purpose: Automatically select the most appropriate chart type based on data.
5
+
6
+ Entry Points:
7
+ - detect_chart_type(data, x_field, y_field, color_field, column_descriptions) -> (str, str)
8
+ - detect_chart_type_full(...) -> DetectionResult (includes field suggestions)
9
+
10
+ Detection considers:
11
+ - Data types: numeric, categorical, temporal, identifier
12
+ - Column count: chartable columns (excluding identifiers)
13
+ - Cardinality: distinct values in each column
14
+ - Column names: hints from naming conventions
15
+ - Database types: cursor.description when available (trusts DB over value parsing)
16
+
17
+ Dependencies:
18
+ - datetime (for temporal detection)
19
+ - re (for pattern matching)
20
+
21
+ See also:
22
+ - types.py: Chart type definitions
23
+ - render/chart_decisions.py: Uses db_type_to_dtype for DB type mapping
24
+ - render/vega_lite.py: Uses detected chart type for rendering
25
+ """
26
+
27
+ import re
28
+ from dataclasses import dataclass
29
+ from datetime import datetime
30
+ from typing import Any
31
+
32
+ # Chart type constants
33
+ CHART_TYPE_KPI = "kpi"
34
+ CHART_TYPE_LINE = "line"
35
+ CHART_TYPE_BAR = "bar"
36
+ CHART_TYPE_SCATTER = "scatter"
37
+ CHART_TYPE_TABLE = "table"
38
+ CHART_TYPE_HEATMAP = "heatmap"
39
+ CHART_TYPE_AREA = "area"
40
+ CHART_TYPE_PIE = "pie"
41
+
42
+ # Identifier column name patterns
43
+ _IDENTIFIER_PATTERNS = re.compile(
44
+ r"(^id$|_id$|_key$|_pk$|^pk$|^uuid$|^guid$|_uuid$|_guid$|^sk$|_sk$)",
45
+ re.IGNORECASE,
46
+ )
47
+
48
+ # DB types that are always identifiers regardless of name
49
+ _IDENTIFIER_DB_TYPES = frozenset({"UUID", "SERIAL", "BIGSERIAL", "SMALLSERIAL"})
50
+
51
+ # Chartable column threshold — above this, default to table
52
+ _MAX_CHARTABLE_COLUMNS = 8
53
+
54
+
55
+ @dataclass
56
+ class DetectionResult:
57
+ """Result of chart type auto-detection, including field suggestions."""
58
+
59
+ chart_type: str
60
+ reasoning: str
61
+ x: str | None = None
62
+ y: str | list[str] | None = None
63
+ color: str | None = None
64
+ value: str | None = None
65
+ theta: str | None = None
66
+
67
+
68
+ def detect_chart_type(
69
+ data: list[dict[str, Any]],
70
+ x_field: str | None = None,
71
+ y_field: str | None = None,
72
+ color_field: str | None = None,
73
+ value_field: str | None = None,
74
+ column_descriptions: dict[str, tuple] | None = None,
75
+ ) -> tuple[str, str]:
76
+ """Detect the most appropriate chart type based on data structure.
77
+
78
+ Returns (chart_type, reasoning).
79
+
80
+ Args:
81
+ data: Query result data as list of dicts
82
+ x_field: Optional x-axis field (if specified by user)
83
+ y_field: Optional y-axis field (if specified by user)
84
+ color_field: Optional color field (if specified by user)
85
+ value_field: Optional KPI value column reference (if specified by user)
86
+ column_descriptions: Optional PEP 249 cursor.description tuples keyed
87
+ by column name. Used for DB-type-aware classification.
88
+
89
+ Returns:
90
+ Tuple of (chart_type, reasoning)
91
+ """
92
+ result = detect_chart_type_full(
93
+ data, x_field, y_field, color_field, value_field, column_descriptions
94
+ )
95
+ return result.chart_type, result.reasoning
96
+
97
+
98
+ def detect_chart_type_full(
99
+ data: list[dict[str, Any]],
100
+ x_field: str | None = None,
101
+ y_field: str | None = None,
102
+ color_field: str | None = None,
103
+ value_field: str | None = None,
104
+ column_descriptions: dict[str, tuple] | None = None,
105
+ ) -> DetectionResult:
106
+ """Full detection returning chart type AND field suggestions.
107
+
108
+ This is the richer API that also suggests x/y/color/value/theta fields.
109
+ The simpler detect_chart_type() wraps it for callers that only need
110
+ the chart type and reasoning.
111
+ """
112
+ if not data:
113
+ return DetectionResult(CHART_TYPE_TABLE, "Empty data defaults to table display")
114
+
115
+ columns = list(data[0].keys())
116
+ column_types = _analyze_column_types(data, columns, column_descriptions)
117
+ column_cardinalities = _analyze_cardinality(data, columns)
118
+
119
+ identifier_cols = [c for c, t in column_types.items() if t == "identifier"]
120
+ numeric_cols = [c for c, t in column_types.items() if t == "numeric"]
121
+ temporal_cols = [c for c, t in column_types.items() if t == "temporal"]
122
+ categorical_cols = [c for c, t in column_types.items() if t == "categorical"]
123
+
124
+ chartable_cols = [c for c in columns if c not in identifier_cols]
125
+
126
+ # ════════════════════════════════════════════════════════════════════
127
+ # HEURISTIC 0: Many chartable columns → Table
128
+ # ════════════════════════════════════════════════════════════════════
129
+ if len(chartable_cols) > _MAX_CHARTABLE_COLUMNS:
130
+ return DetectionResult(
131
+ CHART_TYPE_TABLE,
132
+ f"Many chartable columns ({len(chartable_cols)}) suggests tabular display",
133
+ )
134
+
135
+ # ════════════════════════════════════════════════════════════════════
136
+ # HEURISTIC 1: Single row with single numeric value → KPI
137
+ # ════════════════════════════════════════════════════════════════════
138
+ if len(data) == 1 and len(numeric_cols) == 1 and len(chartable_cols) <= 2:
139
+ return DetectionResult(
140
+ CHART_TYPE_KPI,
141
+ "Single numeric value suggests a KPI/big number display",
142
+ value=numeric_cols[0],
143
+ )
144
+
145
+ if value_field and len(data) == 1:
146
+ return DetectionResult(
147
+ CHART_TYPE_KPI,
148
+ f"Value field '{value_field}' with single row suggests KPI",
149
+ value=value_field,
150
+ )
151
+
152
+ # ════════════════════════════════════════════════════════════════════
153
+ # HEURISTIC 2: User specified both x and y fields
154
+ # ════════════════════════════════════════════════════════════════════
155
+ if x_field and y_field:
156
+ x_type = column_types.get(x_field, "categorical")
157
+ y_type = column_types.get(y_field, "numeric")
158
+
159
+ if x_type == "temporal" and y_type == "numeric":
160
+ return DetectionResult(
161
+ CHART_TYPE_LINE,
162
+ f"Temporal x-axis ({x_field}) with numeric y-axis ({y_field}) suggests a line chart",
163
+ x=x_field,
164
+ y=y_field,
165
+ )
166
+
167
+ if x_type == "numeric" and y_type == "numeric":
168
+ return DetectionResult(
169
+ CHART_TYPE_SCATTER,
170
+ f"Two numeric columns ({x_field}, {y_field}) suggests a scatter plot",
171
+ x=x_field,
172
+ y=y_field,
173
+ )
174
+
175
+ if x_type in ("categorical", "identifier") and y_type == "numeric":
176
+ return DetectionResult(
177
+ CHART_TYPE_BAR,
178
+ f"Categorical x-axis ({x_field}) with numeric y-axis ({y_field}) suggests a bar chart",
179
+ x=x_field,
180
+ y=y_field,
181
+ )
182
+
183
+ # ════════════════════════════════════════════════════════════════════
184
+ # HEURISTIC 3: Infer from data structure when fields not specified
185
+ # ════════════════════════════════════════════════════════════════════
186
+
187
+ # Temporal + numeric → line chart (always line, never area)
188
+ if len(temporal_cols) >= 1 and len(numeric_cols) >= 1:
189
+ x_pick = temporal_cols[0]
190
+ y_pick: str | list[str] = (
191
+ numeric_cols[0] if len(numeric_cols) == 1 else numeric_cols
192
+ )
193
+ return DetectionResult(
194
+ CHART_TYPE_LINE,
195
+ f"Temporal column ({x_pick}) with numeric data suggests a line chart",
196
+ x=x_pick,
197
+ y=y_pick,
198
+ )
199
+
200
+ # Temporal + categorical + numeric → heatmap (e.g. weekday × hour)
201
+ if (
202
+ len(temporal_cols) >= 1
203
+ and len(categorical_cols) >= 1
204
+ and len(numeric_cols) >= 1
205
+ ):
206
+ t_card = column_cardinalities.get(temporal_cols[0], 0)
207
+ c_card = column_cardinalities.get(categorical_cols[0], 0)
208
+ if t_card <= 12 and c_card <= 12:
209
+ return DetectionResult(
210
+ CHART_TYPE_HEATMAP,
211
+ f"Temporal ({temporal_cols[0]}) x categorical ({categorical_cols[0]}) with measure suggests a heatmap",
212
+ x=temporal_cols[0],
213
+ y=categorical_cols[0],
214
+ )
215
+
216
+ # 2+ categorical + numeric → heatmap (both low cardinality) or bar
217
+ if len(categorical_cols) >= 2 and len(numeric_cols) >= 1:
218
+ cat1_card = column_cardinalities.get(categorical_cols[0], 0)
219
+ cat2_card = column_cardinalities.get(categorical_cols[1], 0)
220
+ if cat1_card <= 10 and cat2_card <= 10:
221
+ return DetectionResult(
222
+ CHART_TYPE_HEATMAP,
223
+ f"Two categorical dimensions ({categorical_cols[0]}, {categorical_cols[1]}) with numeric measure suggests a heatmap",
224
+ x=categorical_cols[0],
225
+ y=categorical_cols[1],
226
+ )
227
+ return DetectionResult(
228
+ CHART_TYPE_BAR,
229
+ "Multiple categories with numeric measure suggests a bar chart",
230
+ x=categorical_cols[0],
231
+ y=numeric_cols[0],
232
+ color=categorical_cols[1],
233
+ )
234
+
235
+ # 1 categorical + 1+ numeric → pie (3-6 values) or bar
236
+ if len(categorical_cols) == 1 and len(numeric_cols) >= 1:
237
+ cardinality = column_cardinalities.get(categorical_cols[0], 0)
238
+ if 3 <= cardinality <= 6:
239
+ return DetectionResult(
240
+ CHART_TYPE_PIE,
241
+ f"Low cardinality categorical ({categorical_cols[0]}) with {cardinality} values suggests a pie chart",
242
+ theta=numeric_cols[0],
243
+ color=categorical_cols[0],
244
+ )
245
+ if cardinality <= 20:
246
+ return DetectionResult(
247
+ CHART_TYPE_BAR,
248
+ f"Categorical column ({categorical_cols[0]}) with numeric data suggests a bar chart",
249
+ x=categorical_cols[0],
250
+ y=numeric_cols[0],
251
+ )
252
+
253
+ # 2+ numeric, no temporal → scatter
254
+ if len(numeric_cols) >= 2 and len(temporal_cols) == 0:
255
+ return DetectionResult(
256
+ CHART_TYPE_SCATTER,
257
+ f"Multiple numeric columns ({', '.join(numeric_cols[:2])}) suggests a scatter plot",
258
+ x=numeric_cols[0],
259
+ y=numeric_cols[1],
260
+ )
261
+
262
+ # ════════════════════════════════════════════════════════════════════
263
+ # FALLBACK
264
+ # ════════════════════════════════════════════════════════════════════
265
+ if len(numeric_cols) >= 1:
266
+ x_cand = categorical_cols[0] if categorical_cols else None
267
+ return DetectionResult(
268
+ CHART_TYPE_BAR,
269
+ "Default to bar chart for numeric data",
270
+ x=x_cand,
271
+ y=numeric_cols[0],
272
+ )
273
+
274
+ return DetectionResult(
275
+ CHART_TYPE_TABLE,
276
+ "Unable to determine optimal chart type, defaulting to table",
277
+ )
278
+
279
+
280
+ def _analyze_column_types(
281
+ data: list[dict[str, Any]],
282
+ columns: list[str],
283
+ column_descriptions: dict[str, tuple] | None = None,
284
+ ) -> dict[str, str]:
285
+ """Analyze and classify column types.
286
+
287
+ Categories: numeric, temporal, categorical, identifier.
288
+
289
+ When column_descriptions are available, database types take priority
290
+ over value-based heuristics (e.g. VARCHAR is always categorical even
291
+ if values look numeric like zip codes).
292
+ """
293
+ column_types: dict[str, str] = {}
294
+ row_count = len(data)
295
+
296
+ for col in columns:
297
+ sample_values = [row.get(col) for row in data[:100] if row.get(col) is not None]
298
+
299
+ # Layer 1: Use database type if available
300
+ db_dtype = None
301
+ db_type_str = None
302
+ if column_descriptions and col in column_descriptions:
303
+ desc = column_descriptions[col]
304
+ if desc and len(desc) > 1 and desc[1] is not None:
305
+ db_type_str = str(desc[1])
306
+ from dataface.core.render.chart.decisions import db_type_to_dtype
307
+
308
+ db_dtype = db_type_to_dtype(db_type_str)
309
+
310
+ # Layer 2: Value-based classification (fallback)
311
+ if db_dtype:
312
+ base_type = db_dtype
313
+ elif not sample_values:
314
+ base_type = "categorical"
315
+ else:
316
+ base_type = classify_column_type(col, sample_values, db_type_str)
317
+
318
+ # Layer 3: Identifier detection (overrides numeric/categorical)
319
+ if _is_identifier(col, base_type, sample_values, row_count, db_type_str):
320
+ column_types[col] = "identifier"
321
+ else:
322
+ column_types[col] = base_type
323
+
324
+ return column_types
325
+
326
+
327
+ def _is_identifier(
328
+ col_name: str,
329
+ base_type: str,
330
+ sample_values: list[Any],
331
+ row_count: int,
332
+ db_type_str: str | None = None,
333
+ ) -> bool:
334
+ """Detect if a column is an identifier (primary key, UUID, etc.).
335
+
336
+ Identifiers are excluded from chart axis candidates.
337
+ """
338
+ if db_type_str:
339
+ upper = db_type_str.upper().split("(")[0].strip()
340
+ if upper in _IDENTIFIER_DB_TYPES:
341
+ return True
342
+
343
+ if not _IDENTIFIER_PATTERNS.search(col_name):
344
+ return False
345
+
346
+ if not sample_values or base_type not in ("numeric", "categorical"):
347
+ return False
348
+
349
+ distinct = len({str(v) for v in sample_values})
350
+ sample_size = min(len(sample_values), row_count)
351
+ if sample_size == 0:
352
+ return False
353
+
354
+ return distinct / sample_size > 0.9
355
+
356
+
357
+ def classify_column_type(
358
+ column_name: str,
359
+ sample_values: list[Any],
360
+ db_type_str: str | None = None,
361
+ ) -> str:
362
+ """Classify a single column's type based on its values and name.
363
+
364
+ When db_type_str indicates VARCHAR/TEXT, string values are NOT parsed
365
+ as numbers — the database type is trusted.
366
+
367
+ Returns: "numeric", "temporal", or "categorical"
368
+ """
369
+ is_db_string = False
370
+ if db_type_str:
371
+ upper = db_type_str.upper().split("(")[0].strip()
372
+ if upper.startswith("VARCHAR") or upper in ("TEXT", "CHAR", "STRING", "ENUM"):
373
+ is_db_string = True
374
+
375
+ name_lower = column_name.lower()
376
+
377
+ temporal_patterns = [
378
+ r"date",
379
+ r"time",
380
+ r"timestamp",
381
+ r"created",
382
+ r"updated",
383
+ r"_at$",
384
+ r"day",
385
+ r"week",
386
+ r"month",
387
+ r"year",
388
+ r"quarter",
389
+ r"period",
390
+ ]
391
+ for pattern in temporal_patterns:
392
+ if re.search(pattern, name_lower) and any(
393
+ _is_temporal_value(v) for v in sample_values[:5]
394
+ ):
395
+ return "temporal"
396
+
397
+ numeric_count = 0
398
+ temporal_count = 0
399
+ categorical_count = 0
400
+
401
+ for val in sample_values:
402
+ if isinstance(val, bool):
403
+ categorical_count += 1
404
+ elif isinstance(val, (int, float)):
405
+ numeric_count += 1
406
+ elif isinstance(val, datetime):
407
+ temporal_count += 1
408
+ elif isinstance(val, str):
409
+ if _is_temporal_value(val):
410
+ temporal_count += 1
411
+ elif not is_db_string and _is_numeric_string(val):
412
+ numeric_count += 1
413
+ else:
414
+ categorical_count += 1
415
+ else:
416
+ categorical_count += 1
417
+
418
+ total = len(sample_values)
419
+ if total == 0:
420
+ return "categorical"
421
+
422
+ if numeric_count / total > 0.8:
423
+ return "numeric"
424
+ if temporal_count / total > 0.8:
425
+ return "temporal"
426
+
427
+ return "categorical"
428
+
429
+
430
+ def _is_temporal_value(value: Any) -> bool:
431
+ """Check if a value appears to be temporal (date/time).
432
+
433
+ Recognises standard date formats and common period strings:
434
+ - ISO dates: 2024-01-15, 2024/01/15
435
+ - US dates: 01/15/2024, 01-15-2024
436
+ - Text dates: Jan 15, 2024
437
+ - Year-months: 2024-01, Jan 2024
438
+ - Quarters: 2024-Q1, Q1 2024, 2024Q1
439
+ - Plain years: 2024 (4-digit, in plausible range)
440
+ """
441
+ if isinstance(value, datetime):
442
+ return True
443
+ if not isinstance(value, str):
444
+ return False
445
+ date_patterns = [
446
+ r"^\d{4}-\d{2}-\d{2}", # 2024-01-15
447
+ r"^\d{2}/\d{2}/\d{4}", # 01/15/2024
448
+ r"^\d{2}-\d{2}-\d{4}", # 01-15-2024
449
+ r"^\d{4}/\d{2}/\d{2}", # 2024/01/15
450
+ r"^\w{3}\s+\d{1,2},?\s+\d{4}", # Jan 15, 2024
451
+ r"^\d{4}-Q[1-4]$", # 2024-Q1
452
+ r"^Q[1-4]\s*\d{4}$", # Q1 2024, Q12024
453
+ r"^\d{4}Q[1-4]$", # 2024Q1
454
+ r"^\d{4}-\d{2}$", # 2024-01 (year-month)
455
+ r"^(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{4}$", # Jan 2024
456
+ ]
457
+ return any(re.match(pattern, value, re.IGNORECASE) for pattern in date_patterns)
458
+
459
+
460
+ def _is_numeric_string(value: str) -> bool:
461
+ """Check if a string value represents a number."""
462
+ try:
463
+ float(value.replace(",", "").replace("$", "").replace("%", ""))
464
+ return True
465
+ except (ValueError, AttributeError):
466
+ return False
467
+
468
+
469
+ def _analyze_cardinality(
470
+ data: list[dict[str, Any]],
471
+ columns: list[str],
472
+ ) -> dict[str, int]:
473
+ """Calculate cardinality (distinct value count) for each column."""
474
+ cardinalities: dict[str, int] = {}
475
+ for col in columns:
476
+ distinct_values = set()
477
+ for row in data:
478
+ val = row.get(col)
479
+ if val is not None:
480
+ distinct_values.add(str(val))
481
+ cardinalities[col] = len(distinct_values)
482
+ return cardinalities
483
+
484
+
485
+ def is_auto_chart_type(chart_type: str | None) -> bool:
486
+ """Check if chart type indicates auto-detection should be used."""
487
+ if chart_type is None:
488
+ return True
489
+ return chart_type.lower() == "auto"