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,565 @@
1
+ """Chart and layout-item rendering entrypoints.
2
+
3
+ This module owns:
4
+ - executing a chart query
5
+ - resolving the chart into render-ready semantics
6
+ - dispatching to chart vs nested-face rendering
7
+
8
+ UI chrome such as menus is handled outside core rendering.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import html
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ from dataface.core.compile.config import get_config
17
+ from dataface.core.compile.errors import DatafaceError
18
+ from dataface.core.compile.models.chart.compiled import (
19
+ SVG_LAYOUT_PADDED_TYPES,
20
+ Chart,
21
+ )
22
+ from dataface.core.compile.models.face.compiled import (
23
+ Face,
24
+ LayoutItem,
25
+ MergedFace,
26
+ ResolvedLayoutItem,
27
+ VariableValues,
28
+ )
29
+ from dataface.core.compile.models.style.merged import MergedStyle
30
+ from dataface.core.errors import DF_RENDER_INTERNAL, StructuredError
31
+ from dataface.core.execute.executor import Executor
32
+ from dataface.core.render.chart.spec_builders import additive_padding
33
+
34
+ if TYPE_CHECKING:
35
+ from dataface.core.execute.adapters.base import SchemaStatus
36
+ from dataface.core.render.svg_utils import (
37
+ _px,
38
+ extract_svg_dimensions,
39
+ extract_svg_inner_content,
40
+ )
41
+ from dataface.core.render.variable_controls import evaluate_visible
42
+
43
+ __all__ = [
44
+ "render_layout_item",
45
+ "render_chart_item",
46
+ ]
47
+
48
+
49
+ def _uses_svg_family_layout_padding(chart_type: str) -> bool:
50
+ """Return whether the chart should be inset by layout card padding."""
51
+ return chart_type in SVG_LAYOUT_PADDED_TYPES
52
+
53
+
54
+ def _ensure_resolved_face(face: Face | MergedFace) -> MergedFace:
55
+ """Return the face as a MergedFace, resolving it if it's a Face."""
56
+ if isinstance(face, MergedFace):
57
+ return face
58
+ from dataface.core.resolve_face import resolve_nested_face
59
+
60
+ return resolve_nested_face(face)
61
+
62
+
63
+ def _coerce_border_radius(value: object) -> float:
64
+ """Return a numeric border radius from numeric or CSS-ish values."""
65
+ if value is None:
66
+ return 0.0
67
+ if isinstance(value, (int, float)):
68
+ return float(value)
69
+
70
+ text = str(value).strip()
71
+ if not text:
72
+ return 0.0
73
+ if text.endswith("px"):
74
+ text = text[:-2].strip()
75
+
76
+ try:
77
+ return float(text)
78
+ except ValueError as exc:
79
+ raise ValueError(f"Invalid border_radius value: {value!r}") from exc
80
+
81
+
82
+ def render_layout_item(
83
+ item: LayoutItem | ResolvedLayoutItem,
84
+ executor: Executor,
85
+ variables: VariableValues,
86
+ card_gap: float,
87
+ available_width: float,
88
+ available_height: float = 300.0,
89
+ gap: float = 0.0,
90
+ resolved_style: MergedStyle | None = None,
91
+ interactive: bool = True,
92
+ render_cache: dict[tuple[str, float, float], tuple[str, float]] | None = None,
93
+ error_collector: list[StructuredError] | None = None,
94
+ face_level: int = 1,
95
+ vega_config: dict[str, Any] | None = None,
96
+ ) -> tuple[str, float]:
97
+ """Render a single layout item (chart, nested face, or details section).
98
+
99
+ If the item has details metadata, renders a collapsible section:
100
+ - Summary bar (always rendered, clickable)
101
+ - Content (only rendered when the details variable is true)
102
+
103
+ Args:
104
+ item: Layout item to render
105
+ executor: Executor for query execution
106
+ variables: Variable values for queries
107
+ available_width: Width in pixels for the item
108
+ available_height: Height in pixels for the item
109
+ resolved_style: Board-scoped MergedStyle for effective chart style.
110
+ interactive: Whether to render interactive variable controls (foreignObject).
111
+ face_level: Heading level of the parent face (root=1, nested=2, …).
112
+
113
+ Returns:
114
+ (svg_string, actual_height) — actual_height is the true rendered height.
115
+ """
116
+ # Evaluate visible expression before doing any work — hidden items cost nothing.
117
+ if not evaluate_visible(item.visible, variables, executor):
118
+ return "", 0.0
119
+
120
+ # Use calculated dimensions if available, otherwise use passed dimensions
121
+ width = item.width if item.width > 0 else available_width
122
+ height = item.height if item.height > 0 else available_height
123
+
124
+ # Details (collapsible section): render summary bar + conditionally render content
125
+ if item.details_variable and item.details_summary:
126
+ from dataface.core.compile.models.style.merged import resolve_style
127
+ from dataface.core.render.layouts import (
128
+ is_details_expanded,
129
+ render_details_summary,
130
+ )
131
+
132
+ resolved_style = resolved_style or resolve_style(get_config().style)
133
+ details_config = resolved_style.layout.details
134
+ summary_height = float(details_config.summary_height)
135
+
136
+ expanded = is_details_expanded(item, variables)
137
+ summary_svg = render_details_summary(
138
+ item,
139
+ variables,
140
+ width,
141
+ expanded=expanded,
142
+ resolved_style=resolved_style, # keyword-only, required
143
+ )
144
+
145
+ if expanded and item.face:
146
+ from dataface.core.compile.sizing import details_chrome_height
147
+ from dataface.core.render.faces import render_nested_face
148
+
149
+ content_height = height - details_chrome_height(
150
+ item, gap=gap, card_gap=card_gap
151
+ )
152
+ content_svg, face_actual_height = render_nested_face(
153
+ _ensure_resolved_face(item.face),
154
+ executor,
155
+ variables,
156
+ width,
157
+ content_height,
158
+ card_gap,
159
+ interactive,
160
+ render_cache,
161
+ error_collector=error_collector,
162
+ )
163
+ actual_height = float(
164
+ _px(summary_height + details_config.content_y_offset)
165
+ + face_actual_height
166
+ )
167
+ section_border = (
168
+ f'<rect x="0" y="0" width="{width}" height="{actual_height}" '
169
+ f'fill="none" stroke="{resolved_style.border.color}" stroke-width="{details_config.border.width}" rx="{details_config.border.radius}"/>'
170
+ )
171
+ rendered = (
172
+ f"{section_border}"
173
+ f"{summary_svg}"
174
+ f'<g transform="translate(0, {_px(summary_height + details_config.content_y_offset)})">'
175
+ f"{content_svg}</g>"
176
+ )
177
+ else:
178
+ rendered = summary_svg
179
+ actual_height = summary_height
180
+ else:
181
+ rendered = ""
182
+ actual_height = height
183
+ if item.type == "chart" and item.chart:
184
+ # Card padding strategy differs by renderer family:
185
+ # - SVG-family (table/kpi/spark_bar): render at reduced size, wrap in
186
+ # SVG translate so content is inset by card_padding on all sides.
187
+ # - Vega-family: render at full item size, forward card_padding as
188
+ # Vega's internal padding dict so Vega handles the inset itself.
189
+ # This aligns Vega content (marks, axis) with face titles which are
190
+ # also positioned at x_offset + card_padding.
191
+ from dataface.core.compile.models.style.merged import resolve_style
192
+
193
+ resolved_style = resolved_style or resolve_style(get_config().style)
194
+ card_padding = float(resolved_style.board.card_padding)
195
+ is_svg_family = _uses_svg_family_layout_padding(item.chart.type)
196
+
197
+ if is_svg_family:
198
+ chart_svg, chart_actual_height = render_chart_item(
199
+ item.chart,
200
+ executor,
201
+ variables,
202
+ width,
203
+ height,
204
+ resolved_style=resolved_style,
205
+ render_cache=render_cache,
206
+ svg_layout_padding=card_padding,
207
+ error_collector=error_collector,
208
+ face_level=face_level,
209
+ vega_config=vega_config,
210
+ )
211
+ if chart_svg:
212
+ rendered = chart_svg
213
+ actual_height = chart_actual_height
214
+ else:
215
+ # Vega-family: full item dimensions, padding forwarded to Vega spec.
216
+ chart_svg, chart_actual_height = render_chart_item(
217
+ item.chart,
218
+ executor,
219
+ variables,
220
+ width,
221
+ height,
222
+ resolved_style=resolved_style,
223
+ render_cache=render_cache,
224
+ padding=additive_padding(
225
+ card_padding, resolved_style.charts.padding
226
+ ),
227
+ error_collector=error_collector,
228
+ face_level=face_level,
229
+ vega_config=vega_config,
230
+ )
231
+ if chart_svg:
232
+ rendered = chart_svg
233
+ actual_height = chart_actual_height
234
+ elif item.type == "face" and item.face:
235
+ # Import here to avoid circular imports
236
+ from dataface.core.render.faces import render_nested_face
237
+
238
+ rendered, actual_height = render_nested_face(
239
+ _ensure_resolved_face(item.face),
240
+ executor,
241
+ variables,
242
+ width,
243
+ available_height=height,
244
+ card_gap=card_gap,
245
+ interactive=interactive,
246
+ render_cache=render_cache,
247
+ error_collector=error_collector,
248
+ )
249
+
250
+ if rendered and item.description:
251
+ escaped_description = html.escape(item.description)
252
+ return (
253
+ f'<g class="dft-layout-item" data-layout-description="{escaped_description}"'
254
+ f' aria-label="{escaped_description}">'
255
+ f"{rendered}</g>",
256
+ actual_height,
257
+ )
258
+ return rendered, actual_height
259
+
260
+
261
+ def _render_callout_block(
262
+ exc: DatafaceError,
263
+ chart: Chart,
264
+ executor: Executor,
265
+ variables: VariableValues,
266
+ available_width: float,
267
+ available_height: float,
268
+ resolved_style: MergedStyle | None,
269
+ error_collector: list[StructuredError] | None,
270
+ ) -> tuple[str, float]:
271
+ """Build an inline error SVG block and optionally collect the StructuredError.
272
+
273
+ Single call site for chart-error materialisation — the streaming task can
274
+ wrap this as a yield point without hunting across multiple files.
275
+ """
276
+ from dataface.core.render.chart.callout import render_callout_svg
277
+
278
+ # Stamp chart_id on fields so to_structured() carries it.
279
+ exc.fields["chart_id"] = chart.id
280
+ structured = exc.to_structured()
281
+
282
+ if error_collector is not None:
283
+ error_collector.append(structured)
284
+
285
+ chart_svg = render_callout_svg(
286
+ chart_id=chart.id,
287
+ message=structured.message,
288
+ width=available_width,
289
+ height=available_height,
290
+ title=f"Chart Error: {chart.id}",
291
+ resolved_style=resolved_style,
292
+ code=structured.code,
293
+ hint=structured.hint,
294
+ doc_url=structured.doc_url,
295
+ tone="negative",
296
+ )
297
+ svg, actual_height = _wrap_rendered_chart_svg(
298
+ chart,
299
+ chart_svg,
300
+ available_width=available_width,
301
+ executor=executor,
302
+ resolved_title=chart.id,
303
+ extra_classes=("dft-chart-callout",),
304
+ is_error_fallback=True,
305
+ )
306
+ return svg, actual_height
307
+
308
+
309
+ def render_chart_item(
310
+ chart: Chart,
311
+ executor: Executor,
312
+ variables: VariableValues,
313
+ available_width: float,
314
+ available_height: float = 300.0,
315
+ *,
316
+ resolved_style: MergedStyle | None = None,
317
+ render_cache: dict[tuple[str, float, float], tuple[str, float]] | None = None,
318
+ padding: dict[str, Any] | None = None,
319
+ svg_layout_padding: float = 0.0,
320
+ error_collector: list[StructuredError] | None = None,
321
+ face_level: int = 1,
322
+ vega_config: dict[str, Any] | None = None,
323
+ ) -> tuple[str, float]:
324
+ """Render a chart item with explicit dimensions.
325
+
326
+ Args:
327
+ chart: Chart to render
328
+ executor: Executor for query execution
329
+ variables: Variable values for queries
330
+ available_width: Width in pixels for the chart
331
+ available_height: Height in pixels for the chart
332
+ resolved_style: Board-scoped MergedStyle for effective chart style.
333
+ render_cache: Optional cache of pre-rendered SVGs from sizing pass.
334
+ padding: Vega-Lite padding dict forwarded from layout (Vega-family only).
335
+ error_collector: When provided, per-chart StructuredErrors are appended here.
336
+
337
+ Returns:
338
+ (svg_string, actual_height) — actual_height is the true rendered height.
339
+ """
340
+ from dataface.core.execute.errors import ExecutionError
341
+ from dataface.core.render.errors import RenderError
342
+
343
+ try:
344
+ render_width = available_width
345
+ render_height = available_height
346
+ if _uses_svg_family_layout_padding(chart.type):
347
+ render_width = max(available_width - 2 * svg_layout_padding, 0.0)
348
+ render_height = max(available_height - 2 * svg_layout_padding, 0.0)
349
+
350
+ svg, actual_height = _render_chart_item_inner(
351
+ chart,
352
+ executor,
353
+ variables,
354
+ render_width,
355
+ render_height,
356
+ resolved_style=resolved_style,
357
+ render_cache=render_cache,
358
+ padding=padding,
359
+ face_level=face_level,
360
+ vega_config=vega_config,
361
+ )
362
+ if svg_layout_padding and _uses_svg_family_layout_padding(chart.type):
363
+ padding_px = _px(svg_layout_padding)
364
+ svg = f'<g transform="translate({padding_px}, {padding_px})">' f"{svg}</g>"
365
+ actual_height += 2 * padding_px
366
+ return svg, actual_height
367
+ except (RenderError, ExecutionError, DatafaceError) as e:
368
+ # RenderError subsumes ChartDataError — existing KPI multirow path preserved.
369
+ # DatafaceError subsumes JinjaError and other compile-stage errors that
370
+ # escape into render (e.g. jinja in a chart title evaluated at render time).
371
+ return _render_callout_block(
372
+ e,
373
+ chart,
374
+ executor,
375
+ variables,
376
+ available_width,
377
+ available_height,
378
+ resolved_style,
379
+ error_collector,
380
+ )
381
+ except Exception as e: # noqa: BLE001
382
+ wrapped = RenderError.from_code(DF_RENDER_INTERNAL, inner_message=str(e))
383
+ return _render_callout_block(
384
+ wrapped,
385
+ chart,
386
+ executor,
387
+ variables,
388
+ available_width,
389
+ available_height,
390
+ resolved_style,
391
+ error_collector,
392
+ )
393
+
394
+
395
+ def _render_chart_item_inner(
396
+ chart: Chart,
397
+ executor: Executor,
398
+ variables: VariableValues,
399
+ available_width: float,
400
+ available_height: float = 300.0,
401
+ resolved_style: MergedStyle | None = None,
402
+ render_cache: dict[tuple[str, float, float], tuple[str, float]] | None = None,
403
+ padding: dict[str, Any] | None = None,
404
+ face_level: int = 1,
405
+ vega_config: dict[str, Any] | None = None,
406
+ ) -> tuple[str, float]:
407
+ """Internal chart rendering — raises on error.
408
+
409
+ Delegates chart-resolution and vl-convert rendering to render_chart_to_svg()
410
+ so the pipeline (query-execute → resolve → render) lives in one place.
411
+ SVG-family charts (table/kpi/spark_bar) use an explicit height; Vega-family
412
+ charts auto-size vertically (height=None). Cache is only used for Vega-family.
413
+ """
414
+ from dataface.core.compile.jinja import resolve_jinja_template
415
+ from dataface.core.render.chart.render_single import render_chart_to_svg
416
+
417
+ # SVG-family charts own their height; Vega-family auto-sizes vertically.
418
+ # Cache is only valid for Vega-family (SVG-family always re-renders).
419
+ is_svg_family = chart.type in SVG_LAYOUT_PADDED_TYPES
420
+ height_arg: float | None = available_height if is_svg_family else None
421
+
422
+ cache_key = (chart.id, available_width, available_height)
423
+ if render_cache is not None and not is_svg_family and cache_key in render_cache:
424
+ chart_svg, _ = render_cache[cache_key]
425
+ else:
426
+ # render_chart_to_svg owns the full pipeline: execute → resolve → vl-convert.
427
+ # Note: Renders individually; batch rendering could improve performance.
428
+ # See: plans/archive/VEGA_SVG_BATCH_CONVERSION_ANALYSIS.md
429
+ chart_svg, _, _ = render_chart_to_svg(
430
+ chart,
431
+ executor,
432
+ variables,
433
+ available_width,
434
+ height=height_arg,
435
+ resolved_style=resolved_style,
436
+ padding=padding,
437
+ face_level=face_level,
438
+ vega_config=vega_config,
439
+ )
440
+
441
+ # Resolve chart title for data attributes (Jinja only — no full resolve needed).
442
+ # KPI charts carry their authored text in ``label``; every other chart type
443
+ # uses ``title``. The wire-level ``data-chart-title`` attribute stays under
444
+ # one name regardless (it is an accessibility/identification handle, not
445
+ # tied to the YAML field name).
446
+ chart_title_source = chart.label if chart.type == "kpi" else chart.title
447
+ chart_title = (
448
+ resolve_jinja_template(chart_title_source, variables, strict=False)
449
+ if chart_title_source
450
+ else ""
451
+ ) or chart.id
452
+
453
+ # Apply chart-local background wrapper. ``style.background`` is a
454
+ # chart-local-only field — read it directly off the authored ``chart.style``.
455
+ # ``style.border`` is now eagerly merged into ``resolved_style.charts.border``
456
+ # by the cascade, so border.radius comes from there.
457
+ chart_style = getattr(chart, "style", None)
458
+ bg_color = getattr(chart_style, "background", None) if chart_style else None
459
+ if bg_color:
460
+ inner_content = extract_svg_inner_content(chart_svg)
461
+ dims = extract_svg_dimensions(chart_svg)
462
+ assert resolved_style is not None
463
+ chart_border_radius = _coerce_border_radius(resolved_style.charts.border.radius)
464
+ bg_rect = f'<rect x="0" y="0" width="{dims.width}" height="{dims.height}" fill="{bg_color}" rx="{chart_border_radius}"/>'
465
+ chart_svg = f'<svg xmlns="http://www.w3.org/2000/svg" width="{dims.width}" height="{dims.height}" viewBox="0 0 {dims.width} {dims.height}">{bg_rect}{inner_content}</svg>'
466
+
467
+ return _wrap_rendered_chart_svg(
468
+ chart,
469
+ chart_svg,
470
+ available_width=available_width,
471
+ executor=executor,
472
+ resolved_title=chart_title,
473
+ )
474
+
475
+
476
+ def _wrap_rendered_chart_svg(
477
+ chart: Chart,
478
+ chart_svg: str,
479
+ available_width: float,
480
+ executor: Executor,
481
+ resolved_title: str | None = None,
482
+ extra_classes: tuple[str, ...] = (),
483
+ is_error_fallback: bool = False,
484
+ ) -> tuple[str, float]:
485
+ dims = extract_svg_dimensions(chart_svg)
486
+ chart_height_actual = dims.height
487
+ # Internal SVG-family renderers embed directly in the chart wrapper <g>;
488
+ # extract their inner content to avoid a redundant nested <svg> viewport.
489
+ # is_error_fallback marks the ChartDataError path, where the payload is a
490
+ # render_callout_svg card on top of any chart.type (including Vega).
491
+ # Vega/Vega-Lite output otherwise remains as a standalone nested <svg>.
492
+ is_internal_svg = (
493
+ chart.type in SVG_LAYOUT_PADDED_TYPES
494
+ or chart.type == "callout"
495
+ or is_error_fallback
496
+ )
497
+ if is_internal_svg:
498
+ chart_content = extract_svg_inner_content(chart_svg)
499
+ else:
500
+ chart_content = chart_svg
501
+
502
+ var_deps = chart.variable_dependencies or set()
503
+ var_attrs = (
504
+ " ".join(f'data-var-{html.escape(v)}="true"' for v in sorted(var_deps))
505
+ if var_deps
506
+ else ""
507
+ )
508
+
509
+ chart_title = (resolved_title or chart.id) or chart.id
510
+ escaped_title = html.escape(chart_title)
511
+ escaped_id = html.escape(chart.id)
512
+ escaped_description = html.escape(chart.description) if chart.description else ""
513
+ accessible_label = chart_title
514
+ if chart.description:
515
+ accessible_label = f"{chart_title} — {chart.description}"
516
+ escaped_accessible_label = html.escape(accessible_label)
517
+
518
+ schema_status = _resolve_schema_status(executor, chart.query_name)
519
+
520
+ classes = ["dft-chart", *extra_classes]
521
+ if chart.type == "callout" and "dft-chart-callout" not in classes:
522
+ classes.append("dft-chart-callout")
523
+ if schema_status:
524
+ classes.append(f"dft-schema-{schema_status}")
525
+ class_attr = " ".join(classes)
526
+ attrs_parts = [
527
+ f'class="{class_attr}"',
528
+ f'id="chart-{escaped_id}"',
529
+ f'data-chart-id="{escaped_id}"',
530
+ f'data-chart-title="{escaped_title}"',
531
+ f'data-chart-width="{available_width}"',
532
+ f'data-chart-height="{chart_height_actual}"',
533
+ f'aria-label="{escaped_accessible_label}"',
534
+ ]
535
+ if schema_status:
536
+ attrs_parts.append(f'data-schema-status="{schema_status}"')
537
+ if escaped_description:
538
+ attrs_parts.append(f'data-chart-description="{escaped_description}"')
539
+ if var_attrs:
540
+ attrs_parts.append(var_attrs)
541
+
542
+ attrs_str = " " + " ".join(attrs_parts)
543
+ return f"<g{attrs_str}>{chart_content}</g>", chart_height_actual
544
+
545
+
546
+ def _resolve_schema_status(
547
+ executor: Executor, query_name: str | None
548
+ ) -> SchemaStatus | None:
549
+ """Derive aggregate schema status for a chart from its query's provenance.
550
+
551
+ Returns "changed" if any relation used a dev/branch schema,
552
+ "unchanged" if all relations matched prod, "unknown" if provenance
553
+ exists but classification is uncertain, or None if no provenance.
554
+ """
555
+ if not query_name:
556
+ return None
557
+ relations = executor.get_query_provenance(query_name)
558
+ if not relations:
559
+ return None
560
+ statuses = {r.status for r in relations}
561
+ if "changed" in statuses:
562
+ return "changed"
563
+ if "unknown" in statuses:
564
+ return "unknown"
565
+ return "unchanged"
@@ -0,0 +1,90 @@
1
+ """Serialization helpers for chart rendering outputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from dataface.core.compile.models.chart.compiled import (
8
+ Chart,
9
+ )
10
+ from dataface.core.render.utils import normalize_data_types
11
+
12
+
13
+ def build_dataface_json(
14
+ chart: Chart | Any,
15
+ data: list[dict[str, Any]],
16
+ width: float | None = None,
17
+ height: float | None = None,
18
+ ) -> dict[str, Any]:
19
+ """Build the normalized Dataface JSON representation for a chart."""
20
+ normalized_data = normalize_data_types(data)
21
+ result: dict[str, Any] = {"type": chart.type}
22
+
23
+ if chart.title:
24
+ result["title"] = chart.title
25
+ if getattr(chart, "label", None):
26
+ result["label"] = chart.label
27
+ if chart.subtitle:
28
+ result["subtitle"] = chart.subtitle
29
+ if chart.description:
30
+ result["description"] = chart.description
31
+
32
+ for field in ["x", "y", "color", "size", "shape"]:
33
+ value = getattr(chart, field, None)
34
+ if value is not None:
35
+ result[field] = value
36
+
37
+ for field in ["x_label", "y_label"]:
38
+ value = getattr(chart, field, None)
39
+ if value is not None:
40
+ result[field] = value
41
+
42
+ if chart.format is not None:
43
+ result["format"] = (
44
+ chart.format.model_dump(exclude_none=True)
45
+ if hasattr(chart.format, "model_dump")
46
+ else chart.format
47
+ )
48
+
49
+ # KPI quantitative-text-object fields. `support` is a Pydantic model so it
50
+ # needs ``model_dump`` to be JSON-serializable.
51
+ support = getattr(chart, "support", None)
52
+ if support is not None:
53
+ result["support"] = support.model_dump(exclude_none=True)
54
+
55
+ for field in [
56
+ "geo",
57
+ "geo_source",
58
+ "lookup",
59
+ "value",
60
+ "projection",
61
+ "latitude",
62
+ "longitude",
63
+ "background",
64
+ ]:
65
+ value = getattr(chart, field, None)
66
+ if value is not None:
67
+ result[field] = value
68
+
69
+ if chart.filters is not None:
70
+ result["filters"] = {
71
+ col: fd.to_yaml_form() for col, fd in chart.filters.items()
72
+ }
73
+
74
+ if chart.style:
75
+ result["style"] = chart.style.model_dump(exclude_none=True)
76
+
77
+ link = getattr(chart, "link", None)
78
+ if link:
79
+ result["link"] = link
80
+
81
+ if chart.type == "table":
82
+ result["columns"] = list(normalized_data[0].keys()) if normalized_data else []
83
+
84
+ result["data"] = normalized_data
85
+ if width is not None:
86
+ result["width"] = width
87
+ if height is not None:
88
+ result["height"] = height
89
+
90
+ return result