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,467 @@
1
+ """Dataface renderer module.
2
+
3
+ Stage: RENDER
4
+ Purpose: Render compiled datafaces to various output formats.
5
+
6
+ Entry Points:
7
+ - render(face, executor, format, variables, **options) -> str | bytes
8
+ - render_chart(chart, data, **options) -> str
9
+
10
+ This is the main rendering orchestration module. It:
11
+ 1. Takes a Face from compile stage
12
+ 2. Delegates SVG rendering to faces.py
13
+ 3. Delegates format conversion to converters/
14
+ 4. Produces output in requested format
15
+
16
+ Dependencies:
17
+ - dataface.compile (for Face, Chart)
18
+ - dataface.execute (for Executor)
19
+ - .faces (for SVG rendering)
20
+ - .charts (for chart rendering)
21
+ - .converters (for format conversion)
22
+ """
23
+
24
+ import contextlib
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+ from dataface.core.compile.config import (
29
+ get_config,
30
+ get_project_warnings_ignore,
31
+ get_rendering_config,
32
+ )
33
+ from dataface.core.compile.errors import DatafaceError
34
+ from dataface.core.compile.models.face.compiled import Face, VariableValues
35
+ from dataface.core.compile.normalizer import sync_face_resolved_style
36
+ from dataface.core.compile.variables import parse_variable_json_strings
37
+ from dataface.core.errors import StructuredError
38
+ from dataface.core.execute.batch import collect_layout_chart_query_names
39
+ from dataface.core.execute.executor import Executor
40
+ from dataface.core.execute.parallel import (
41
+ execute_queries_parallel,
42
+ execute_queries_sequential,
43
+ plan_query_execution_phases,
44
+ )
45
+ from dataface.core.render.chart.renderers import SVG_RENDERERS
46
+ from dataface.core.render.converters import to_html, to_pdf, to_png
47
+ from dataface.core.render.errors import (
48
+ FormatError,
49
+ MissingRequiredVariablesError,
50
+ MissingVariable,
51
+ RenderError,
52
+ )
53
+ from dataface.core.render.faces import render_face_svg
54
+ from dataface.core.render.layout_sizing import calculate_data_aware_layout
55
+ from dataface.core.render.render_result import RenderResult
56
+ from dataface.core.render.warnings import (
57
+ RenderWarning,
58
+ WarningContext,
59
+ registry as _warnings_registry,
60
+ run_all,
61
+ )
62
+ from dataface.core.render.warnings.suppression import partition as _partition_warnings
63
+ from dataface.core.resolve_face import resolve_face
64
+
65
+ # Formats that produce SVG output (require vl-convert chart rendering).
66
+ _SVG_FORMATS = frozenset({"svg", "html", "png", "pdf"})
67
+
68
+ # Chart types that do NOT produce a Vega-Lite spec (own SVG renderer).
69
+ # vega_specs in WarningContext is sparse: these chart ids are omitted.
70
+ _NON_VEGA_CHART_TYPES = frozenset(SVG_RENDERERS.keys())
71
+
72
+
73
+ def _collect_render_warnings(
74
+ face: Face,
75
+ executor: Executor,
76
+ variables: VariableValues,
77
+ ) -> list[RenderWarning]:
78
+ """Build WarningContext from cached query results and run all detectors.
79
+
80
+ Called after queries have already executed (results are in the executor
81
+ cache). Only charts reachable from the layout tree are included — orphan
82
+ charts (present in face.charts but absent from the layout) were never
83
+ pre-executed and are intentionally excluded so this function never
84
+ triggers a fresh adapter call.
85
+ """
86
+ # Fast path: skip all spec/query work when no detectors are registered.
87
+ # Avoids double-building vega specs on every SVG render for zero benefit.
88
+ if not _warnings_registry.DETECTORS:
89
+ return []
90
+
91
+ # Restrict to charts the layout actually renders — same set that
92
+ # execute_queries_parallel / execute_queries_sequential pre-executed.
93
+ # Orphan charts (in face.charts but absent from the layout) were never
94
+ # pre-cached; including them would trigger a fresh adapter call.
95
+ pre_executed_query_names = collect_layout_chart_query_names(face)
96
+
97
+ chart_results: dict[str, list[dict[str, Any]]] = {}
98
+ for chart_id, chart in face.charts.items():
99
+ if chart.query_name not in pre_executed_query_names:
100
+ continue
101
+ # Omit failed charts so detectors can distinguish failure from genuine
102
+ # zero rows: absent from chart_results = failed execute.
103
+ with contextlib.suppress(
104
+ Exception
105
+ ): # noqa: BLE001 — query failure already recorded elsewhere
106
+ chart_results[chart_id] = executor.execute_chart(chart, variables)
107
+
108
+ from dataface.core.render.chart import generate_vega_lite_spec
109
+
110
+ vega_specs: dict[str, dict[str, Any]] = {}
111
+ for chart_id, chart in face.charts.items():
112
+ if chart.query_name not in pre_executed_query_names:
113
+ continue
114
+ if chart.type in _NON_VEGA_CHART_TYPES:
115
+ continue
116
+ data = chart_results.get(chart_id, [])
117
+ # Spec generation failure must not block the render — omit from vega_specs.
118
+ with contextlib.suppress(
119
+ Exception
120
+ ): # noqa: BLE001 — spec failure must not block render
121
+ vega_specs[chart_id] = generate_vega_lite_spec(chart, data)
122
+
123
+ ctx = WarningContext(
124
+ face_spec=face, chart_results=chart_results, vega_specs=vega_specs
125
+ )
126
+ return run_all(ctx)
127
+
128
+
129
+ def render(
130
+ face: Face,
131
+ executor: Executor,
132
+ format: str = "svg",
133
+ variables: VariableValues | None = None,
134
+ ignore_codes: set[str] | None = None,
135
+ project_dir: Path | None = None,
136
+ **options: Any,
137
+ ) -> RenderResult:
138
+ """Render a compiled dataface.
139
+
140
+ Stage: RENDER (Main Entry Point)
141
+
142
+ This is the main rendering function. It walks the layout structure,
143
+ renders each chart (triggering lazy query execution), and produces
144
+ output in the requested format.
145
+
146
+ Args:
147
+ face: Compiled dataface to render
148
+ executor: Executor for query execution
149
+ format: Output format (svg, html, png, pdf, terminal, json, text, yaml)
150
+ variables: Variable values for queries
151
+ ignore_codes: Caller-supplied set of warning codes to suppress (CLI seam).
152
+ project_dir: Project directory for reading dataface.yml warnings.ignore.
153
+ **options: Format-specific options
154
+ - background: Background color
155
+ - scale: Scale factor (for png)
156
+ - grid: Show grid overlay (for debugging)
157
+
158
+ Returns:
159
+ RenderResult with:
160
+ - output: rendered content (str or bytes)
161
+ - chart_errors: per-chart runtime failures (face still rendered)
162
+ - face_error: post-validation fatal (None when render succeeded)
163
+ - render_warnings: active (non-suppressed) warnings
164
+ - suppressed_warnings: warnings dropped by any ignore layer
165
+
166
+ Raises:
167
+ RenderError: If a face-level invariant is violated before rendering starts
168
+ FormatError: If format is unknown
169
+ """
170
+ # Compile warns on orphans; render hard-fails the empty-layout-with-charts
171
+ # case so the dashboard never silently renders with nothing visible.
172
+ if face.charts and not face.layout.items:
173
+ from dataface.core.errors import DF_RENDER_NO_LAYOUT
174
+
175
+ chart_list = ", ".join(sorted(face.charts.keys()))
176
+ raise RenderError.from_code(DF_RENDER_NO_LAYOUT, charts=chart_list)
177
+
178
+ # Trust the normalizer - use pre-computed variable_defaults
179
+ variable_registry = face.variable_registry or {}
180
+
181
+ # Merge variables: start with None for all vars, then defaults, then user values
182
+ all_variables: dict[str, Any] = dict.fromkeys(variable_registry)
183
+ all_variables.update(face.variable_defaults) # Pre-computed by normalizer
184
+ # Parse JSON strings in variables (from URL parameters) and merge
185
+ parsed_variables = parse_variable_json_strings(variables or {})
186
+ merged_variables = {**all_variables, **parsed_variables}
187
+
188
+ # Face-level precondition: required variables must have a value before any query runs.
189
+ # None, empty string, and empty list all count as "not provided" — URL params arrive
190
+ # as strings so ?var= produces "" which is as unscoped as no value at all.
191
+ if variable_registry:
192
+
193
+ def _is_absent(v: Any) -> bool:
194
+ if v is None:
195
+ return True
196
+ if isinstance(v, str):
197
+ return not v.strip()
198
+ if isinstance(v, (list, tuple, set)):
199
+ return len(v) == 0
200
+ return False
201
+
202
+ missing = [
203
+ MissingVariable(
204
+ key=key,
205
+ label=var.label,
206
+ description=var.description,
207
+ input_type=var.input,
208
+ )
209
+ for key, var in variable_registry.items()
210
+ if var.required is True and _is_absent(merged_variables.get(key))
211
+ ]
212
+ if missing:
213
+ raise MissingRequiredVariablesError(missing)
214
+
215
+ # Pre-render query execution — authoritative, not a fallback.
216
+ # Queries run BEFORE calculate_data_aware_layout so the sizing pass can
217
+ # use cached results (render-first sizing reads query data from the executor
218
+ # cache without re-executing).
219
+ # Phase 1: chart-direct queries without {{ results.X }} run in parallel.
220
+ # Phase 2: chart-direct {{ results.X }} queries run sequentially once
221
+ # their upstream results are cached.
222
+ # Errors are stored on the executor (executor._query_errors) so that
223
+ # execute_chart() during the render walk raises the stored error
224
+ # instead of re-executing. The render walk never retries a failed query.
225
+ query_names = collect_layout_chart_query_names(face)
226
+ parallel_query_names, sequential_query_names = plan_query_execution_phases(
227
+ face, query_names
228
+ )
229
+ execute_queries_parallel(executor, face, parallel_query_names, merged_variables)
230
+ execute_queries_sequential(executor, sequential_query_names, merged_variables)
231
+
232
+ # Run warning detectors now that queries are cached.
233
+ # Partition into active vs suppressed using the union of three ignore layers.
234
+ _all_warnings = _collect_render_warnings(face, executor, merged_variables)
235
+ _project_codes = get_project_warnings_ignore(project_dir)
236
+ _per_chart_codes: dict[str, set[str]] = {
237
+ chart_id: set(chart.warnings_ignore)
238
+ for chart_id, chart in face.charts.items()
239
+ if chart.warnings_ignore
240
+ }
241
+ render_warnings, suppressed_warnings = _partition_warnings(
242
+ _all_warnings,
243
+ cli_codes=ignore_codes or set(),
244
+ project_codes=set(_project_codes),
245
+ per_chart_codes=_per_chart_codes,
246
+ )
247
+
248
+ # Sync face.resolved_style (and all nested sub-board faces) from face.theme
249
+ # before layout sizing so calculate_data_aware_layout reads the live value.
250
+ sync_face_resolved_style(face)
251
+
252
+ # Calculate layout with data awareness — table heights use actual row counts,
253
+ # and Vega-Lite charts are rendered to get true heights (render-first sizing).
254
+ # Render-first sizing is skipped for non-SVG formats (yaml/json/text) since
255
+ # those formats don't render charts and don't benefit from actual heights.
256
+ # Returns (face, render_cache) where render_cache holds pre-rendered SVGs.
257
+ face, render_cache = calculate_data_aware_layout(
258
+ face,
259
+ executor,
260
+ merged_variables,
261
+ render_first=format in _SVG_FORMATS,
262
+ )
263
+
264
+ # Resolve the sized face: bake board config constants, style, and Vega config.
265
+ # Called after data-aware sizing so resolved dimensions reflect actual heights.
266
+ config = get_config()
267
+ resolved_face = resolve_face(face, config)
268
+
269
+ # Resolve SVG canvas background: API override wins, otherwise use the
270
+ # cascaded face background (theme default or authored override, already merged).
271
+ override = options.get("background")
272
+ if override is not None:
273
+ background = None if override == "transparent" else override
274
+ else:
275
+ resolved_bg = resolved_face.style.background
276
+ background = None if resolved_bg == "transparent" else resolved_bg
277
+
278
+ # Per-chart error collector: single append site in render_chart_item.
279
+ # All formats share this collector so callers get chart_errors regardless of format.
280
+ error_collector: list[StructuredError] = []
281
+
282
+ # JSON/text formats: skip SVG rendering entirely — walk layout tree directly
283
+ if format == "json":
284
+ from dataface.core.render.json_format import render_face_json
285
+
286
+ output = render_face_json(
287
+ face, executor, merged_variables, error_collector=error_collector
288
+ )
289
+ return RenderResult(
290
+ output=output,
291
+ chart_errors=error_collector,
292
+ render_warnings=render_warnings,
293
+ suppressed_warnings=suppressed_warnings,
294
+ )
295
+
296
+ if format == "text":
297
+ from dataface.core.render.text_format import render_face_text
298
+
299
+ output = render_face_text(
300
+ face, executor, merged_variables, error_collector=error_collector
301
+ )
302
+ return RenderResult(
303
+ output=output,
304
+ chart_errors=error_collector,
305
+ render_warnings=render_warnings,
306
+ suppressed_warnings=suppressed_warnings,
307
+ )
308
+
309
+ if format == "yaml":
310
+ from dataface.core.render.yaml_format import render_face_yaml
311
+
312
+ output = render_face_yaml(
313
+ face, executor, merged_variables, error_collector=error_collector
314
+ )
315
+ return RenderResult(
316
+ output=output,
317
+ chart_errors=error_collector,
318
+ render_warnings=render_warnings,
319
+ suppressed_warnings=suppressed_warnings,
320
+ )
321
+
322
+ # Render layout to SVG
323
+ # Format determines whether variables are interactive (foreignObject) or read-only (static text)
324
+ # PNG/PDF export requires read-only because svglib doesn't support foreignObject
325
+ # HTML format supports foreignObject, so it should also get interactive variables
326
+ interactive = format in ("svg", "html")
327
+
328
+ # Board link rewriting: set context for the duration of this render pass
329
+ from dataface.core.render.board_links import set_link_context
330
+
331
+ link_context = options.get("link_context")
332
+ set_link_context(link_context)
333
+
334
+ try:
335
+ grid_enabled = options.get("grid", False)
336
+ margins_enabled = options.get("margins", False)
337
+ svg_content = render_face_svg(
338
+ resolved_face,
339
+ executor,
340
+ merged_variables,
341
+ background,
342
+ grid_enabled,
343
+ interactive,
344
+ render_cache=render_cache,
345
+ margins=margins_enabled,
346
+ error_collector=error_collector,
347
+ )
348
+ except DatafaceError as e:
349
+ # Face-level fatal: a DF-RENDER-* code escaped chart isolation
350
+ # (e.g. DF_RENDER_NO_LAYOUT, MissingRequiredVariablesError). Surface as
351
+ # face_error so callers see the structured code unwrapped.
352
+ set_link_context(None)
353
+ return RenderResult(
354
+ output=None,
355
+ chart_errors=error_collector,
356
+ face_error=e.to_structured(),
357
+ render_warnings=render_warnings,
358
+ suppressed_warnings=suppressed_warnings,
359
+ )
360
+ except Exception as e: # noqa: BLE001
361
+ from dataface.core.errors import DF_RENDER_INTERNAL
362
+
363
+ set_link_context(None)
364
+ wrapped = RenderError.from_code(DF_RENDER_INTERNAL, inner_message=str(e))
365
+ return RenderResult(
366
+ output=None,
367
+ chart_errors=error_collector,
368
+ face_error=wrapped.to_structured(),
369
+ render_warnings=render_warnings,
370
+ suppressed_warnings=suppressed_warnings,
371
+ )
372
+ finally:
373
+ set_link_context(None)
374
+
375
+ # Convert to requested format
376
+ if format == "svg":
377
+ return RenderResult(
378
+ output=svg_content,
379
+ chart_errors=error_collector,
380
+ render_warnings=render_warnings,
381
+ suppressed_warnings=suppressed_warnings,
382
+ )
383
+
384
+ elif format == "html":
385
+ html_output = to_html(
386
+ face,
387
+ svg_content,
388
+ background,
389
+ executor,
390
+ merged_variables,
391
+ )
392
+ return RenderResult(
393
+ output=html_output,
394
+ chart_errors=error_collector,
395
+ render_warnings=render_warnings,
396
+ suppressed_warnings=suppressed_warnings,
397
+ )
398
+
399
+ elif format == "png":
400
+ png_scale = options.get("scale")
401
+ if png_scale is None:
402
+ png_scale = get_rendering_config().png.scale
403
+ return RenderResult(
404
+ output=to_png(svg_content, png_scale),
405
+ chart_errors=error_collector,
406
+ render_warnings=render_warnings,
407
+ suppressed_warnings=suppressed_warnings,
408
+ )
409
+
410
+ elif format == "pdf":
411
+ return RenderResult(
412
+ output=to_pdf(svg_content),
413
+ chart_errors=error_collector,
414
+ render_warnings=render_warnings,
415
+ suppressed_warnings=suppressed_warnings,
416
+ )
417
+
418
+ elif format == "terminal":
419
+ return RenderResult(
420
+ output=_to_terminal(face, executor, merged_variables, **options),
421
+ chart_errors=error_collector,
422
+ render_warnings=render_warnings,
423
+ suppressed_warnings=suppressed_warnings,
424
+ )
425
+
426
+ else:
427
+ from dataface.core.errors import DF_RENDER_FORMAT_UNSUPPORTED
428
+
429
+ raise FormatError.from_code(DF_RENDER_FORMAT_UNSUPPORTED, format=format)
430
+
431
+
432
+ def _to_terminal(
433
+ face: Face,
434
+ executor: Executor,
435
+ variables: VariableValues,
436
+ **options: Any,
437
+ ) -> str:
438
+ """Render face to terminal output.
439
+
440
+ Args:
441
+ face: Face to render
442
+ executor: Executor for query execution
443
+ variables: Variable values for queries
444
+ **options: Terminal-specific options
445
+ - width: Terminal width in characters
446
+ - height: Terminal height in characters
447
+ - colors: Whether to use ANSI colors (default: True)
448
+
449
+ Returns:
450
+ Terminal-formatted string
451
+ """
452
+ from dataface.core.render.terminal import render_face_terminal
453
+
454
+ return render_face_terminal(
455
+ face,
456
+ executor,
457
+ variables=variables,
458
+ width=options.get("width"),
459
+ height=options.get("height"),
460
+ colors=options.get("colors", True),
461
+ **{k: v for k, v in options.items() if k not in ("width", "height", "colors")},
462
+ )
463
+
464
+
465
+ __all__ = [
466
+ "render",
467
+ ]
@@ -0,0 +1,16 @@
1
+ """Helpers for embedding JavaScript into rendered SVG output."""
2
+
3
+
4
+ def escape_cdata(content: str) -> str:
5
+ """Escape sequences that would terminate a CDATA section."""
6
+ return content.replace("]]>", "]]]]><![CDATA[>")
7
+
8
+
9
+ def embed_svg_script(script: str) -> str:
10
+ """Wrap JavaScript for safe inline SVG execution."""
11
+ script = escape_cdata(script)
12
+ return f"""<script type="text/javascript">
13
+ <![CDATA[
14
+ {script}
15
+ ]]>
16
+ </script>"""
@@ -0,0 +1,212 @@
1
+ """SVG utility functions for rendering.
2
+
3
+ Stage: RENDER
4
+ Purpose: Provide SVG-specific utilities for dataface rendering.
5
+
6
+ This module handles:
7
+ - Grid pattern generation (loaded from template file)
8
+ - SVG styles generation (loaded from template file)
9
+ - Title rendering (using mdsvg)
10
+ - Error message rendering (using template file)
11
+ - SVG content extraction and dimension parsing
12
+
13
+ Dependencies:
14
+ - .template_loader (for Jinja2 template loading)
15
+ - .themes (for theme colors)
16
+ - mdsvg (required, for markdown rendering and text wrapping)
17
+ """
18
+
19
+ import re
20
+ from dataclasses import dataclass
21
+ from typing import TYPE_CHECKING, Literal
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Pixel snapping
25
+ # ---------------------------------------------------------------------------
26
+
27
+
28
+ def _px(v: float) -> int:
29
+ """Snap a float coordinate to the nearest integer pixel.
30
+
31
+ SVG transforms are applied before pixel rasterization: a sub-pixel ancestor
32
+ translate pushes a 1px-tall rule onto fractional rows and the rasterizer
33
+ splits it across two pixels at reduced opacity — reading visually as "thin"
34
+ or "faded". Rounding each emitted translate keeps every cumulative transform
35
+ on an integer boundary, so rules, rects, and other 1px structural marks stay
36
+ crisp.
37
+
38
+ Uses Python's built-in ``round()``, which applies banker's rounding
39
+ (round-half-to-even): ``_px(24.5) == 24``, ``_px(25.5) == 26``. Values
40
+ that fall exactly on a .5 boundary may round either direction; this is
41
+ non-monotonic but produces the least cumulative drift over long sequences.
42
+ """
43
+ return int(round(v))
44
+
45
+
46
+ if TYPE_CHECKING:
47
+ from dataface.core.compile.models.style.merged import MergedStyle
48
+
49
+ from dataface.core.render.template_loader import render_template
50
+
51
+ # Compiled regex patterns for SVG parsing - avoids recompilation on each call
52
+ _SVG_CONTENT_RE = re.compile(r"<svg[^>]*>(.*)</svg>", re.DOTALL)
53
+ _SVG_WIDTH_RE = re.compile(r'width=["\'](\d+(?:\.\d+)?)["\']')
54
+ _SVG_HEIGHT_RE = re.compile(r'height=["\'](\d+(?:\.\d+)?)["\']')
55
+ _SVG_VIEWBOX_RE = re.compile(
56
+ r'viewBox=["\']0\s+0\s+(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)["\']'
57
+ )
58
+
59
+
60
+ @dataclass
61
+ class SVGDimensions:
62
+ """Dimensions extracted from an SVG string."""
63
+
64
+ width: float
65
+ height: float
66
+
67
+
68
+ def extract_svg_inner_content(svg: str) -> str:
69
+ """Extract inner content from an SVG string (strip the <svg> wrapper).
70
+
71
+ Args:
72
+ svg: Full SVG string with wrapper
73
+
74
+ Returns:
75
+ Inner content without <svg>...</svg> wrapper
76
+
77
+ Raises:
78
+ ValueError: If SVG content cannot be extracted
79
+
80
+ TODO(mdsvg#10): Replace with structured RenderResult when available.
81
+ See: https://github.com/davefowler/markdown-svg/issues/10
82
+ """
83
+ match = _SVG_CONTENT_RE.search(svg)
84
+ if not match:
85
+ raise ValueError(f"Failed to extract SVG content from: {svg[:100]}...")
86
+ return match.group(1)
87
+
88
+
89
+ def extract_svg_dimensions(
90
+ svg: str, default_width: float = 0.0, default_height: float = 0.0
91
+ ) -> SVGDimensions:
92
+ """Extract width and height from an SVG string.
93
+
94
+ Checks viewBox first (more reliable for Vega-Lite output), then falls back
95
+ to width/height attributes.
96
+
97
+ Args:
98
+ svg: SVG string to parse
99
+ default_width: Default width if not found
100
+ default_height: Default height if not found
101
+
102
+ Returns:
103
+ SVGDimensions with extracted or default values
104
+ """
105
+ # Prefer viewBox dimensions (more reliable for Vega-Lite output)
106
+ viewbox_match = _SVG_VIEWBOX_RE.search(svg)
107
+ if viewbox_match:
108
+ return SVGDimensions(
109
+ width=float(viewbox_match.group(1)),
110
+ height=float(viewbox_match.group(2)),
111
+ )
112
+
113
+ # Fall back to explicit width/height attributes
114
+ width = default_width
115
+ height = default_height
116
+
117
+ width_match = _SVG_WIDTH_RE.search(svg)
118
+ if width_match:
119
+ width = float(width_match.group(1))
120
+
121
+ height_match = _SVG_HEIGHT_RE.search(svg)
122
+ if height_match:
123
+ height = float(height_match.group(1))
124
+
125
+ return SVGDimensions(width=width, height=height)
126
+
127
+
128
+ def create_grid_pattern() -> str:
129
+ """Create an SVG pattern definition for a 100x100 grid.
130
+
131
+ Returns:
132
+ SVG <pattern> element for 100x100 grid overlay.
133
+ """
134
+ return render_template("svg/grid_pattern.svg")
135
+
136
+
137
+ def generate_svg_styles(
138
+ emoji_mode: Literal["monochrome", "color", "system-default", "disabled"],
139
+ ) -> str:
140
+ """Generate CSS styles for SVG charts and interactions.
141
+
142
+ Args:
143
+ emoji_mode: One of monochrome / color / system-default / disabled.
144
+ Controls whether the @font-face emoji partial and font-variant-emoji
145
+ rule are included in the embedded SVG stylesheet.
146
+
147
+ Returns:
148
+ SVG <style> element with CSS rules
149
+ """
150
+ return render_template("svg/styles.css", emoji_mode=emoji_mode)
151
+
152
+
153
+ def render_title(
154
+ title: str,
155
+ width: float,
156
+ text_align: Literal["left", "center", "right"] = "left",
157
+ *,
158
+ level: int = 1,
159
+ prose: bool = False,
160
+ resolved_style: "MergedStyle | None" = None,
161
+ ) -> str:
162
+ """Render face title using mdsvg for proper text handling.
163
+
164
+ Font size is determined by card pixel width (width-based tier).
165
+ When ``prose=True`` the title uses the serif family (Source Serif 4)
166
+ regardless of width, matching the prose body text.
167
+
168
+ Args:
169
+ title: Title text to render
170
+ width: Available width (drives tier selection: narrow/medium/wide)
171
+ text_align: Text alignment ("left", "center", "right")
172
+ prose: When True, render in serif to match prose body text.
173
+
174
+ Returns:
175
+ SVG string for the title
176
+ """
177
+ from dataface.core.compile.config import get_config
178
+ from dataface.core.compile.models.style.merged import resolve_style
179
+ from dataface.core.compile.sizing import get_compact_style
180
+ from dataface.core.compile.typography import face_title_markdown
181
+ from dataface.core.fonts import get_inter_font_path
182
+ from mdsvg import render as render_markdown
183
+
184
+ markdown_title, h1_size, heading_weight = face_title_markdown(
185
+ title, width, level=level
186
+ )
187
+
188
+ if prose:
189
+ assert resolved_style is not None
190
+ font_family = str(resolved_style.title.font.family)
191
+ else:
192
+ font_family = None
193
+ _ms = (
194
+ resolved_style
195
+ if resolved_style is not None
196
+ else resolve_style(get_config().style)
197
+ )
198
+ style = get_compact_style(
199
+ _ms,
200
+ text_align=text_align,
201
+ h1_size=h1_size,
202
+ heading_font_weight=heading_weight,
203
+ font_family=font_family,
204
+ )
205
+ font_path = str(get_inter_font_path())
206
+ return render_markdown(
207
+ markdown_title,
208
+ width=width,
209
+ padding=0.0,
210
+ style=style,
211
+ font_path=font_path,
212
+ )