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,865 @@
1
+ """Data-aware layout sizing for the render pipeline.
2
+
3
+ Stage: RENDER
4
+ Purpose: Calculate layout dimensions using actual query data and rendered chart heights.
5
+
6
+ This module owns all data-aware sizing logic that requires an executor or vl-convert:
7
+ - Render-first Vega sizing (render charts to get true heights, cache SVGs)
8
+ - Table sizing from actual row counts
9
+ - Cols height alignment (re-render shorter Vega items at max height)
10
+
11
+ It injects a HeightProvider callback into the pure layout algorithms in
12
+ compile/sizing.py, keeping the compile module free of render/execute imports.
13
+
14
+ Entry Point:
15
+ - calculate_data_aware_layout(face, executor, variables, render_first)
16
+ Called from renderer.py after query pre-execution.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ from dataclasses import dataclass, field
23
+ from typing import TYPE_CHECKING, Any
24
+
25
+ from dataface.core.compile.models.chart.compiled import (
26
+ NON_ASPECT_RATIO_TYPES,
27
+ Chart,
28
+ )
29
+ from dataface.core.compile.models.face.compiled import Face, Layout, LayoutItem
30
+ from dataface.core.compile.sizing import (
31
+ HeightProvider,
32
+ calculate_layout_height,
33
+ calculate_layout_items,
34
+ get_chart_content_height,
35
+ get_face_gap,
36
+ get_item_content_height,
37
+ get_title_height,
38
+ )
39
+ from dataface.core.execute.errors import ExecutionError
40
+ from dataface.core.render.chart.render_single import RenderCache
41
+ from dataface.core.render.chart.spec_builders import additive_padding
42
+ from dataface.core.render.errors import ChartDataError
43
+
44
+ if TYPE_CHECKING:
45
+ from dataface.core.compile.models.style.merged import MergedStyle
46
+ from dataface.core.execute.executor import Executor
47
+
48
+ _log = logging.getLogger(__name__)
49
+ _FIXED_ASPECT_PIE_TYPES = frozenset({"pie", "arc"})
50
+
51
+
52
+ @dataclass
53
+ class SizingRenderCtx:
54
+ """Render context threaded through data-aware sizing.
55
+
56
+ When present, the data-aware height provider renders Vega-Lite charts via
57
+ vl-convert instead of using aspect-ratio estimates. Results are cached
58
+ in render_cache for reuse by the render pass.
59
+ """
60
+
61
+ resolved_style: MergedStyle
62
+ render_cache: RenderCache = field(default_factory=dict)
63
+ vega_config: dict[str, Any] | None = None
64
+ executor: Executor | None = None
65
+ variables: dict[str, Any] | None = None
66
+ # Heading level of the face containing the charts being sized (root=1, nested=2, …).
67
+ # Chart title uses face_level + 1; sizer and renderer must agree.
68
+ face_level: int = 1
69
+ # Maps chart_id → corrected spec.width for data_table charts.
70
+ # autosize:pad makes outer SVG wider than spec.width by a constant overhead;
71
+ # the render-first pass measures this and stores the shrunk spec.width here so
72
+ # _align_cols_heights can reuse it instead of item.width (which would re-overflow).
73
+ data_table_corrected_widths: dict[str, float] = field(default_factory=dict)
74
+ # Maps (chart_id, slot_width) → natural rendered height.
75
+ # Populated during the sizing pass; used to deduplicate renders of the same
76
+ # chart at the same width and to drive the close-enough skip in _align_cols_heights.
77
+ natural_heights: dict[tuple[str, float], float] = field(default_factory=dict)
78
+
79
+
80
+ def _nested_render_ctx(
81
+ render_ctx: SizingRenderCtx,
82
+ nested_face: Face,
83
+ ) -> SizingRenderCtx:
84
+ """Build a SizingRenderCtx for a nested face, inheriting the shared cache."""
85
+ from dataface.core.compile.vega_config import compile_effective_vega_config
86
+
87
+ return SizingRenderCtx(
88
+ render_cache=render_ctx.render_cache,
89
+ natural_heights=render_ctx.natural_heights,
90
+ vega_config=compile_effective_vega_config(nested_face.theme),
91
+ resolved_style=nested_face.resolved_style,
92
+ executor=render_ctx.executor,
93
+ variables=render_ctx.variables,
94
+ face_level=nested_face.level,
95
+ data_table_corrected_widths=render_ctx.data_table_corrected_widths,
96
+ )
97
+
98
+
99
+ def _get_table_height_from_data(
100
+ chart: Chart,
101
+ executor: Executor,
102
+ variables: dict[str, Any] | None,
103
+ resolved_style: MergedStyle,
104
+ width: float | None = None,
105
+ face_level: int = 1,
106
+ ) -> float:
107
+ """Calculate table height from actual row count.
108
+
109
+ ``resolved_style`` is the face's resolved style (the same value the renderer
110
+ receives via ``board_style``). Face-level and chart-local overrides are merged
111
+ via ``build_resolved_style`` so sizer and renderer agree on row heights,
112
+ padding, and header dimensions.
113
+
114
+ ``width`` drives the title font size (via ``chart_title_spec``) and must
115
+ match what the renderer will see at layout time. When ``None``, falls back
116
+ to the renderer's own ``TABLE_DEFAULT_WIDTH`` so both stay in lockstep.
117
+
118
+ If the query fails, sizes for an error message display.
119
+ """
120
+ from dataface.core.compile.style_cascade import build_resolved_style
121
+ from dataface.core.render.chart.table import (
122
+ TABLE_DEFAULT_WIDTH,
123
+ compute_table_title_block_layout,
124
+ )
125
+
126
+ effective_charts = build_resolved_style(resolved_style, chart.style)
127
+ tc = effective_charts.table # TableChartStyle — face-merged, all with defaults
128
+
129
+ row_height = int(tc.row.height)
130
+ # When the header row is hidden, it contributes no vertical extent —
131
+ # matches the renderer's `if not tc.header.visible: header_height = 0` branch.
132
+ header_height = int(tc.header.height) if tc.header.visible else 0
133
+ padding_y = int(tc.outer_padding)
134
+ title_height = compute_table_title_block_layout(
135
+ chart_title=chart.title,
136
+ chart_subtitle=chart.subtitle,
137
+ table_width=width if width is not None else TABLE_DEFAULT_WIDTH,
138
+ tc=tc,
139
+ padding=padding_y,
140
+ title_style=effective_charts.title,
141
+ resolved_chart_style=effective_charts,
142
+ face_level=face_level,
143
+ ).height
144
+ bottom_padding = int(tc.bottom_padding)
145
+
146
+ # Chart-local pagination is pre-merged into effective_charts.pagination by
147
+ # build_resolved_style — read the resolved page_size off the merged value.
148
+ pagination = effective_charts.pagination
149
+ resolved_page_size = (
150
+ pagination.page_size if pagination is not None and pagination.enabled else None
151
+ )
152
+
153
+ # Header-body gap also vanishes when header is hidden — the renderer
154
+ # uses the same zero-out so sizer + renderer agree on total height.
155
+ header_body_gap = int(row_height * 0.25) if tc.header.visible else 0
156
+
157
+ try:
158
+ data = executor.execute_chart(chart, variables)
159
+ row_count = len(data)
160
+ except ExecutionError as exc:
161
+ # Migrate-or-fail: only the query-failed signal warrants a static height
162
+ # fallback. ValueError/OSError/RuntimeError/LookupError are bug-class —
163
+ # let them surface loudly so the underlying bug is fixed, not masked.
164
+ _log.warning("Table height estimation failed for chart %r: %s", chart.id, exc)
165
+ return (
166
+ title_height
167
+ + header_height
168
+ + header_body_gap
169
+ + row_height
170
+ + padding_y
171
+ + bottom_padding
172
+ )
173
+
174
+ multi_page = resolved_page_size is not None and row_count > resolved_page_size
175
+ if resolved_page_size is not None:
176
+ row_count = min(row_count, resolved_page_size)
177
+ height = (
178
+ title_height
179
+ + header_height
180
+ + header_body_gap
181
+ + (row_count * row_height)
182
+ + padding_y
183
+ + bottom_padding
184
+ )
185
+ if multi_page:
186
+ from dataface.core.render.chart.table import _PAGINATION_CONTROL_HEIGHT
187
+
188
+ height += _PAGINATION_CONTROL_HEIGHT
189
+ return height
190
+
191
+
192
+ def _make_data_aware_height_provider(
193
+ render_ctx: SizingRenderCtx,
194
+ executor: Executor,
195
+ variables: dict[str, Any] | None,
196
+ card_padding: float,
197
+ ) -> HeightProvider:
198
+ """Create a HeightProvider that uses render-first sizing for Vega charts.
199
+
200
+ For Vega-Lite charts: renders via vl-convert, caches SVG, returns actual height.
201
+ For tables: uses actual row counts from executor.
202
+ For everything else: delegates to the static compile-time calculator.
203
+
204
+ Render-first Vega sizing (vl-convert renders) only fires when
205
+ render_ctx.executor is not None. When render_ctx.executor is None
206
+ (render_first=False), the provider falls back to static aspect-ratio
207
+ estimates for Vega charts while still sizing tables from row counts.
208
+ """
209
+ from dataface.core.render.chart.render_single import render_chart_to_svg
210
+
211
+ def provider(
212
+ item: LayoutItem,
213
+ card_gap: float,
214
+ gap: float,
215
+ width: float,
216
+ variable_defaults: dict[str, Any] | None,
217
+ ) -> float:
218
+ if item.type == "chart" and item.chart:
219
+ card_pad = card_padding
220
+
221
+ # Table: use actual row counts
222
+ if item.chart.type == "table":
223
+ return (
224
+ _get_table_height_from_data(
225
+ item.chart,
226
+ executor,
227
+ variables,
228
+ resolved_style=render_ctx.resolved_style,
229
+ width=width,
230
+ face_level=render_ctx.face_level,
231
+ )
232
+ + 2 * card_pad
233
+ )
234
+
235
+ if item.chart.type == "callout":
236
+ _chart_svg, _, actual_height = render_chart_to_svg(
237
+ item.chart,
238
+ executor,
239
+ variables or {},
240
+ width,
241
+ height=None,
242
+ vega_config=render_ctx.vega_config,
243
+ resolved_style=render_ctx.resolved_style,
244
+ face_level=render_ctx.face_level,
245
+ )
246
+ render_ctx.render_cache[(item.chart.id, width, actual_height)] = (
247
+ _chart_svg,
248
+ actual_height,
249
+ )
250
+ render_ctx.natural_heights[(item.chart.id, width)] = actual_height
251
+ return actual_height
252
+
253
+ # spark_bar: natural-height SVG renderer — renders content-first.
254
+ # Returns natural_height + 2*card_pad to match SVG_LAYOUT_PADDED_TYPES behavior
255
+ # (card_pad inset applied via translate in render_chart_item).
256
+ # Not cached: rendering.py skips render_cache for SVG_LAYOUT_PADDED_TYPES,
257
+ # so the final render pass re-renders regardless.
258
+ if item.chart.type == "spark_bar":
259
+ _chart_svg, _, actual_height = render_chart_to_svg(
260
+ item.chart,
261
+ executor,
262
+ variables or {},
263
+ width,
264
+ height=None,
265
+ vega_config=render_ctx.vega_config,
266
+ resolved_style=render_ctx.resolved_style,
267
+ face_level=render_ctx.face_level,
268
+ )
269
+ return actual_height + 2 * card_pad
270
+
271
+ # Render-first Vega sizing.
272
+ # Vega charts now render at full item width with card_pad as internal
273
+ # Vega padding, so the returned SVG height already includes the inset.
274
+ # Do NOT add 2*card_pad here — it would double-count the padding.
275
+ if (
276
+ render_ctx.executor is not None
277
+ and item.chart.type not in NON_ASPECT_RATIO_TYPES
278
+ ):
279
+ render_inner_width = width
280
+ chart_padding = render_ctx.resolved_style.charts.padding
281
+ # static_estimate approximates Vega output height: marks + Vega padding
282
+ static_estimate = (
283
+ get_chart_content_height(
284
+ item.chart,
285
+ width=render_inner_width,
286
+ resolved_style=render_ctx.resolved_style,
287
+ )
288
+ + 2 * card_pad
289
+ )
290
+ # Use item.height when it has been pre-set (future: cols alignment
291
+ # pre-pass). In the normal sizing flow item.height is 0 here because
292
+ # _calculate_rows_dimensions fires the provider in its first pass before
293
+ # the second pass sets item.height. _fix_slot_heights_in_tree runs after
294
+ # calculate_layout_items to re-render at the final slot height when the
295
+ # two values diverge (e.g. a 600px wrapper around an 800px estimate).
296
+ render_height = item.height if item.height > 0 else static_estimate
297
+
298
+ # Use cached result if already rendered at this width
299
+ if (item.chart.id, render_inner_width) in render_ctx.natural_heights:
300
+ cached_height = render_ctx.natural_heights[
301
+ (item.chart.id, render_inner_width)
302
+ ]
303
+ return max(cached_height, static_estimate)
304
+
305
+ try:
306
+ chart_svg, actual_width, actual_height = render_chart_to_svg(
307
+ item.chart,
308
+ render_ctx.executor,
309
+ render_ctx.variables or {},
310
+ render_inner_width,
311
+ height=render_height,
312
+ vega_config=render_ctx.vega_config,
313
+ resolved_style=render_ctx.resolved_style,
314
+ padding=additive_padding(card_pad, chart_padding),
315
+ face_level=render_ctx.face_level,
316
+ )
317
+ except (ExecutionError, ChartDataError) as exc:
318
+ # Migrate-or-fail: only ExecutionError (query failed) and
319
+ # ChartDataError (vega-lite spec rejected) are expected
320
+ # data-side failures we estimate around. KeyError/ValueError/
321
+ # OSError/RuntimeError/LookupError are bug-class exceptions
322
+ # — let them surface loudly, do not silently render an
323
+ # aspect-ratio fallback that masks the bug.
324
+ _log.warning(
325
+ "Render-first sizing failed for chart %r "
326
+ "(will use aspect-ratio estimate): %s",
327
+ item.chart.id,
328
+ exc,
329
+ )
330
+ else:
331
+ # Clamp: tiny overshoot (≤2px above static_estimate) → static_estimate.
332
+ # Two-pass height correction (endpoint-label hconcat) can return
333
+ # actual_height = static_estimate + ε due to fractional-pixel rounding.
334
+ # Without the clamp, natural_heights differs by ε between endpoint_labels
335
+ # ON and OFF, causing row_height drift. Undershoots are also snapped up.
336
+ if actual_height - static_estimate <= 2.0:
337
+ actual_height = static_estimate
338
+
339
+ # autosize:pad (set by attach_data_table) makes the outer
340
+ # SVG wider than render_inner_width by the y-axis label +
341
+ # padding + legend overhead. Measure the first render's
342
+ # actual outer width, compute the overhead, and re-render
343
+ # with spec.width pre-shrunk so the second render's outer
344
+ # SVG fits the allocated slot.
345
+ # Pass height=static_estimate (not actual_height) to the
346
+ # re-render: under autosize:pad, spec.height is the *inner*
347
+ # plot rect, so Vega adds strip/axis on top — using
348
+ # actual_height as spec.height would inflate the output by
349
+ # ~strip_height on every re-render.
350
+ # Store the shrunk width in data_table_corrected_widths so
351
+ # _align_cols_heights can reuse it instead of item.width,
352
+ # preventing the overhead from being re-applied on alignment.
353
+ if item.chart.data_table is not None:
354
+ overhead = actual_width - render_inner_width
355
+ if overhead > 0:
356
+ shrunk_width = max(render_inner_width - overhead, 1.0)
357
+ try:
358
+ chart_svg, _, rerender_height = render_chart_to_svg(
359
+ item.chart,
360
+ render_ctx.executor,
361
+ render_ctx.variables or {},
362
+ shrunk_width,
363
+ height=render_height,
364
+ vega_config=render_ctx.vega_config,
365
+ resolved_style=render_ctx.resolved_style,
366
+ padding=additive_padding(card_pad, chart_padding),
367
+ face_level=render_ctx.face_level,
368
+ )
369
+ except (ExecutionError, ChartDataError) as exc:
370
+ _log.warning(
371
+ "Width-correction re-render failed for chart %r "
372
+ "(keeping first render): %s",
373
+ item.chart.id,
374
+ exc,
375
+ )
376
+ else:
377
+ actual_height = max(rerender_height, static_estimate)
378
+ render_ctx.data_table_corrected_widths[
379
+ item.chart.id
380
+ ] = shrunk_width
381
+
382
+ render_ctx.render_cache[
383
+ (item.chart.id, render_inner_width, actual_height)
384
+ ] = (chart_svg, actual_height)
385
+ render_ctx.natural_heights[(item.chart.id, render_inner_width)] = (
386
+ actual_height
387
+ )
388
+ return actual_height
389
+
390
+ # Fallback: static aspect-ratio estimate (marks + Vega padding)
391
+ return (
392
+ get_chart_content_height(
393
+ item.chart,
394
+ width=width,
395
+ resolved_style=render_ctx.resolved_style,
396
+ )
397
+ + 2 * card_pad
398
+ )
399
+
400
+ if item.type == "face" and item.face:
401
+ data_aware = get_item_content_height(
402
+ item,
403
+ card_gap,
404
+ gap,
405
+ width,
406
+ variable_defaults,
407
+ height_provider=provider,
408
+ resolved_style=render_ctx.resolved_style,
409
+ )
410
+ return max(data_aware, item.height)
411
+
412
+ # Fallback for any other item types
413
+ return get_item_content_height(
414
+ item,
415
+ card_gap,
416
+ gap,
417
+ width,
418
+ variable_defaults,
419
+ height_provider=provider,
420
+ resolved_style=render_ctx.resolved_style,
421
+ )
422
+
423
+ return provider
424
+
425
+
426
+ def _col_chart_body_target(
427
+ items: list[LayoutItem], render_ctx: SizingRenderCtx
428
+ ) -> float:
429
+ """Max natural chart body height across all items in a cols row.
430
+
431
+ For direct chart items uses the natural height from natural_heights.
432
+ For face items the logic depends on the face's inner layout:
433
+ - All-Vega rows face (every sub-item is a Vega chart): contribute
434
+ item.height (= sum of sub-chart heights + gaps, set by the sizing pass).
435
+ An adjacent direct chart must fill the same cell height.
436
+ - Any other face layout (mixed content, cols sub-layout, single chart):
437
+ contribute max(sub-chart natural heights) — the non-chart overhead must
438
+ not inflate adjacent chart bodies.
439
+ Non-aspect-ratio types (table, kpi, spark_bar) are excluded.
440
+ Returns 0.0 if no Vega charts are found.
441
+ """
442
+ max_h = 0.0
443
+ for item in items:
444
+ if item.chart is not None and item.chart.type not in NON_ASPECT_RATIO_TYPES:
445
+ h = render_ctx.natural_heights.get((item.chart.id, item.width))
446
+ if h is not None:
447
+ max_h = max(max_h, h)
448
+ elif item.face is not None:
449
+ sub_items = item.face.layout.items
450
+ is_all_vega_rows = (
451
+ item.face.layout.type == "rows"
452
+ and len(sub_items) > 1
453
+ and all(
454
+ s.chart is not None and s.chart.type not in NON_ASPECT_RATIO_TYPES
455
+ for s in sub_items
456
+ )
457
+ )
458
+ if is_all_vega_rows and item.height > 0:
459
+ # face item.height = sum(sub-chart heights) + inter-item gaps,
460
+ # computed by the sizing pass. Use it as-is so the adjacent
461
+ # direct chart fills the same cell.
462
+ max_h = max(max_h, item.height)
463
+ else:
464
+ for sub in sub_items:
465
+ if (
466
+ sub.chart is not None
467
+ and sub.chart.type not in NON_ASPECT_RATIO_TYPES
468
+ ):
469
+ h = render_ctx.natural_heights.get((sub.chart.id, sub.width))
470
+ if h is not None:
471
+ max_h = max(max_h, h)
472
+ return max_h
473
+
474
+
475
+ def _effective_alignment_target_for_chart(
476
+ chart: Chart,
477
+ width: float,
478
+ target_height: float,
479
+ render_ctx: SizingRenderCtx,
480
+ ) -> float:
481
+ """Return the per-chart alignment target, capping pie/arc growth."""
482
+ natural_h = render_ctx.natural_heights.get((chart.id, width))
483
+ if (
484
+ chart.type in _FIXED_ASPECT_PIE_TYPES
485
+ and natural_h is not None
486
+ and target_height > natural_h
487
+ ):
488
+ return natural_h
489
+ return target_height
490
+
491
+
492
+ def _align_face_charts(
493
+ face_item: LayoutItem, target_height: float, render_ctx: SizingRenderCtx
494
+ ) -> None:
495
+ """Expand Vega charts inside a face item toward target_height.
496
+
497
+ The face bounds the expansion: charts can grow at most to face.layout.content_height
498
+ minus the space taken by non-chart siblings (rows layout only — stacked items share
499
+ vertical space). For cols layouts, items are side-by-side so non-chart siblings do
500
+ not reduce the Vega chart's available height.
501
+
502
+ Updates each chart's item.height so the render layer uses the correct cache key,
503
+ then delegates the actual re-render to _align_cols_heights.
504
+ """
505
+ assert face_item.face is not None, "_align_face_charts requires a face LayoutItem"
506
+ face = face_item.face
507
+ vega_count = sum(
508
+ 1
509
+ for sub in face.layout.items
510
+ if sub.chart is not None and sub.chart.type not in NON_ASPECT_RATIO_TYPES
511
+ )
512
+ # cols layout: all side-by-side charts need equalization.
513
+ # non-cols with exactly 1 Vega chart (e.g. chart + text caption): safe to expand.
514
+ # non-cols with multiple Vega charts: stacked rows — equalizing would overflow the face.
515
+ if face.layout.type != "cols" and vega_count != 1:
516
+ return
517
+ content_h: float = face.layout.content_height
518
+ if content_h <= 0:
519
+ return
520
+
521
+ if face.layout.type == "cols":
522
+ # cols: items are side-by-side — non-chart siblings don't consume Vega height.
523
+ face_chart_max = content_h
524
+ else:
525
+ # rows with 1 Vega chart: stacked non-chart items (captions, KPIs) do consume height.
526
+ non_chart_h = sum(
527
+ sub.height
528
+ for sub in face.layout.items
529
+ if sub.chart is None or sub.chart.type in NON_ASPECT_RATIO_TYPES
530
+ )
531
+ face_chart_max = max(content_h - non_chart_h, 0.0)
532
+ effective = min(target_height, face_chart_max)
533
+
534
+ for sub in face.layout.items:
535
+ if sub.chart is not None and sub.chart.type not in NON_ASPECT_RATIO_TYPES:
536
+ sub.height = _effective_alignment_target_for_chart(
537
+ sub.chart,
538
+ sub.width,
539
+ effective,
540
+ render_ctx,
541
+ )
542
+
543
+ _align_cols_heights(face.layout.items, effective, render_ctx)
544
+
545
+
546
+ def _align_cols_heights(
547
+ items: list[LayoutItem], target_height: float, render_ctx: SizingRenderCtx
548
+ ) -> None:
549
+ """Re-render shorter Vega cols items at target_height; update their cache entries.
550
+
551
+ Handles both direct chart items and face items (delegating to
552
+ _align_face_charts for the latter). Non-Vega items (table, KPI) are not
553
+ re-rendered — their container gets the max height but their content stays
554
+ at its natural height.
555
+ """
556
+ from dataface.core.render.chart.render_single import render_chart_to_svg
557
+
558
+ card_pad = float(render_ctx.resolved_style.board.card_padding)
559
+ chart_padding = render_ctx.resolved_style.charts.padding
560
+
561
+ if render_ctx.executor is None:
562
+ return
563
+
564
+ executor = render_ctx.executor
565
+ variables = render_ctx.variables or {}
566
+
567
+ for item in items:
568
+ if item.face is not None:
569
+ _align_face_charts(item, target_height, render_ctx)
570
+ continue
571
+ if item.chart is None or item.chart.type in NON_ASPECT_RATIO_TYPES:
572
+ continue
573
+ chart_id = item.chart.id
574
+ natural_h = render_ctx.natural_heights.get((chart_id, item.width))
575
+ effective_target = _effective_alignment_target_for_chart(
576
+ item.chart,
577
+ item.width,
578
+ target_height,
579
+ render_ctx,
580
+ )
581
+ if natural_h is None or abs(natural_h - effective_target) < 1.0:
582
+ continue
583
+
584
+ # Vega-family: render at item width (or the corrected shrunk width for
585
+ # data_table charts). autosize:pad (activated by attach_data_table) makes
586
+ # the outer SVG wider than spec.width by a constant overhead; using item.width
587
+ # as spec.width would re-apply that overhead and overflow the allocated slot.
588
+ render_width = render_ctx.data_table_corrected_widths.get(chart_id, item.width)
589
+ try:
590
+ chart_svg, _, actual_height = render_chart_to_svg(
591
+ item.chart,
592
+ executor,
593
+ variables,
594
+ render_width,
595
+ height=effective_target,
596
+ vega_config=render_ctx.vega_config,
597
+ resolved_style=render_ctx.resolved_style,
598
+ padding=additive_padding(card_pad, chart_padding),
599
+ face_level=render_ctx.face_level,
600
+ )
601
+ except ChartDataError as exc:
602
+ # Migrate-or-fail: only ChartDataError (vega-lite rejected the spec)
603
+ # is an expected per-chart failure we keep going through. Other
604
+ # exception types are bug-class — let them surface loudly.
605
+ _log.warning(
606
+ "Cols alignment re-render failed for chart %r; "
607
+ "keeping original height: %s",
608
+ chart_id,
609
+ exc,
610
+ )
611
+ continue
612
+
613
+ # autosize:pad (set by attach_data_table) makes the outer SVG taller than
614
+ # spec.height by the strip + axis overhead. If inflation detected, compute
615
+ # the overhead and re-render with spec.height pre-shrunk so the outer SVG
616
+ # fits within target_height.
617
+ if item.chart.data_table is not None:
618
+ overhead = actual_height - effective_target
619
+ if overhead > 0:
620
+ corrected_height = max(effective_target - overhead, 1.0)
621
+ try:
622
+ chart_svg, _, actual_height = render_chart_to_svg(
623
+ item.chart,
624
+ executor,
625
+ variables,
626
+ render_width,
627
+ height=corrected_height,
628
+ vega_config=render_ctx.vega_config,
629
+ resolved_style=render_ctx.resolved_style,
630
+ padding=additive_padding(card_pad, chart_padding),
631
+ face_level=render_ctx.face_level,
632
+ )
633
+ except ChartDataError as exc:
634
+ _log.warning(
635
+ "Height-correction re-render failed for chart %r "
636
+ "(keeping first alignment render): %s",
637
+ chart_id,
638
+ exc,
639
+ )
640
+
641
+ render_ctx.render_cache[(chart_id, item.width, effective_target)] = (
642
+ chart_svg,
643
+ actual_height,
644
+ )
645
+
646
+
647
+ def _align_grid_rows(items: list[LayoutItem], render_ctx: SizingRenderCtx) -> None:
648
+ """Align Vega chart heights within each row of a grid layout.
649
+
650
+ Groups items by (row, height). Items with the same row and row_span share
651
+ the same height and form one visual row band to align.
652
+ """
653
+ row_groups: dict[tuple[int, float], list[LayoutItem]] = {}
654
+ for item in items:
655
+ key = (item.row or 0, item.height)
656
+ if key not in row_groups:
657
+ row_groups[key] = []
658
+ row_groups[key].append(item)
659
+ for (_, row_height), row_items in row_groups.items():
660
+ if len(row_items) > 1:
661
+ _align_cols_heights(row_items, row_height, render_ctx)
662
+
663
+
664
+ def _fix_slot_heights_in_tree(layout: Layout, render_ctx: SizingRenderCtx) -> None:
665
+ """Re-render Vega charts whose slot height differs from the cached render height.
666
+
667
+ Called after calculate_layout_items has set all item.height values.
668
+ When a layout wrapper declares an explicit height (e.g. height: 600 on a
669
+ rows wrapper), the sizing provider may have rendered at the aspect-ratio
670
+ estimate (because item.height was 0 at provider call time). If the layout
671
+ algorithm then scaled the item.height down to the slot, the cache entry
672
+ (chart_id, width, estimate) won't match the renderer's lookup key
673
+ (chart_id, width, item.height). This pass re-renders at item.height and
674
+ updates the cache so the render pass hits without a fresh vl-convert call.
675
+
676
+ Only fires when item.height differs from the natural height by more than 1px
677
+ (avoids redundant re-renders for sub-pixel rounding differences).
678
+ """
679
+ from dataface.core.render.chart.render_single import render_chart_to_svg
680
+
681
+ if render_ctx.executor is None:
682
+ return
683
+
684
+ card_pad = float(render_ctx.resolved_style.board.card_padding)
685
+ chart_padding = render_ctx.resolved_style.charts.padding
686
+
687
+ for item in layout.items:
688
+ if item.face is not None:
689
+ nested_ctx = _nested_render_ctx(render_ctx, item.face)
690
+ _fix_slot_heights_in_tree(item.face.layout, nested_ctx)
691
+ continue
692
+
693
+ if item.chart is None or item.chart.type in NON_ASPECT_RATIO_TYPES:
694
+ continue
695
+ if item.height <= 0:
696
+ continue
697
+
698
+ chart_id = item.chart.id
699
+ natural_h = render_ctx.natural_heights.get((chart_id, item.width))
700
+ if natural_h is None or abs(natural_h - item.height) < 1.0:
701
+ continue
702
+
703
+ # Slot height differs from the rendered height — re-render at item.height.
704
+ render_width = render_ctx.data_table_corrected_widths.get(chart_id, item.width)
705
+ try:
706
+ chart_svg, _, actual_height = render_chart_to_svg(
707
+ item.chart,
708
+ render_ctx.executor,
709
+ render_ctx.variables or {},
710
+ render_width,
711
+ height=item.height,
712
+ vega_config=render_ctx.vega_config,
713
+ resolved_style=render_ctx.resolved_style,
714
+ padding=additive_padding(card_pad, chart_padding),
715
+ face_level=render_ctx.face_level,
716
+ )
717
+ except (ExecutionError, ChartDataError) as exc:
718
+ _log.warning(
719
+ "Slot-height re-render failed for chart %r "
720
+ "(keeping aspect-ratio render): %s",
721
+ chart_id,
722
+ exc,
723
+ )
724
+ continue
725
+
726
+ render_ctx.render_cache[(chart_id, item.width, item.height)] = (
727
+ chart_svg,
728
+ actual_height,
729
+ )
730
+ render_ctx.natural_heights[(chart_id, item.width)] = actual_height
731
+
732
+
733
+ def _align_all_cols_in_tree(layout: Layout, render_ctx: SizingRenderCtx) -> None:
734
+ """Walk the layout tree depth-first and align cols/grid heights where needed."""
735
+ if render_ctx.render_cache and layout.items:
736
+ if layout.type == "cols":
737
+ chart_target = _col_chart_body_target(layout.items, render_ctx)
738
+ if chart_target > 0:
739
+ for item in layout.items:
740
+ if (
741
+ item.chart is not None
742
+ and item.chart.type not in NON_ASPECT_RATIO_TYPES
743
+ ):
744
+ item.height = _effective_alignment_target_for_chart(
745
+ item.chart,
746
+ item.width,
747
+ chart_target,
748
+ render_ctx,
749
+ )
750
+ _align_cols_heights(layout.items, chart_target, render_ctx)
751
+ elif layout.type == "grid":
752
+ _align_grid_rows(layout.items, render_ctx)
753
+
754
+ # Nested layouts are always modeled as nested faces in the Face tree;
755
+ # face-only recursion is correct and complete.
756
+ for item in layout.items:
757
+ if item.face is not None and item.face.layout.items:
758
+ nested_ctx = _nested_render_ctx(render_ctx, item.face)
759
+ _align_all_cols_in_tree(item.face.layout, nested_ctx)
760
+
761
+
762
+ def calculate_data_aware_layout(
763
+ face: Face,
764
+ executor: Executor,
765
+ variables: dict[str, Any] | None = None,
766
+ render_first: bool = True,
767
+ ) -> tuple[Face, RenderCache]:
768
+ """Calculate layout dimensions using actual query data for table sizing.
769
+
770
+ Like calculate_layout(), but uses the executor to get actual row counts
771
+ for table charts and renders Vega-Lite charts to get true heights.
772
+
773
+ Must be called after execute_face_batch() so query results are cached.
774
+
775
+ Args:
776
+ face: Face with layout structure
777
+ executor: Executor with cached query results
778
+ variables: Variable values for query execution
779
+ render_first: When True, Vega-Lite charts are rendered during sizing to
780
+ get actual heights (render-first sizing). Set False for non-SVG
781
+ formats (yaml/json/text) to avoid unnecessary chart rendering.
782
+
783
+ Returns:
784
+ (Face with calculated dimensions, render_cache mapping chart_id
785
+ to (svg_string, actual_height) for Vega charts rendered during sizing)
786
+ """
787
+ board = face.resolved_style.board
788
+
789
+ container_width = float(board.width)
790
+ content_width = max(container_width - 2 * float(board.margin), 0.0)
791
+ min_height = float(board.min_height)
792
+ card_padding = float(board.card_padding)
793
+
794
+ card_gap = float(board.card_gap) if face.card_gap else 0.0
795
+ gap = get_face_gap(face)
796
+
797
+ variable_defaults = face.variable_defaults
798
+
799
+ # Create render context; executor=None disables render-first Vega sizing.
800
+ from dataface.core.compile.vega_config import compile_effective_vega_config
801
+
802
+ render_ctx = SizingRenderCtx(
803
+ resolved_style=face.resolved_style,
804
+ vega_config=compile_effective_vega_config(face.theme),
805
+ executor=executor if render_first else None,
806
+ variables=variables,
807
+ face_level=face.level,
808
+ )
809
+
810
+ # Build a data-aware height provider
811
+ height_provider = _make_data_aware_height_provider(
812
+ render_ctx,
813
+ executor,
814
+ variables,
815
+ card_padding=card_padding,
816
+ )
817
+
818
+ # Calculate layout height using compile sizing algorithms + data-aware provider
819
+ container_height = calculate_layout_height(
820
+ face.layout,
821
+ card_gap,
822
+ gap,
823
+ min_height,
824
+ available_width=content_width,
825
+ variable_defaults=variable_defaults,
826
+ height_provider=height_provider,
827
+ resolved_style=face.resolved_style,
828
+ )
829
+
830
+ if face.title:
831
+ title_measure_width = max(content_width - 2 * card_padding, 0.0)
832
+ title_height = get_title_height(
833
+ face.title, title_measure_width, variable_defaults
834
+ )
835
+ container_height += title_height + gap + card_gap
836
+
837
+ face.layout.width = container_width
838
+ face.layout.height = container_height
839
+ face.layout.content_width = content_width
840
+ face.layout.content_height = container_height
841
+
842
+ # Assign dimensions using compile sizing + data-aware provider
843
+ calculate_layout_items(
844
+ face.layout,
845
+ content_width,
846
+ container_height,
847
+ card_gap,
848
+ gap,
849
+ variable_defaults,
850
+ height_provider,
851
+ resolved_style=face.resolved_style,
852
+ )
853
+
854
+ # Slot-height fix: re-render charts whose cached SVG was sized by the
855
+ # aspect-ratio estimate but whose assigned slot height differs. This handles
856
+ # layout wrappers with an explicit height (rows: [- height: 600, rows: [chart]])
857
+ # where the sizing provider ran before item.height was set.
858
+ if render_ctx.render_cache:
859
+ _fix_slot_heights_in_tree(face.layout, render_ctx)
860
+
861
+ # Cols alignment: walk tree, re-render shorter Vega items at aligned height
862
+ if render_ctx.render_cache:
863
+ _align_all_cols_in_tree(face.layout, render_ctx)
864
+
865
+ return face, render_ctx.render_cache