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,287 @@
1
+ """Control template discovery and loading.
2
+
3
+ Stage: RENDER
4
+ Purpose: Discover and render variable control templates.
5
+
6
+ This module provides the ControlRegistry class that:
7
+ 1. Discovers control templates from built-in and project directories
8
+ 2. Supports flexible directory organization (flat, self-contained, categorized)
9
+ 3. Renders templates with Jinja2
10
+ 4. Collects CSS/JS assets for used controls
11
+ """
12
+
13
+ import json
14
+ import logging
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class ControlRegistry:
24
+ """Registry for variable control templates.
25
+
26
+ Discovers and loads control templates from:
27
+ 1. Project controls directory (user customizations) - checked first
28
+ 2. Built-in controls (dataface defaults)
29
+
30
+ Supports flexible directory organization:
31
+ - controls/rating.html (flat)
32
+ - controls/rating/rating.html (self-contained)
33
+ - controls/pickers/color/color.html (categorized)
34
+
35
+ Example:
36
+ registry = ControlRegistry(project_root=Path("/my/project"))
37
+ html = registry.render_control("select", {
38
+ "name": "region",
39
+ "label": "Region",
40
+ "value": "US",
41
+ "options": [{"value": "US", "label": "United States", "selected": True}]
42
+ })
43
+ """
44
+
45
+ def __init__(self, project_root: Path | None = None):
46
+ """Initialize the control registry.
47
+
48
+ Args:
49
+ project_root: Optional project root for loading custom controls.
50
+ If provided, looks for controls in {project_root}/controls/
51
+ """
52
+ self._project_root = project_root
53
+ self._builtin_dir = Path(__file__).parent / "templates" / "controls"
54
+ self._templates: dict[str, Path] = {}
55
+ self._css: dict[str, str] = {}
56
+ self._js: dict[str, str] = {}
57
+ self._jinja_env: Environment | None = None
58
+ self._used_controls: set[str] = set()
59
+ self._base_css: str | None = None
60
+
61
+ self._load_base_css()
62
+ self._load_builtins()
63
+ if project_root:
64
+ self._load_project_controls(project_root)
65
+
66
+ def _load_base_css(self) -> None:
67
+ """Load the base styles CSS file."""
68
+ emoji_partial = Path(__file__).parent / "fonts" / "_emoji_font_face.css"
69
+ base_css_path = self._builtin_dir / "_styles.css"
70
+ if base_css_path.exists():
71
+ self._base_css = emoji_partial.read_text() + base_css_path.read_text()
72
+
73
+ def _load_builtins(self) -> None:
74
+ """Load built-in control templates."""
75
+ if not self._builtin_dir.exists():
76
+ logger.warning(
77
+ f"Built-in controls directory not found: {self._builtin_dir}"
78
+ )
79
+ return
80
+
81
+ for template in self._builtin_dir.glob("*.html"):
82
+ if not template.name.startswith("_"):
83
+ self._register_control(template.stem, template)
84
+
85
+ def _load_project_controls(self, project_root: Path) -> None:
86
+ """Load controls from project directory with flexible organization.
87
+
88
+ Supports:
89
+ - controls/rating.html (flat)
90
+ - controls/rating/rating.html (self-contained directory)
91
+ - controls/pickers/color/color.html (categorized)
92
+
93
+ Files starting with _ are ignored (for shared partials/imports).
94
+ """
95
+ controls_dir = project_root / "controls"
96
+ if not controls_dir.exists():
97
+ return
98
+
99
+ # Find all HTML templates (excluding _prefixed files)
100
+ for template in controls_dir.rglob("*.html"):
101
+ rel_path = template.relative_to(controls_dir)
102
+
103
+ # Skip files/dirs starting with _ (check relative path only)
104
+ if any(part.startswith("_") for part in rel_path.parts):
105
+ continue
106
+
107
+ if template.stem == template.parent.name:
108
+ # Self-contained: controls/rating/rating.html -> "rating"
109
+ # Or nested: controls/pickers/color/color.html -> "pickers/color"
110
+ parent_rel = rel_path.parent
111
+ if parent_rel != Path("."):
112
+ control_name = str(parent_rel).replace("\\", "/")
113
+ else:
114
+ control_name = template.stem
115
+ else:
116
+ # Flat: controls/toggle.html -> "toggle"
117
+ # Or categorized flat: controls/pickers/icon.html -> "pickers/icon"
118
+ if rel_path.parent != Path("."):
119
+ control_name = str(rel_path.parent / template.stem).replace(
120
+ "\\", "/"
121
+ )
122
+ else:
123
+ control_name = template.stem
124
+
125
+ # Project controls override built-ins
126
+ self._register_control(control_name, template, override=True)
127
+ logger.debug(f"Loaded project control: {control_name} from {template}")
128
+
129
+ def _register_control(
130
+ self, name: str, template_path: Path, override: bool = False
131
+ ) -> None:
132
+ """Register a control and discover its companion assets (CSS, JS)."""
133
+ if name in self._templates and not override:
134
+ return
135
+
136
+ self._templates[name] = template_path
137
+
138
+ # Load companion CSS if exists
139
+ css_path = template_path.with_suffix(".css")
140
+ if css_path.exists():
141
+ self._css[name] = css_path.read_text()
142
+
143
+ # Load companion JS if exists
144
+ js_path = template_path.with_suffix(".js")
145
+ if js_path.exists():
146
+ self._js[name] = js_path.read_text()
147
+
148
+ def find_control(self, input_type: str) -> Path | None:
149
+ """Find control template by type name.
150
+
151
+ Supports:
152
+ - "rating" -> controls/rating.html or controls/rating/rating.html
153
+ - "pickers/color" -> controls/pickers/color/color.html
154
+
155
+ Args:
156
+ input_type: The control type (e.g., "select", "checkbox", "rating")
157
+
158
+ Returns:
159
+ Path to the template file, or None if not found
160
+ """
161
+ # Direct match
162
+ if input_type in self._templates:
163
+ return self._templates[input_type]
164
+
165
+ # Try without namespace prefix for built-ins
166
+ base_name = input_type.split("/")[-1]
167
+ if base_name in self._templates:
168
+ return self._templates[base_name]
169
+
170
+ return None
171
+
172
+ def has_control(self, input_type: str) -> bool:
173
+ """Check if a control type is available."""
174
+ return self.find_control(input_type) is not None
175
+
176
+ def render_control(
177
+ self,
178
+ input_type: str,
179
+ context: dict[str, Any],
180
+ ) -> str:
181
+ """Render a control template with the given context.
182
+
183
+ Args:
184
+ input_type: The control type (e.g., "select", "checkbox")
185
+ context: Template context dict containing:
186
+ - name: Variable name
187
+ - label: Display label
188
+ - value: Current value
189
+ - default: Default value
190
+ - options: List of {value, label, selected} for select controls
191
+ - min, max, step: For number/slider controls
192
+ - config: Full variable config (for advanced templates)
193
+ - depends_on: List of variable dependencies
194
+
195
+ Returns:
196
+ Rendered HTML string
197
+ """
198
+ template_path = self.find_control(input_type)
199
+
200
+ if not template_path:
201
+ logger.warning(f"Unknown control type '{input_type}', using readonly")
202
+ template_path = self.find_control("readonly")
203
+ if not template_path:
204
+ # Ultimate fallback if no readonly template
205
+ name = context.get("name", "unknown")
206
+ label = context.get("label", name)
207
+ value = context.get("value", "")
208
+ return f'<span class="dft-variable-readonly"><span class="dft-variable-label">{_html_escape(label)}:</span> <span class="dft-variable-readonly-value">{_html_escape(str(value))}</span></span>'
209
+
210
+ # Track which controls are used (for CSS/JS collection)
211
+ self._used_controls.add(input_type)
212
+
213
+ env = self._get_jinja_env()
214
+ # Load template content directly to support project overrides
215
+ template_content = template_path.read_text()
216
+ template = env.from_string(template_content)
217
+ return template.render(**context)
218
+
219
+ def get_base_css(self) -> str:
220
+ """Get the base CSS styles for all controls."""
221
+ return self._base_css or ""
222
+
223
+ def get_used_css(self) -> str:
224
+ """Get combined CSS for controls that were actually rendered.
225
+
226
+ Returns CSS only for controls that have been rendered via render_control(),
227
+ avoiding unnecessary CSS bloat.
228
+ """
229
+ css_parts = []
230
+ for control_name in self._used_controls:
231
+ if control_name in self._css:
232
+ css_parts.append(f"/* {control_name} */\n{self._css[control_name]}")
233
+ return "\n\n".join(css_parts)
234
+
235
+ def get_all_css(self) -> str:
236
+ """Get base CSS plus any control-specific CSS for used controls."""
237
+ parts = []
238
+ if self._base_css:
239
+ parts.append(self._base_css)
240
+ used_css = self.get_used_css()
241
+ if used_css:
242
+ parts.append(used_css)
243
+ return "\n\n".join(parts)
244
+
245
+ def reset_used_controls(self) -> None:
246
+ """Reset the set of used controls (call before rendering a new face)."""
247
+ self._used_controls.clear()
248
+
249
+ def _get_jinja_env(self) -> Environment:
250
+ """Get or create Jinja environment with all template directories."""
251
+ if self._jinja_env is None:
252
+ # Templates are loaded directly by path, not by name lookup
253
+ # So we just need any directory for the loader (it's not used for discovery)
254
+ template_dirs = [str(self._builtin_dir)]
255
+
256
+ self._jinja_env = Environment(
257
+ loader=FileSystemLoader(template_dirs),
258
+ autoescape=select_autoescape(["html", "xml"]),
259
+ )
260
+
261
+ # Add custom filters
262
+ self._jinja_env.filters["js_escape"] = _js_escape_filter
263
+ self._jinja_env.filters["tojson"] = _tojson_filter
264
+
265
+ # Add useful globals
266
+ self._jinja_env.globals["range"] = range
267
+
268
+ return self._jinja_env
269
+
270
+
271
+ def _js_escape_filter(s: str) -> str:
272
+ """Escape string for JavaScript single-quoted string context."""
273
+ if s is None:
274
+ return ""
275
+ return json.dumps(str(s))[1:-1].replace("'", "\\'")
276
+
277
+
278
+ def _tojson_filter(obj: Any) -> str:
279
+ """Convert object to JSON string."""
280
+ return json.dumps(obj)
281
+
282
+
283
+ def _html_escape(s: str) -> str:
284
+ """HTML escape a string."""
285
+ import html
286
+
287
+ return html.escape(str(s))
@@ -0,0 +1,24 @@
1
+ """Format conversion functions for rendered output.
2
+
3
+ Stage: RENDER
4
+ Purpose: Convert SVG output to other formats (HTML, PNG, PDF).
5
+
6
+ This package provides format converters:
7
+ - html.py: SVG to HTML conversion (with wrapper page)
8
+ - png.py: SVG to PNG conversion (using svglib)
9
+ - pdf.py: SVG to PDF conversion (using svglib)
10
+
11
+ Dependencies:
12
+ - svglib (optional, for PNG/PDF export)
13
+ - reportlab (optional, for PNG/PDF export)
14
+ """
15
+
16
+ from dataface.core.render.converters.html import to_html
17
+ from dataface.core.render.converters.pdf import to_pdf
18
+ from dataface.core.render.converters.png import to_png
19
+
20
+ __all__ = [
21
+ "to_html",
22
+ "to_png",
23
+ "to_pdf",
24
+ ]
@@ -0,0 +1,276 @@
1
+ """Chart output conversion helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import json
7
+ import re
8
+ from collections.abc import Iterable
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from dataface.core.compile.sizing import DEFAULT_CHART_HEIGHT
12
+ from dataface.core.render.board_links import get_link_context, resolve_href
13
+ from dataface.core.render.chart.artifacts import RenderArtifact
14
+
15
+ if TYPE_CHECKING:
16
+ from dataface.core.compile.models.style.merged import MergedStyle
17
+ from dataface.core.render.chart.title_overflow import fix_title_alignment
18
+ from dataface.core.render.converters.pdf import to_pdf
19
+ from dataface.core.render.converters.png import to_png
20
+ from dataface.core.render.errors import ChartDataError, FormatError
21
+ from dataface.core.render.font_support import register_vl_convert_fonts
22
+
23
+ # Strip sentinel prefix from vl_convert-rendered chart href <a> elements.
24
+ # vl_convert uses xlink:href and mangles relative URLs, so we embed a sentinel
25
+ # prefix that we can detect and strip here. See standard_renderer._HREF_SENTINEL.
26
+ _SENTINEL_HREF_RE = re.compile(r'<a xlink:href="http://dft\.invalid([^"]*)"')
27
+
28
+
29
+ def _fix_chart_click_hrefs(svg: str) -> str:
30
+ """Convert sentinel xlink:href to plain href in SVG <a> elements.
31
+
32
+ Converts ``<a xlink:href="http://dft.invalid/path?q=v">``
33
+ to ``<a href="/path?q=v">`` so variables.js can intercept variable-update
34
+ clicks and the browser can follow navigation clicks directly.
35
+
36
+ When a link context is active (board resolver), board-root paths are
37
+ rewritten for the current runtime (serve vs Cloud). Query-string-only
38
+ links (?var=value) pass through unchanged — they are in-page variable
39
+ updates intercepted by variables.js, not cross-board navigation.
40
+ """
41
+ ctx = get_link_context()
42
+
43
+ def _replace(m: re.Match[str]) -> str:
44
+ url = m.group(1)
45
+ if ctx is not None:
46
+ url = resolve_href(url, ctx)
47
+ return f'<a href="{url}"'
48
+
49
+ return _SENTINEL_HREF_RE.sub(_replace, svg)
50
+
51
+
52
+ # Inject stroke-linecap="round" on legend-symbol <path> elements emitted by
53
+ # vl_convert. Vega-Lite has no spec-level surface for this (probed
54
+ # legend.symbolStrokeCap, config.legend.symbolStrokeCap, config.style.symbol.
55
+ # strokeCap, config.mark.strokeCap — all silently dropped), so the legend's
56
+ # dash segments render with butt caps while the chart lines use round caps
57
+ # (LineStyle theme default). The mismatch is most visible when the dash
58
+ # palette contains a dotted entry: round caps render 0-length dashes as
59
+ # circular dots, butt caps render them as nothing. Match the chart's cap by
60
+ # stamping round onto the legend paths during post-processing — but only on
61
+ # charts that actually use the strokeDash encoding (gated by the caller),
62
+ # so that charts with no dashes keep producing byte-identical SVG output.
63
+ _LEGEND_SYMBOL_LINECAP_RE = re.compile(
64
+ r'(class="[^"]*role-legend-symbol[^"]*"[^>]*><path)(?![^/]*stroke-linecap)([^/]*?)(/>)'
65
+ )
66
+
67
+
68
+ def _fix_legend_symbol_linecap(svg: str) -> str:
69
+ """Stamp ``stroke-linecap="round"`` onto legend-symbol paths."""
70
+ return _LEGEND_SYMBOL_LINECAP_RE.sub(r'\1 stroke-linecap="round"\2\3', svg)
71
+
72
+
73
+ def _spec_has_encoding(spec: dict[str, Any], channels: Iterable[str]) -> bool:
74
+ """True when ``spec`` carries any of ``channels`` in an encoding.
75
+
76
+ Recurses through ``spec["encoding"]``, ``spec["layer"]``, ``spec["hconcat"]``,
77
+ and ``spec["vconcat"]`` at every depth — deep enough to catch the
78
+ endpoint-label-rail wrapper that wraps a layered line chart inside
79
+ ``hconcat[0]``, and any future nested composition. Cheap gate used before
80
+ SVG post-processors so charts that don't use the channel keep producing
81
+ byte-identical SVG.
82
+ """
83
+ channel_set = frozenset(channels)
84
+
85
+ def _has(node: dict[str, Any]) -> bool:
86
+ if channel_set & node.get("encoding", {}).keys():
87
+ return True
88
+ for child in node.get("layer", []):
89
+ if isinstance(child, dict) and _has(child):
90
+ return True
91
+ for child in node.get("hconcat", []):
92
+ if isinstance(child, dict) and _has(child):
93
+ return True
94
+ for child in node.get("vconcat", []):
95
+ if isinstance(child, dict) and _has(child):
96
+ return True
97
+ return False
98
+
99
+ return _has(spec)
100
+
101
+
102
+ def _correct_concat_overshoot(
103
+ spec: dict[str, Any],
104
+ target_width: float | None,
105
+ target_height: float | None,
106
+ vlc: Any,
107
+ ) -> None:
108
+ """Two-pass width/height correction for hconcat/vconcat endpoint-label specs.
109
+
110
+ vl-convert ignores ``autosize: fit`` on concat children, so the first
111
+ render measures the actual SVG dimensions; the overshoot is subtracted from
112
+ the resizable pane(s) before the real render.
113
+
114
+ Width: hconcat shrinks pane[0] only (label pane is fixed-width). vconcat
115
+ shrinks both panes — they share the x scale and must resize together so rail
116
+ labels stay centered.
117
+
118
+ Height: hconcat shrinks both panes equally so neither governs a taller total.
119
+ vconcat height is handled upfront in _wrap_horizontal_top_row_rail, so
120
+ target_height is not expected on vconcat wrappers.
121
+ """
122
+ if target_width is None and target_height is None:
123
+ return
124
+ is_hconcat = "hconcat" in spec and spec["hconcat"]
125
+ is_vconcat = "vconcat" in spec and spec["vconcat"]
126
+ if not is_hconcat and not is_vconcat:
127
+ return
128
+ try:
129
+ probe_svg = vlc.vegalite_to_svg(spec)
130
+ except Exception: # noqa: BLE001, S110 — vl-convert throws untyped JS errors
131
+ return
132
+ if target_width is not None:
133
+ m = re.search(r"<svg[^>]*\bwidth=\"([0-9.]+)\"", probe_svg)
134
+ if m:
135
+ overshoot = float(m.group(1)) - float(target_width)
136
+ if overshoot > 0:
137
+ width_panes = (
138
+ [spec["hconcat"][0]] if is_hconcat else list(spec["vconcat"])
139
+ )
140
+ for pane in width_panes:
141
+ orig_w = float(pane.get("width", target_width))
142
+ new_w = orig_w - overshoot
143
+ if new_w > 0:
144
+ pane["width"] = new_w
145
+ if target_height is not None and is_hconcat:
146
+ m = re.search(r"<svg[^>]*\bheight=\"([0-9.]+)\"", probe_svg)
147
+ if m:
148
+ overshoot = float(m.group(1)) - float(target_height)
149
+ if overshoot > 0:
150
+ for pane in spec["hconcat"]:
151
+ orig_h = float(pane.get("height", target_height))
152
+ new_h = orig_h - overshoot
153
+ if new_h > 0:
154
+ pane["height"] = new_h
155
+
156
+
157
+ def render_svg_content(svg_content: str, format: str, *, scale: float = 1.0) -> str:
158
+ """Convert SVG content to the requested encoded output."""
159
+ if format == "svg":
160
+ return svg_content
161
+ if format == "png":
162
+ return base64.b64encode(to_png(svg_content, scale=scale)).decode("utf-8")
163
+ if format == "pdf":
164
+ return base64.b64encode(to_pdf(svg_content)).decode("utf-8")
165
+ raise ValueError(f"Unsupported SVG format: {format}")
166
+
167
+
168
+ def render_vega_spec(
169
+ spec: dict[str, Any],
170
+ format: str,
171
+ width: float | None,
172
+ height: float | None,
173
+ is_placeholder: bool,
174
+ resolved_style: MergedStyle | None = None,
175
+ ) -> str:
176
+ """Render a Vega-Lite spec into SVG, PNG, or PDF."""
177
+ try:
178
+ import vl_convert as vlc
179
+ except ImportError:
180
+ raise FormatError(
181
+ f"vl-convert-python is required for {format} rendering. "
182
+ "Install with: pip install vl-convert-python",
183
+ ) from None
184
+
185
+ register_vl_convert_fonts(vlc)
186
+
187
+ # Two-pass width/height correction for hconcat endpoint-label specs.
188
+ # vl-convert ignores autosize:fit on concat children, so the first render
189
+ # measures the actual SVG dimensions; the overshoots are subtracted from
190
+ # the resizable pane(s) before the real render. Both sentinels are stamped
191
+ # by _maybe_wrap_endpoint_label_pane and must be popped before rendering.
192
+ target_width = spec.pop("$df_target_width", None)
193
+ target_height = spec.pop("$df_target_height", None)
194
+ if target_width is not None or target_height is not None:
195
+ _correct_concat_overshoot(spec, target_width, target_height, vlc)
196
+
197
+ try:
198
+ svg_result = vlc.vegalite_to_svg(spec)
199
+ except Exception as exc:
200
+ # vl-convert JS errors (e.g. Vega scene-graph TypeErrors on unsupported
201
+ # layered specs) escape as Python exceptions. Re-raise as ChartDataError
202
+ # so render_chart_item records a per-tile error card instead of aborting
203
+ # the entire dashboard render process.
204
+ raise ChartDataError(str(exc)) from exc
205
+ svg_result = _fix_chart_click_hrefs(svg_result)
206
+ if _spec_has_encoding(spec, ["strokeDash"]):
207
+ svg_result = _fix_legend_symbol_linecap(svg_result)
208
+
209
+ # For hconcat specs (endpoint-label wrapper), padding lives on pane[0], not
210
+ # the wrapper root — the wrapper has no padding so spec.get("padding") returns
211
+ # nothing and fix_title_alignment would never run without this lookup.
212
+ padding = (
213
+ spec["hconcat"][0].get("padding")
214
+ if "hconcat" in spec and spec["hconcat"]
215
+ else spec.get("padding")
216
+ ) or {}
217
+ if isinstance(padding, dict):
218
+ padding_left = float(padding.get("left", 0) or 0)
219
+ elif isinstance(padding, (int, float)):
220
+ padding_left = float(padding)
221
+ else:
222
+ padding_left = 0.0
223
+ if padding_left > 0:
224
+ svg_result = fix_title_alignment(svg_result, padding_left)
225
+
226
+ if is_placeholder:
227
+ from dataface.core.compile.models.primitives import FontStyle
228
+ from dataface.core.render.placeholder import (
229
+ add_placeholder_overlay,
230
+ apply_placeholder_opacity,
231
+ )
232
+
233
+ assert resolved_style is not None
234
+ chart_width = width or 400
235
+ chart_height = height or DEFAULT_CHART_HEIGHT
236
+ svg_result = apply_placeholder_opacity(svg_result)
237
+ svg_result = add_placeholder_overlay(
238
+ svg_result,
239
+ chart_width,
240
+ chart_height,
241
+ font=FontStyle(family=resolved_style.font.family),
242
+ )
243
+
244
+ return render_svg_content(svg_result, format, scale=1.0)
245
+
246
+
247
+ def render_chart_artifact(
248
+ artifact: RenderArtifact,
249
+ format: str,
250
+ width: float | None,
251
+ height: float | None,
252
+ is_placeholder: bool = False,
253
+ resolved_style: MergedStyle | None = None,
254
+ ) -> str:
255
+ """Render a chart-domain artifact into the requested output format."""
256
+ if artifact.kind == "json":
257
+ if format != "json":
258
+ raise ValueError(f"JSON artifact cannot render as {format}")
259
+ return json.dumps(artifact.payload, indent=2)
260
+
261
+ if artifact.kind == "svg":
262
+ return render_svg_content(str(artifact.payload), format)
263
+
264
+ if artifact.kind == "vega_spec":
265
+ if not isinstance(artifact.payload, dict):
266
+ raise ValueError("Vega spec artifact payload must be a dictionary")
267
+ return render_vega_spec(
268
+ artifact.payload,
269
+ format,
270
+ width,
271
+ height,
272
+ is_placeholder=is_placeholder,
273
+ resolved_style=resolved_style,
274
+ )
275
+
276
+ raise ValueError(f"Unsupported artifact kind: {artifact.kind}")
@@ -0,0 +1,98 @@
1
+ """HTML format conversion.
2
+
3
+ Stage: RENDER
4
+ Purpose: Convert SVG output to HTML page with minimal wrapper.
5
+ """
6
+
7
+ import html as html_module
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from dataface.core.compile.models.face.compiled import Face, VariableValues
12
+ from dataface.core.execute.executor import Executor
13
+
14
+ _EMOJI_FONT_FACE_PATH = Path(__file__).parent.parent / "fonts" / "_emoji_font_face.css"
15
+
16
+
17
+ def to_html(
18
+ face: Face,
19
+ svg_content: str,
20
+ background: str | None,
21
+ executor: Executor,
22
+ variables: VariableValues,
23
+ **options: Any,
24
+ ) -> str:
25
+ """Convert SVG to HTML page with minimal wrapper.
26
+
27
+ SVG-First Migration: HTML format now simply wraps SVG output in an HTML
28
+ document. The SVG already contains:
29
+ - Interactive variable controls (via foreignObject)
30
+ - JavaScript for variable interactivity (via <script> tags)
31
+ - All chart rendering and layout
32
+
33
+ This approach:
34
+ - Eliminates duplicate HTML-specific rendering code paths
35
+ - Ensures consistent output between SVG and HTML formats
36
+ - Simplifies maintenance by having a single rendering path
37
+
38
+ Args:
39
+ face: Face being rendered
40
+ svg_content: The SVG content to wrap
41
+ background: Background color for the page
42
+ executor: Executor instance (unused, for API compatibility)
43
+ variables: Variable values (for title resolution)
44
+ Returns:
45
+ Complete HTML document as string
46
+
47
+ """
48
+ from dataface.core.compile.jinja import resolve_jinja_template
49
+
50
+ # Resolve title for <title> tag
51
+ page_title = face.title or "Dataface"
52
+ page_title = resolve_jinja_template(page_title, variables, strict=False)
53
+ escaped_title = html_module.escape(page_title)
54
+
55
+ font_family = face.resolved_style.font.family
56
+ emoji_font_face = _EMOJI_FONT_FACE_PATH.read_text()
57
+
58
+ page_bg = face.resolved_style.page.background
59
+ bg_style = f"background-color: {page_bg};" if page_bg else ""
60
+
61
+ return f"""<!DOCTYPE html>
62
+ <html>
63
+ <head>
64
+ <meta charset="UTF-8">
65
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
66
+ <title>{escaped_title}</title>
67
+ <style>
68
+ {emoji_font_face}
69
+ * {{
70
+ box-sizing: border-box;
71
+ }}
72
+ body {{
73
+ margin: 0;
74
+ padding: 0;
75
+ font-family: {font_family};
76
+ {bg_style}
77
+ }}
78
+ .dataface-wrapper {{
79
+ width: 100%;
80
+ margin: 0 auto;
81
+ }}
82
+ .dataface-svg-container {{
83
+ width: 100%;
84
+ }}
85
+ .dataface-svg-container svg {{
86
+ max-width: 100%;
87
+ height: auto;
88
+ }}
89
+ </style>
90
+ </head>
91
+ <body>
92
+ <div class="dataface-wrapper">
93
+ <div class="dataface-svg-container">
94
+ {svg_content}
95
+ </div>
96
+ </div>
97
+ </body>
98
+ </html>"""