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,43 @@
1
+ @font-face {
2
+ font-family: 'Inter Variable';
3
+ src: url('/static/fonts/InterVariable.woff2') format('woff2-variations');
4
+ font-weight: 100 900;
5
+ font-style: normal;
6
+ font-display: swap;
7
+ }
8
+ @font-face {
9
+ font-family: 'DFT Sans Tabular';
10
+ src: url('/static/fonts/DFTSansTabular-Regular.woff2') format('woff2-variations');
11
+ font-weight: 100 900;
12
+ font-style: normal;
13
+ font-display: swap;
14
+ }
15
+ @font-face {
16
+ font-family: 'Source Serif 4';
17
+ src: url('/static/fonts/source-serif-4-variable-latin.woff2') format('woff2-variations');
18
+ font-weight: 200 900;
19
+ font-style: normal;
20
+ font-display: swap;
21
+ }
22
+ @font-face {
23
+ font-family: 'Noto Emoji';
24
+ src: url('/static/fonts/NotoEmoji-Regular.woff2') format('woff2');
25
+ font-display: swap;
26
+ unicode-range:
27
+ U+1F000-1F02F, U+1F0A0-1F0FF, U+1F100-1F1FF, U+1F200-1F2FF,
28
+ U+1F300-1F5FF, U+1F600-1F64F, U+1F650-1F67F, U+1F680-1F6FF,
29
+ U+1F700-1F77F, U+1F780-1F7FF, U+1F800-1F8FF, U+1F900-1F9FF,
30
+ U+1FA00-1FA6F, U+1FA70-1FAFF, U+2600-26FF, U+2700-27BF,
31
+ U+FE00-FE0F, U+1F1E6-1F1FF;
32
+ }
33
+ @font-face {
34
+ font-family: 'Noto Color Emoji';
35
+ src: url('/static/fonts/NotoColorEmoji-Regular.woff2') format('woff2');
36
+ font-display: swap;
37
+ unicode-range:
38
+ U+1F000-1F02F, U+1F0A0-1F0FF, U+1F100-1F1FF, U+1F200-1F2FF,
39
+ U+1F300-1F5FF, U+1F600-1F64F, U+1F650-1F67F, U+1F680-1F6FF,
40
+ U+1F700-1F77F, U+1F780-1F7FF, U+1F800-1F8FF, U+1F900-1F9FF,
41
+ U+1FA00-1FA6F, U+1FA70-1FAFF, U+2600-26FF, U+2700-27BF,
42
+ U+FE00-FE0F, U+1F1E6-1F1FF;
43
+ }
@@ -0,0 +1,329 @@
1
+ """D3 formatting utilities for value display.
2
+
3
+ Stage: RENDER
4
+ Purpose: Resolve format aliases from the theme cascade and format values
5
+ using D3-style format strings.
6
+
7
+ The format alias vocabulary lives in theme YAML (style.formats). Callers
8
+ pass compiled_style.formats to resolve_format(); unknown keys and raw d3
9
+ specs pass through unchanged for d3 to parse.
10
+
11
+ The d3-format spec parsing and number formatting is delegated to the
12
+ ``d3_format`` library (``libs/d3-format/``), which implements byte-for-byte
13
+ parity with d3.js for the supported type subset.
14
+
15
+ Dataface-specific extensions over d3-format:
16
+ - ``analytic`` notation: SI ``k/M/G/T`` suffixes mapped to ``K/M/B/T`` with a
17
+ space separator (e.g. d3's ``"1.5G"`` becomes ``"1.5 B"``).
18
+ - ``narrative`` notation: ``k/M/G/T`` mapped to ``k/mn/bn/tr`` (no space,
19
+ journalistic abbreviations — not part of the d3-format standard).
20
+ - ``None`` value: rendered as ``"—"`` (em dash) regardless of spec.
21
+
22
+ See also:
23
+ - https://d3js.org/d3-format
24
+ - vega_lite.py: Uses these utilities for KPI chart formatting
25
+ """
26
+
27
+ from typing import Any
28
+
29
+ from d3_format import format as _d3_format, parse as _d3_parse
30
+
31
+ # Import FormatConfig from types to avoid duplication
32
+ # Re-export for convenience
33
+ from dataface.core.compile.models.primitives import FormatConfig
34
+
35
+ # ============================================================================
36
+ # NATIVE (NON-D3) FORMATTERS
37
+ # ============================================================================
38
+ # These handle format aliases that cannot be expressed as D3 format strings.
39
+ # Keyed by the alias name (same string an author writes in YAML format: field).
40
+ # Checked in format_value / format_kpi_parts before the D3 path.
41
+ #
42
+ # percent_number / percent_number_delta: the value IS the whole-number percent
43
+ # (12.8 means 12.8%). D3's "%" type multiplies by 100, so it can't express this.
44
+
45
+ _NATIVE_FORMATTERS: dict[str, Any] = {
46
+ "percent_number": lambda v: "—" if v is None else f"{float(v):.1f}%",
47
+ "percent_number_delta": lambda v: "—" if v is None else f"{float(v):+.1f}%",
48
+ }
49
+
50
+ # ============================================================================
51
+ # FORMAT RESOLUTION
52
+ # ============================================================================
53
+
54
+
55
+ def resolve_format(
56
+ format_input: str | FormatConfig | dict[str, Any] | None,
57
+ formats: dict[str, str] | None = None,
58
+ ) -> str:
59
+ """Convert format input to D3 format string.
60
+
61
+ Resolution order:
62
+ 1. formats[key] — theme/face-defined alias lookup (key-sensitive)
63
+ 2. Raw d3 passthrough — author typed a literal d3 spec
64
+
65
+ If an alias key is not in formats and is not a valid d3 spec, d3 itself
66
+ will raise at render time. That is the intended contract.
67
+
68
+ Args:
69
+ format_input: Format specification (string, FormatConfig, dict, or None)
70
+ formats: The resolved format alias dict from the theme cascade
71
+ (compiled_style.formats). None = no aliases defined.
72
+
73
+ Returns:
74
+ D3 format string, or "" for null/empty input.
75
+ """
76
+ if format_input is None:
77
+ return ""
78
+
79
+ # Extract spec from FormatConfig or dict
80
+ if isinstance(format_input, FormatConfig):
81
+ format_str = format_input.spec or ""
82
+ elif isinstance(format_input, dict):
83
+ format_str = format_input.get("spec", "")
84
+ else:
85
+ format_str = str(format_input)
86
+
87
+ if not format_str:
88
+ return ""
89
+
90
+ # Theme/face alias lookup (case-sensitive, key-exact)
91
+ if formats and format_str in formats:
92
+ return formats[format_str]
93
+
94
+ # Raw d3 passthrough — let d3 parse or raise
95
+ return format_str
96
+
97
+
98
+ def get_format_prefix_suffix(
99
+ format_input: str | FormatConfig | dict[str, Any] | None,
100
+ ) -> tuple[str, str]:
101
+ """Extract prefix and suffix from format configuration.
102
+
103
+ Args:
104
+ format_input: Format specification
105
+
106
+ Returns:
107
+ Tuple of (prefix, suffix) strings
108
+ """
109
+ if format_input is None:
110
+ return "", ""
111
+
112
+ if isinstance(format_input, FormatConfig):
113
+ return format_input.prefix or "", format_input.suffix or ""
114
+ elif isinstance(format_input, dict):
115
+ return format_input.get("prefix", ""), format_input.get("suffix", "")
116
+
117
+ # String format has no prefix/suffix
118
+ return "", ""
119
+
120
+
121
+ def _get_notation(
122
+ format_input: str | FormatConfig | dict[str, Any] | None,
123
+ ) -> str | None:
124
+ """Extract notation family from format configuration."""
125
+ if isinstance(format_input, FormatConfig):
126
+ return format_input.notation
127
+ elif isinstance(format_input, dict):
128
+ return format_input.get("notation")
129
+ return None
130
+
131
+
132
+ # ============================================================================
133
+ # D3 FORMATTING IMPLEMENTATION
134
+ # ============================================================================
135
+
136
+
137
+ def format_d3(
138
+ value: int | float | None,
139
+ format_spec: str,
140
+ prefix: str = "",
141
+ suffix: str = "",
142
+ notation: str | None = None,
143
+ ) -> str:
144
+ """Format a value using D3-style format specification.
145
+
146
+ Delegates to ``libs/d3-format/`` for spec parsing and formatting.
147
+ Applies Dataface-specific analytic/narrative notation post-processing for
148
+ SI (``s``-type) specs after the lib produces d3-standard output.
149
+
150
+ Args:
151
+ value: Numeric value to format
152
+ format_spec: D3 format string (e.g., "$,.2f", ".1~%", "~s")
153
+ prefix: Custom prefix to prepend
154
+ suffix: Custom suffix to append
155
+ notation: SI-prefix notation family ("analytic" or "narrative")
156
+
157
+ Returns:
158
+ Formatted string
159
+ """
160
+ if value is None:
161
+ return f"{prefix}—{suffix}"
162
+
163
+ if not format_spec:
164
+ return f"{prefix}{value}{suffix}"
165
+
166
+ formatted = _d3_format(format_spec)(float(value))
167
+
168
+ # Dataface-specific SI notation post-processing.
169
+ # d3 emits SI standard: k/M/G/T with no space. Dataface surfaces show
170
+ # K/M/B/T with a space (analytic) or k/mn/bn/tr (narrative).
171
+ if _is_si_spec(format_spec):
172
+ effective_notation = notation or "analytic"
173
+ if effective_notation == "analytic":
174
+ formatted = _apply_analytic_notation(formatted)
175
+ elif effective_notation == "narrative":
176
+ formatted = _apply_narrative_notation(formatted)
177
+ else:
178
+ raise ValueError(f"Unknown notation: {effective_notation!r}")
179
+
180
+ return f"{prefix}{formatted}{suffix}"
181
+
182
+
183
+ def _is_si_spec(spec: str) -> bool:
184
+ """True when spec's type character is 's' (SI prefix)."""
185
+ return _d3_parse(spec).type == "s"
186
+
187
+
188
+ # d3 SI prefix → Dataface analytic suffix mapping
189
+ _D3_TO_ANALYTIC: dict[str, str] = {
190
+ "k": " K",
191
+ "M": " M",
192
+ "G": " B",
193
+ "T": " T",
194
+ "P": " P",
195
+ "E": " E",
196
+ "Z": " Z",
197
+ "Y": " Y",
198
+ }
199
+
200
+ # d3 SI prefix → Dataface narrative suffix mapping
201
+ _D3_TO_NARRATIVE: dict[str, str] = {
202
+ "k": "k",
203
+ "M": "mn",
204
+ "G": "bn",
205
+ "T": "tr",
206
+ }
207
+
208
+
209
+ def _apply_analytic_notation(d3_str: str) -> str:
210
+ """Convert d3 SI string (e.g. '1.5M') to analytic notation (e.g. '1.5 M')."""
211
+ for d3_sfx, analytic_sfx in _D3_TO_ANALYTIC.items():
212
+ if d3_str.endswith(d3_sfx):
213
+ return d3_str[: -len(d3_sfx)] + analytic_sfx
214
+ return d3_str
215
+
216
+
217
+ def _apply_narrative_notation(d3_str: str) -> str:
218
+ """Convert d3 SI string (e.g. '1.5G') to narrative notation (e.g. '1.5bn')."""
219
+ for d3_sfx, narrative_sfx in _D3_TO_NARRATIVE.items():
220
+ if d3_str.endswith(d3_sfx):
221
+ return d3_str[: -len(d3_sfx)] + narrative_sfx
222
+ return d3_str
223
+
224
+
225
+ def format_value(
226
+ value: int | float | None,
227
+ format_input: str | FormatConfig | dict[str, Any] | None,
228
+ formats: dict[str, str] | None = None,
229
+ ) -> str:
230
+ """Format a value using the given format configuration.
231
+
232
+ Args:
233
+ value: Numeric value to format
234
+ format_input: Format specification (string, FormatConfig, dict, or None)
235
+ formats: Theme format alias dict from compiled_style.formats
236
+
237
+ Returns:
238
+ Formatted string
239
+ """
240
+ format_spec = resolve_format(format_input, formats)
241
+ if format_spec in _NATIVE_FORMATTERS:
242
+ return _NATIVE_FORMATTERS[format_spec](value)
243
+ prefix, suffix = get_format_prefix_suffix(format_input)
244
+ notation = _get_notation(format_input)
245
+
246
+ return format_d3(value, format_spec, prefix, suffix, notation=notation)
247
+
248
+
249
+ def format_kpi_parts(
250
+ value: int | float | None,
251
+ format_input: str | FormatConfig | dict[str, Any] | None,
252
+ formats: dict[str, str] | None = None,
253
+ ) -> tuple[str, str, str]:
254
+ """Format a KPI value into (prefix, number, suffix) parts.
255
+
256
+ Separates prefix symbols ($), magnitude suffixes (M, K, bn), and unit
257
+ suffixes (%, USD) from the formatted number so they can be rendered or
258
+ positioned independently.
259
+
260
+ Args:
261
+ value: Numeric value to format
262
+ format_input: Format specification
263
+ formats: Theme format alias dict from compiled_style.formats
264
+ """
265
+ if value is None:
266
+ return "", "—", ""
267
+
268
+ explicit_prefix, explicit_suffix = get_format_prefix_suffix(format_input)
269
+ format_spec = resolve_format(format_input, formats)
270
+
271
+ # Native formatters bypass D3: split formatted string into number + % suffix.
272
+ if format_spec in _NATIVE_FORMATTERS:
273
+ formatted = _NATIVE_FORMATTERS[format_spec](value)
274
+ if formatted.endswith("%"):
275
+ return "", formatted[:-1], "%"
276
+ return "", formatted, ""
277
+
278
+ d3_prefix = ""
279
+ d3_suffix = ""
280
+ magnitude_suffix = ""
281
+
282
+ if format_spec:
283
+ # Pull $ out of the D3 spec so it renders as a separate element
284
+ if "$" in format_spec:
285
+ d3_prefix = "$"
286
+ format_spec_clean = format_spec.replace("$", "")
287
+ else:
288
+ format_spec_clean = format_spec
289
+
290
+ notation = _get_notation(format_input)
291
+
292
+ # SI/compact: split magnitude suffix into its own field
293
+ if _is_si_spec(format_spec_clean):
294
+ # Format via the lib (without notation post-process), then split suffix
295
+ raw = _d3_format(format_spec_clean)(float(value))
296
+ number_str = raw
297
+ effective_notation = notation or "analytic"
298
+ if effective_notation == "analytic":
299
+ for d3_sfx, analytic_sfx in _D3_TO_ANALYTIC.items():
300
+ if raw.endswith(d3_sfx):
301
+ number_str = raw[: -len(d3_sfx)]
302
+ magnitude_suffix = analytic_sfx.strip()
303
+ break
304
+ elif effective_notation == "narrative":
305
+ for d3_sfx, narrative_sfx in _D3_TO_NARRATIVE.items():
306
+ if raw.endswith(d3_sfx):
307
+ number_str = raw[: -len(d3_sfx)]
308
+ magnitude_suffix = narrative_sfx
309
+ break
310
+ else:
311
+ raise ValueError(f"Unknown notation: {effective_notation!r}")
312
+ else:
313
+ number_str = format_d3(value, format_spec_clean, notation=notation)
314
+
315
+ # Pull trailing % that format_d3 appended
316
+ if "%" in format_spec and number_str.endswith("%"):
317
+ d3_suffix = "%"
318
+ number_str = number_str[:-1]
319
+ else:
320
+ v = float(value)
321
+ if v == int(v) and abs(v) < 1e15:
322
+ number_str = f"{int(v):,}"
323
+ else:
324
+ number_str = f"{v:,.2f}"
325
+
326
+ prefix = ((explicit_prefix or "") + d3_prefix).strip()
327
+ suffix = (magnitude_suffix + d3_suffix + (explicit_suffix or "").strip()).strip()
328
+
329
+ return prefix, number_str.strip(), suffix
@@ -0,0 +1,28 @@
1
+ geo_sources:
2
+ us-states:
3
+ url: "https://vega.github.io/vega-datasets/data/us-10m.json"
4
+ format: topojson
5
+ feature: states
6
+ projection: albersUsa
7
+ key: id
8
+
9
+ us-counties:
10
+ url: "https://vega.github.io/vega-datasets/data/us-10m.json"
11
+ format: topojson
12
+ feature: counties
13
+ projection: albersUsa
14
+ key: id
15
+
16
+ world-countries:
17
+ url: "https://vega.github.io/vega-datasets/data/world-110m.json"
18
+ format: topojson
19
+ feature: countries
20
+ projection: equalEarth
21
+ key: id
22
+
23
+ world-50m:
24
+ url: "https://vega.github.io/vega-datasets/data/world-50m.json"
25
+ format: topojson
26
+ feature: countries
27
+ projection: equalEarth
28
+ key: id
@@ -0,0 +1,146 @@
1
+ """JSON render output format.
2
+
3
+ Walks the layout tree, executes queries, resolves charts, and returns
4
+ the resolved objects plus executed data as a JSON string.
5
+ """
6
+
7
+ import dataclasses
8
+ import json
9
+ from collections.abc import Mapping
10
+ from datetime import date, datetime
11
+ from decimal import Decimal
12
+ from typing import Any
13
+
14
+ from pydantic import BaseModel
15
+
16
+ from dataface.core.compile.errors import DatafaceError
17
+ from dataface.core.compile.models.face.compiled import Face, VariableValues
18
+ from dataface.core.errors import DF_RENDER_INTERNAL, StructuredError
19
+ from dataface.core.execute.executor import Executor
20
+ from dataface.core.render.chart.pipeline import resolve_chart
21
+ from dataface.core.render.utils import resolve_field_names
22
+
23
+
24
+ def _json_default(obj: Any) -> Any:
25
+ """Handle non-serializable types."""
26
+ if isinstance(obj, BaseModel):
27
+ return obj.model_dump(exclude_none=True)
28
+ if isinstance(obj, Mapping):
29
+ return dict(obj)
30
+ if isinstance(obj, set):
31
+ return sorted(obj)
32
+ if isinstance(obj, Decimal):
33
+ return str(obj)
34
+ if isinstance(obj, (date, datetime)):
35
+ return obj.isoformat()
36
+ raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
37
+
38
+
39
+ def _resolved_chart_to_dict(resolved: Any) -> dict[str, Any]:
40
+ """Convert a ResolvedChart dataclass to a dict, skipping source_chart."""
41
+ d = dataclasses.asdict(resolved)
42
+ d.pop("source_chart", None)
43
+ return d
44
+
45
+
46
+ def _render_chart_item(
47
+ chart: Any,
48
+ executor: Executor,
49
+ variables: VariableValues,
50
+ error_collector: list[StructuredError] | None = None,
51
+ ) -> dict[str, Any]:
52
+ """Execute, resolve, and serialize a single chart item."""
53
+ from dataface.core.execute.errors import ExecutionError
54
+ from dataface.core.render.errors import RenderError
55
+
56
+ try:
57
+ data = executor.execute_chart(chart, variables)
58
+ resolved_chart = resolve_field_names(chart, data)
59
+ col_descs = (
60
+ executor.get_column_descriptions(chart.query_name)
61
+ if chart.query_name
62
+ else None
63
+ )
64
+ presentation = resolve_chart(resolved_chart, data, col_descs)
65
+ return {
66
+ "type": "chart",
67
+ "chart": _resolved_chart_to_dict(presentation),
68
+ "data": data,
69
+ }
70
+ except (RenderError, ExecutionError, DatafaceError) as e:
71
+ e.fields["chart_id"] = chart.id
72
+ structured = e.to_structured()
73
+ if error_collector is not None:
74
+ error_collector.append(structured)
75
+ return {"type": "chart", "_error": structured.model_dump(exclude_none=True)}
76
+ except Exception as e: # noqa: BLE001
77
+ wrapped = RenderError.from_code(DF_RENDER_INTERNAL, inner_message=str(e))
78
+ wrapped.fields["chart_id"] = chart.id
79
+ structured = wrapped.to_structured()
80
+ if error_collector is not None:
81
+ error_collector.append(structured)
82
+ return {"type": "chart", "_error": structured.model_dump(exclude_none=True)}
83
+
84
+
85
+ def _render_face_items(
86
+ face: Face,
87
+ executor: Executor,
88
+ variables: VariableValues,
89
+ error_collector: list[StructuredError] | None = None,
90
+ ) -> list[dict[str, Any]]:
91
+ """Walk layout items and serialize each."""
92
+ results: list[dict[str, Any]] = []
93
+ for item in face.layout.items:
94
+ if item.type == "chart" and item.chart:
95
+ results.append(
96
+ _render_chart_item(item.chart, executor, variables, error_collector)
97
+ )
98
+ elif item.type == "face" and item.face:
99
+ results.append(
100
+ {
101
+ "type": "face",
102
+ "face": face_to_dict(
103
+ item.face, executor, variables, error_collector
104
+ ),
105
+ }
106
+ )
107
+ return results
108
+
109
+
110
+ def face_to_dict(
111
+ face: Face,
112
+ executor: Executor,
113
+ variables: VariableValues,
114
+ error_collector: list[StructuredError] | None = None,
115
+ ) -> dict[str, Any]:
116
+ """Convert a face to a JSON-serializable dict."""
117
+ result: dict[str, Any] = {
118
+ "id": face.id,
119
+ "title": face.title,
120
+ "items": _render_face_items(face, executor, variables, error_collector),
121
+ }
122
+ if face.variables:
123
+ result["variables"] = {
124
+ name: var.model_dump(exclude_none=True)
125
+ for name, var in face.variables.items()
126
+ }
127
+ return result
128
+
129
+
130
+ def render_face_json(
131
+ face: Face,
132
+ executor: Executor,
133
+ variables: VariableValues,
134
+ error_collector: list[StructuredError] | None = None,
135
+ ) -> str:
136
+ """Render a compiled face to JSON.
137
+
138
+ Walks the layout tree, executes queries, resolves charts
139
+ (auto type, auto fields), and returns a JSON string with
140
+ the resolved chart semantics and executed data.
141
+ """
142
+ return json.dumps(
143
+ face_to_dict(face, executor, variables, error_collector),
144
+ default=_json_default,
145
+ indent=2,
146
+ )