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,494 @@
1
+ """Chart normalization and inline chart handling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from pydantic import BaseModel
10
+
11
+ from dataface.core.compile.custom_chart_types import BUILTIN_CHART_TYPE_VALUES
12
+ from dataface.core.compile.errors import CompilationError, ReferenceError
13
+ from dataface.core.compile.jinja import extract_variable_dependencies
14
+ from dataface.core.compile.models.chart.authored import (
15
+ VALID_LAYER_TYPES,
16
+ Layer,
17
+ )
18
+ from dataface.core.compile.models.chart.compiled import (
19
+ Chart,
20
+ )
21
+ from dataface.core.compile.models.face.compiled import Layout
22
+ from dataface.core.compile.models.query.compiled import AnyQuery
23
+ from dataface.core.compile.models.style.authored import ChartStylePatch
24
+ from dataface.core.compile.normalize_queries import (
25
+ normalize_query,
26
+ resolve_external_query,
27
+ )
28
+ from dataface.core.compile.parser import looks_like_sql
29
+ from dataface.core.utils import slug_to_text as _slug_to_text
30
+
31
+ if TYPE_CHECKING:
32
+ from dataface.core.compile.custom_chart_types import CustomChartTypeRegistry
33
+
34
+ # Chart type aliases: rewrite alternate names to canonical forms.
35
+ # Applied before any other normalization so all downstream code sees only
36
+ # canonical names. donut → pie is handled separately in _normalize_chart()
37
+ # because it also sets style.inner_radius=0.6, not just a type rename.
38
+ CHART_TYPE_ALIASES: dict[str, str] = {
39
+ "point": "scatter",
40
+ "choropleth": "map",
41
+ }
42
+
43
+ # Maps authored chart-type values to the sub-key on ChartStylePatch.
44
+ # Used both at normalize time (wrapping flat authored style into the monolithic
45
+ # shape) and at cascade/pipeline time (finding the family sub-patch for a
46
+ # given chart type).
47
+ _AUTHORED_FAMILY_TO_MONOLITHIC_KEY: dict[str, str] = {
48
+ "bar": "bar",
49
+ "histogram": "bar",
50
+ "line": "line",
51
+ "area": "area",
52
+ "scatter": "scatter",
53
+ "pie": "pie",
54
+ "donut": "pie",
55
+ "kpi": "kpi",
56
+ "table": "table",
57
+ "spark_bar": "spark_bar",
58
+ "heatmap": "heatmap",
59
+ "map": "geoshape",
60
+ "geoshape": "geoshape",
61
+ "point_map": "point_map",
62
+ "callout": "callout",
63
+ "layered": "layered",
64
+ }
65
+
66
+
67
+ def _wrap_authored_style_for_compiled(
68
+ chart_type: str | None, authored_style: dict[str, Any]
69
+ ) -> dict[str, Any]:
70
+ """Re-wrap a flat per-family authored style dict into the monolithic shape.
71
+
72
+ After the authored-side move to per-family styles, model_dump() produces
73
+ flat dicts (e.g. PieChartStylePatch → {"inner_radius": 0.3}). The compiled
74
+ Chart.style expects the monolithic ChartStylePatch shape with family sub-keys
75
+ (e.g. {"pie": {"inner_radius": 0.3}}). Layered charts use a flat
76
+ LayeredChartStyle that wraps to {"layered": {...}}.
77
+ Families without a monolithic slot (heatmap, geo) also pass through; any
78
+ family-specific fields they contain will be unknown to ChartStylePatch, but
79
+ those families have no authored chart-style extensions yet.
80
+ """
81
+ key = _AUTHORED_FAMILY_TO_MONOLITHIC_KEY.get(chart_type or "")
82
+ if key is None:
83
+ return authored_style
84
+ return {key: authored_style}
85
+
86
+
87
+ def _kpi_subtitle_error(chart_id: str | None) -> str:
88
+ where = f"KPI chart '{chart_id}' " if chart_id else "KPI charts "
89
+ return (
90
+ f"{where}uses `subtitle:` which is no longer part of the KPI "
91
+ "surface. Use the structured `support: {value, label}` block instead."
92
+ )
93
+
94
+
95
+ def _collect_charts_from_layout(
96
+ layout: Layout,
97
+ charts: dict[str, Chart],
98
+ ) -> None:
99
+ """Collect all inline charts from layout into the charts dict.
100
+
101
+ This normalizes the data model so ALL charts end up in face.charts,
102
+ making chart lookup a simple dict access (face.charts[chart_id])
103
+ instead of requiring layout traversal.
104
+
105
+ Args:
106
+ layout: The layout to extract charts from
107
+ charts: Dict to add charts to (modified in place)
108
+ """
109
+ for item in layout.items:
110
+ if item.type == "chart" and item.chart:
111
+ # Add inline chart to charts dict if not already there
112
+ chart_id = item.chart.id
113
+ if chart_id and chart_id not in charts:
114
+ charts[chart_id] = item.chart
115
+
116
+ elif item.type == "face" and item.face:
117
+ # Recurse into nested faces
118
+ _collect_charts_from_layout(item.face.layout, charts)
119
+ # Also collect charts from nested face's charts dict
120
+ if item.face.charts:
121
+ for chart_id, chart in item.face.charts.items():
122
+ if chart_id not in charts:
123
+ charts[chart_id] = chart
124
+
125
+
126
+ def _normalize_chart(
127
+ chart_id: str,
128
+ chart_def: Any,
129
+ query_registry: dict[str, AnyQuery],
130
+ base_path: Path | None = None,
131
+ default_source: str | None = None,
132
+ source_path: str | None = None,
133
+ custom_chart_types: CustomChartTypeRegistry | None = None,
134
+ ) -> Chart:
135
+ """Normalize a chart definition to Chart.
136
+
137
+ Supports external query references using the syntax:
138
+ `query: path/to/file.yml#query_name`
139
+
140
+ Args:
141
+ chart_id: Chart identifier
142
+ chart_def: Chart definition
143
+ query_registry: Query registry for resolution
144
+ base_path: Base path for resolving external file references
145
+ default_source: Default source to apply to inline queries
146
+ source_path: Path in YAML for edit-back support (e.g., "charts.revenue")
147
+
148
+ Returns:
149
+ Chart with resolved query
150
+
151
+ Raises:
152
+ ReferenceError: If chart references unknown query
153
+ """
154
+ if isinstance(chart_def, BaseModel):
155
+ # Covers all per-family *Patch classes (including CalloutChart which
156
+ # does not inherit _SharedChartFields but is still a BaseModel).
157
+ chart_dict = chart_def.model_dump(exclude_none=True)
158
+ elif isinstance(chart_def, dict):
159
+ chart_dict = dict(chart_def)
160
+ else:
161
+ raise CompilationError(f"Invalid chart definition: {chart_def}")
162
+
163
+ # Set ID
164
+ chart_dict["id"] = chart_id
165
+
166
+ # Auto-derive the chart-id slug into the appropriate slot. KPI charts
167
+ # use ``label`` (the text rendered above the headline value); every
168
+ # other chart type uses ``title`` (the section heading). Authoring an
169
+ # explicit empty value is honored so authors can opt out — only a
170
+ # missing key falls through to the slug.
171
+ raw_chart_type = chart_dict.get("type")
172
+ canonical_chart_type = (
173
+ CHART_TYPE_ALIASES.get(raw_chart_type, raw_chart_type)
174
+ if isinstance(raw_chart_type, str)
175
+ else raw_chart_type
176
+ )
177
+ if canonical_chart_type == "kpi":
178
+ if "label" not in chart_dict:
179
+ chart_dict["label"] = _slug_to_text(chart_id)
180
+ else:
181
+ if "title" not in chart_dict:
182
+ chart_dict["title"] = _slug_to_text(chart_id)
183
+
184
+ # Ensure description (might be None or missing)
185
+ if not chart_dict.get("description"):
186
+ chart_dict["description"] = ""
187
+ if not chart_dict.get("subtitle"):
188
+ chart_dict["subtitle"] = ""
189
+
190
+ # Rewrite chart type aliases to canonical forms
191
+ raw_type = chart_dict.get("type")
192
+ if raw_type in CHART_TYPE_ALIASES:
193
+ chart_dict["type"] = CHART_TYPE_ALIASES[raw_type]
194
+
195
+ # Set type to "auto" if not specified (enables auto-detection at render time)
196
+ if "type" not in chart_dict or chart_dict.get("type") is None:
197
+ chart_dict["type"] = "auto"
198
+
199
+ # Validate chart type against builtins + custom registry.
200
+ # Unknown types are always rejected. When a custom_chart_types
201
+ # registry is provided, registered custom types are also accepted.
202
+ _chart_type = chart_dict.get("type")
203
+ if _chart_type and _chart_type not in BUILTIN_CHART_TYPE_VALUES:
204
+ if not (custom_chart_types and custom_chart_types.is_registered(_chart_type)):
205
+ valid = sorted(
206
+ (BUILTIN_CHART_TYPE_VALUES | CHART_TYPE_ALIASES.keys()) - {"auto"}
207
+ )
208
+ msg = (
209
+ f"Chart '{chart_id}' has unknown type '{_chart_type}'. "
210
+ f"Valid built-in types: {valid}"
211
+ )
212
+ if custom_chart_types and (
213
+ custom := sorted(custom_chart_types.registered_names())
214
+ ):
215
+ msg += f". Registered custom types: {custom}"
216
+ raise CompilationError(msg)
217
+
218
+ # Validate layered chart syntax
219
+ if _chart_type == "layered":
220
+ raw_layers = chart_dict.get("layers")
221
+ if not raw_layers:
222
+ raise CompilationError(
223
+ f"Chart '{chart_id}' has type 'layered' but no `layers` list. "
224
+ "Layered charts require at least one layer, each with a `type`."
225
+ )
226
+ validated_layers: list[Layer] = []
227
+ layer_aliases = {
228
+ k for k, v in CHART_TYPE_ALIASES.items() if v in VALID_LAYER_TYPES
229
+ }
230
+ for i, layer in enumerate(raw_layers):
231
+ if not isinstance(layer, dict):
232
+ raise CompilationError(
233
+ f"Chart '{chart_id}', layer {i}: expected a dict, got {type(layer).__name__}"
234
+ )
235
+ layer_type = layer.get("type")
236
+ if not layer_type:
237
+ raise CompilationError(
238
+ f"Chart '{chart_id}', layer {i}: each layer must specify a `type` "
239
+ f"(e.g. bar, line, area). Valid layer types: {sorted(VALID_LAYER_TYPES | layer_aliases)}"
240
+ )
241
+ # Apply chart type aliases to layers too (point→scatter, etc.)
242
+ if layer_type in CHART_TYPE_ALIASES:
243
+ layer_type = CHART_TYPE_ALIASES[layer_type]
244
+ layer["type"] = layer_type
245
+ if layer_type == "layered":
246
+ raise CompilationError(
247
+ f"Chart '{chart_id}', layer {i}: nested `type: layered` is not allowed. "
248
+ "Use primitive mark types (bar, line, area, etc.) inside layers."
249
+ )
250
+ if layer_type not in VALID_LAYER_TYPES:
251
+ raise CompilationError(
252
+ f"Chart '{chart_id}', layer {i}: invalid layer type '{layer_type}'. "
253
+ f"Valid layer types: {sorted(VALID_LAYER_TYPES | layer_aliases)}"
254
+ )
255
+ # Validate per-layer query reference exists in query registry
256
+ layer_query = layer.get("query")
257
+ if layer_query is not None:
258
+ if not isinstance(layer_query, str):
259
+ raise CompilationError(
260
+ f"Chart '{chart_id}', layer {i}: layer `query` must be a "
261
+ "string reference to a named query or inline SQL, got "
262
+ f"{type(layer_query).__name__}"
263
+ )
264
+ if layer_query not in query_registry:
265
+ if looks_like_sql(layer_query):
266
+ layer_inline_name = f"_inline_query_{chart_id}_layer{i}"
267
+ query_registry[layer_inline_name] = normalize_query(
268
+ layer_inline_name,
269
+ {"sql": layer_query},
270
+ default_source=default_source,
271
+ )
272
+ layer["query"] = layer_inline_name
273
+ else:
274
+ raise ReferenceError(
275
+ layer_query, f"chart '{chart_id}', layer {i}"
276
+ )
277
+ validated_layers.append(Layer(**layer))
278
+ chart_dict["layers"] = validated_layers
279
+
280
+ # Donut → pie alias: donut is just a pie with style.inner_radius > 0.
281
+ # After the per-family authored style move, PieChartStylePatch.inner_radius
282
+ # is a flat field (no "pie" sub-key). Inject directly into the style dict.
283
+ # 0.6 = ~64% ring area / ~36% hole area, putting the data ring as the
284
+ # dominant element while leaving room for a center total — the editorial-
285
+ # donut sweet spot. Authors override via explicit style.inner_radius.
286
+ if chart_dict.get("type") == "donut":
287
+ chart_dict["type"] = "pie"
288
+ style = chart_dict.setdefault("style", {})
289
+ if style.get("inner_radius") is None:
290
+ style["inner_radius"] = 0.6
291
+
292
+ # Promote sizing sentinels from per-chart style to chart root.
293
+ # aspect_ratio, min_height, max_height are authored in style: but stored as
294
+ # flat fields on compiled Chart for sizing.py to read directly (no style walk).
295
+ # For single-family charts the fields live in the per-family patch (e.g.
296
+ # BarChartStylePatch); for layered charts they live at the top of
297
+ # ChartStylePatch. In both cases the raw dict has them at style[key] at
298
+ # this point, so the pop loop is uniform.
299
+ # kpi and table patch models exclude these fields, so they can never reach
300
+ # this step for those types; the promotion runs unconditionally for all others.
301
+ if isinstance(chart_dict.get("style"), dict):
302
+ flat_style = chart_dict["style"]
303
+ for _sizing_key in ("aspect_ratio", "min_height", "max_height"):
304
+ if _sizing_key in flat_style:
305
+ chart_dict[_sizing_key] = flat_style.pop(_sizing_key)
306
+
307
+ # Resolve query reference
308
+ query_value = chart_dict.get("query")
309
+ query_is_inline = False
310
+
311
+ # Convert the per-family flat authored style dict to the monolithic
312
+ # ChartStylePatch shape expected by compiled Chart.style, then validate.
313
+ if chart_dict.get("style") and isinstance(chart_dict["style"], dict):
314
+ monolithic = _wrap_authored_style_for_compiled(
315
+ chart_dict.get("type"), chart_dict["style"]
316
+ )
317
+ chart_dict["style"] = ChartStylePatch.model_validate(monolithic)
318
+
319
+ # Handle inline query definitions (query is a dict, not a string reference)
320
+ if query_value is None:
321
+ # Blank chart with no query - this is valid for placeholder charts
322
+ chart_dict["query"] = None
323
+ chart_dict["query_name"] = None
324
+ chart_dict["source_path"] = source_path
325
+ chart_dict["query_is_inline"] = False
326
+ return Chart(**chart_dict)
327
+ elif isinstance(query_value, dict):
328
+ # Create an inline query in the registry
329
+ inline_query_name = f"_inline_query_{chart_id}"
330
+ normalized_query = normalize_query(
331
+ inline_query_name, query_value, default_source=default_source
332
+ )
333
+ query_registry[inline_query_name] = normalized_query
334
+ query_ref = inline_query_name
335
+ query_is_inline = True
336
+ elif isinstance(query_value, str):
337
+ # String reference to existing query
338
+ query_ref = query_value
339
+ if query_ref.startswith("queries."):
340
+ query_ref = query_ref[8:]
341
+
342
+ # Check for external file reference (file.yml#query_name)
343
+ if "#" in query_ref:
344
+ query_ref, query_registry = resolve_external_query(
345
+ query_ref, query_registry, base_path
346
+ )
347
+
348
+ if query_ref not in query_registry:
349
+ if looks_like_sql(query_ref):
350
+ # Promote bare SQL string to an inline query (same as query: {sql: ...})
351
+ inline_query_name = f"_inline_query_{chart_id}"
352
+ normalized_query = normalize_query(
353
+ inline_query_name, {"sql": query_ref}, default_source=default_source
354
+ )
355
+ query_registry[inline_query_name] = normalized_query
356
+ query_ref = inline_query_name
357
+ query_is_inline = True
358
+ else:
359
+ raise ReferenceError(query_ref, f"chart '{chart_id}'")
360
+ else:
361
+ raise CompilationError(
362
+ f"Chart '{chart_id}' has invalid query field. Expected string reference or dict, got {type(query_value)}"
363
+ )
364
+
365
+ # Set query fields
366
+ resolved_query = query_registry[query_ref]
367
+ chart_dict["query"] = resolved_query
368
+ chart_dict["query_name"] = query_ref
369
+
370
+ # Compute variable dependencies (union of own deps + query deps)
371
+ chart_deps: set[str] = set()
372
+
373
+ # Own deps from title/subtitle
374
+ if chart_dict.get("title"):
375
+ chart_deps |= extract_variable_dependencies(chart_dict["title"])
376
+ if chart_dict.get("subtitle"):
377
+ chart_deps |= extract_variable_dependencies(chart_dict["subtitle"])
378
+
379
+ # Inherit query deps
380
+ if resolved_query.variable_dependencies:
381
+ chart_deps |= resolved_query.variable_dependencies
382
+
383
+ chart_dict["variable_dependencies"] = chart_deps
384
+
385
+ # Track source location and query type for edit-back support
386
+ chart_dict["source_path"] = source_path
387
+ chart_dict["query_is_inline"] = query_is_inline
388
+
389
+ chart = Chart(**chart_dict)
390
+
391
+ # Extract filter deps from typed FilterDef instances — single source of shape truth.
392
+ # Done post-validation so we read .var/.template directly without re-parsing raw forms.
393
+ if chart.filters:
394
+ filter_deps: set[str] = set()
395
+ for fd in chart.filters.values():
396
+ if fd.var is not None:
397
+ filter_deps.add(fd.var)
398
+ elif fd.template is not None:
399
+ filter_deps |= extract_variable_dependencies(fd.template)
400
+ if filter_deps:
401
+ chart = chart.model_copy(
402
+ update={"variable_dependencies": chart_deps | filter_deps}
403
+ )
404
+
405
+ # Resolve palette tokens in conditional_formatting color fields.
406
+ # ConditionalRule.background and .font.color are authored color literals
407
+ # that live outside the style tree, so they don't go through resolve_style().
408
+ # Walk them here, at compile time, so the renderer receives hex values.
409
+ if chart.conditional_formatting:
410
+ from dataface.core.compile.models.style.merged import _resolve_color_tokens
411
+
412
+ resolved_cf = {
413
+ col: _resolve_color_tokens(entry)
414
+ for col, entry in chart.conditional_formatting.items()
415
+ }
416
+ chart = chart.model_copy(update={"conditional_formatting": resolved_cf})
417
+
418
+ return chart
419
+
420
+
421
+ def _generate_inline_chart_id(chart_def: Any, fallback_id: str, used_ids: set) -> str:
422
+ """Generate a unique chart ID for an inline chart definition.
423
+
424
+ Uses the chart's authored display text (``label`` for KPI, ``title``
425
+ otherwise) if available; falls back to the provided ID. Handles
426
+ duplicates by appending a number.
427
+
428
+ Args:
429
+ chart_def: The chart definition dict
430
+ fallback_id: Fallback ID to use if no title (e.g., "row0")
431
+ used_ids: Set of already-used chart IDs (will be mutated)
432
+
433
+ Returns:
434
+ A unique chart ID
435
+ """
436
+
437
+ # KPI charts carry their authored display text in ``label`` (the slot
438
+ # rendered above the headline value); every other chart type uses
439
+ # ``title``. The auto-derive in _normalize_chart runs *after* this
440
+ # helper picks the inline ID, so we must read the right slot here too.
441
+ if isinstance(chart_def, dict):
442
+ raw_type = chart_def.get("type")
443
+ chart_type = (
444
+ CHART_TYPE_ALIASES.get(raw_type, raw_type)
445
+ if isinstance(raw_type, str)
446
+ else raw_type
447
+ )
448
+ title = (
449
+ chart_def.get("label") if chart_type == "kpi" else chart_def.get("title")
450
+ )
451
+ else:
452
+ chart_type = getattr(chart_def, "type", None)
453
+ if chart_type == "kpi":
454
+ title = getattr(chart_def, "label", None)
455
+ else:
456
+ title = getattr(chart_def, "title", None)
457
+
458
+ # Generate base slug
459
+ base_slug = _title_to_slug(title) if title else fallback_id
460
+
461
+ # Ensure uniqueness
462
+ if base_slug not in used_ids:
463
+ used_ids.add(base_slug)
464
+ return base_slug
465
+
466
+ # Append number to make unique
467
+ counter = 2
468
+ while f"{base_slug}-{counter}" in used_ids:
469
+ counter += 1
470
+ unique_id = f"{base_slug}-{counter}"
471
+ used_ids.add(unique_id)
472
+ return unique_id
473
+
474
+
475
+ def _title_to_slug(title: str) -> str:
476
+ """Convert a title to a URL-friendly slug.
477
+
478
+ Args:
479
+ title: Display title (e.g., "My First Chart")
480
+
481
+ Returns:
482
+ Slug string (e.g., "my-first-chart")
483
+ """
484
+ # Convert to lowercase
485
+ slug = title.lower()
486
+ # Replace spaces and underscores with hyphens
487
+ slug = slug.replace(" ", "-").replace("_", "-")
488
+ # Remove any characters that aren't alphanumeric or hyphens
489
+ slug = re.sub(r"[^a-z0-9-]", "", slug)
490
+ # Collapse multiple hyphens
491
+ slug = re.sub(r"-+", "-", slug)
492
+ # Strip leading/trailing hyphens
493
+ slug = slug.strip("-")
494
+ return slug or "chart"