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,543 @@
1
+ """Normalization module for transforming input types to compiled types.
2
+
3
+ Stage: COMPILE (Step 3 of 4)
4
+ Purpose: Transform AuthoredFace input types to Face output types.
5
+
6
+ Entry Points:
7
+ - normalize_face(face: AuthoredFace) -> Face
8
+
9
+ This is the core transformation step of compilation:
10
+ 1. Resolve all chart references (queries, extends, partials)
11
+ 2. Resolve remote dataface references
12
+ 3. Generate unique IDs for all entities
13
+ 4. Apply defaults (title from ID, empty description)
14
+ 5. Transform input types → compiled types
15
+ 6. Create unified layout structure
16
+
17
+ After this step, all references are resolved and downstream code can
18
+ rely on guaranteed field presence.
19
+
20
+ Cross-file Query References:
21
+ Charts can reference queries from external files using the syntax:
22
+ `query: path/to/file.yml#query_name`
23
+
24
+ The path is resolved relative to the current file's directory.
25
+ External queries are loaded once and cached in the query registry.
26
+
27
+ Dependencies:
28
+ - .models.face.authored (AuthoredFace)
29
+ - .models.face.compiled (Face, Layout)
30
+ - .models.chart.compiled (Chart)
31
+ - .models.query.compiled (AnyQuery)
32
+ - .jinja (resolve_jinja_template)
33
+ - .errors (ReferenceError, CompilationError)
34
+
35
+ See also:
36
+ - compile/validator.py: Previous step
37
+ - compile/sizing.py: Next step
38
+ """
39
+
40
+ import re
41
+ from datetime import datetime, timezone
42
+ from pathlib import Path
43
+ from typing import Any
44
+
45
+ from dataface.core.compile.errors import CompilationError
46
+ from dataface.core.compile.models.chart.compiled import Chart
47
+ from dataface.core.compile.models.face.authored import AuthoredFace
48
+ from dataface.core.compile.models.face.compiled import (
49
+ Face,
50
+ VariableValues,
51
+ )
52
+ from dataface.core.compile.models.query.compiled import AnyQuery
53
+ from dataface.core.compile.models.style.authored import StylePatch
54
+ from dataface.core.compile.models.style.merged import (
55
+ MergedStyle,
56
+ resolve_style,
57
+ )
58
+ from dataface.core.compile.models.variable.authored import Variable
59
+ from dataface.core.compile.normalize_charts import (
60
+ _collect_charts_from_layout,
61
+ _normalize_chart,
62
+ )
63
+ from dataface.core.compile.normalize_layout import build_unified_layout
64
+ from dataface.core.compile.normalize_queries import (
65
+ normalize_query, # noqa: F401 — re-exported (used by compiler.py, tests)
66
+ )
67
+ from dataface.core.compile.normalize_variables import (
68
+ _build_variable_registry,
69
+ _compute_variable_dependencies,
70
+ _detect_variable_input_type,
71
+ _expand_chart_variable_dependencies,
72
+ _generate_layout_variables,
73
+ _promote_inline_option_queries,
74
+ _validate_variable_references,
75
+ _validate_variable_value,
76
+ )
77
+
78
+
79
+ def compile_board_resolved_style(
80
+ face_style: StylePatch | None,
81
+ parent_resolved: MergedStyle | None,
82
+ theme_name: str | None = None,
83
+ ) -> MergedStyle:
84
+ """Build authoritative MergedStyle for this board scope.
85
+
86
+ Cascade:
87
+ 1. Theme defaults (theme_name → compiled theme → resolve_style)
88
+ 2. Parent semantic tokens (muted, accent) — propagate tonal identity
89
+ 3. Face's style: block as StylePatch — all fields cascade normally
90
+ """
91
+ from dataface.core.compile.config import get_compiled_theme, get_config
92
+
93
+ base = get_compiled_theme(theme_name) if theme_name else get_config().style
94
+
95
+ parent_cascade = None
96
+ if parent_resolved is not None:
97
+ parent_cascade = StylePatch.model_validate(
98
+ {"muted": parent_resolved.muted, "accent": parent_resolved.accent}
99
+ )
100
+
101
+ if face_style is None:
102
+ return parent_resolved or resolve_style(base)
103
+
104
+ return (
105
+ resolve_style(base, parent_cascade, face_style)
106
+ if parent_cascade is not None
107
+ else resolve_style(base, face_style)
108
+ )
109
+
110
+
111
+ def get_cascade_context(
112
+ face: AuthoredFace, parent_context: dict[str, Any]
113
+ ) -> dict[str, Any]:
114
+ """Build the cascade dict from Cascade-annotated fields on AuthoredFace.
115
+
116
+ Iterates model_fields, detects Cascade markers, and resolves each cascade
117
+ key's effective value (child's explicit value, or parent's propagated value).
118
+ Fields sharing a context key (e.g. source + sources → default_source) are
119
+ deduplicated: the first Cascade with that key wins, calling via= once.
120
+ """
121
+ from dataface.core.compile.models.markers import Cascade
122
+
123
+ result: dict[str, Any] = {}
124
+ seen_keys: set[str] = set()
125
+
126
+ for field_name, field_info in AuthoredFace.model_fields.items():
127
+ cascade = next(
128
+ (m for m in (field_info.metadata or []) if isinstance(m, Cascade)), None
129
+ )
130
+ if cascade is None:
131
+ continue
132
+ key = cascade.key or field_name
133
+ if key in seen_keys:
134
+ continue
135
+ seen_keys.add(key)
136
+ value = (
137
+ getattr(face, cascade.via)() if cascade.via else getattr(face, field_name)
138
+ )
139
+ result[key] = value or parent_context.get(key)
140
+
141
+ return result
142
+
143
+
144
+ def sync_face_resolved_style(
145
+ face: Any, parent_resolved: MergedStyle | None = None
146
+ ) -> None:
147
+ """Re-resolve face.resolved_style from face.theme and recurse into nested faces.
148
+
149
+ Passes the freshly-computed resolved style as parent_resolved to each
150
+ nested sub-board so the parent→child semantic color cascade (muted, accent)
151
+ is preserved. Call this when face.theme has been mutated after compile.
152
+ """
153
+ face.resolved_style = compile_board_resolved_style(
154
+ face.authored_style, parent_resolved, theme_name=face.theme
155
+ )
156
+ for item in face.layout.items or []:
157
+ if item.face is not None:
158
+ sync_face_resolved_style(item.face, face.resolved_style)
159
+
160
+
161
+ def _propagate_resolved_style(
162
+ layout: Any,
163
+ parent_resolved: MergedStyle,
164
+ ) -> None:
165
+ """Walk the layout tree and set resolved_style on nested Faces."""
166
+ from dataface.core.compile.models.face.compiled import Layout
167
+
168
+ if not isinstance(layout, Layout):
169
+ return
170
+
171
+ for item in layout.items:
172
+ if item.face is not None:
173
+ item.face.resolved_style = compile_board_resolved_style(
174
+ item.face.authored_style,
175
+ parent_resolved,
176
+ theme_name=item.face.theme,
177
+ )
178
+ _propagate_resolved_style(item.face.layout, item.face.resolved_style)
179
+
180
+
181
+ def slugify(text: str) -> str:
182
+ """Convert text to a URL-safe slug.
183
+
184
+ "Raw Data" → "raw_data"
185
+ "My Tab!" → "my_tab"
186
+ """
187
+ slug = re.sub(r"[^\w\s-]", "", text.lower().strip())
188
+ return re.sub(r"[\s-]+", "_", slug).strip("_")
189
+
190
+
191
+ def normalize_face(
192
+ face: AuthoredFace,
193
+ face_id: str | None = None,
194
+ parent_context: dict[str, Any] | None = None,
195
+ query_registry: dict[str, AnyQuery] | None = None,
196
+ chart_registry: dict[str, Any] | None = None,
197
+ depth: int = 0,
198
+ base_path: Path | None = None,
199
+ parent_level: int = 0,
200
+ ) -> Face:
201
+ """Normalize a AuthoredFace into a Face with guaranteed structure.
202
+
203
+ Stage: COMPILE (Step 3 of 4: Normalization)
204
+
205
+ This is the core transformation from user-provided YAML structure to the
206
+ internal compiled representation. After this step, all references are resolved,
207
+ IDs are assigned, and downstream code can rely on guaranteed field presence.
208
+
209
+ Normalization performs:
210
+ - Resolve all chart references (queries, extends, partials)
211
+ - Generate unique IDs for all entities
212
+ - Apply defaults (title from ID, empty description)
213
+ - Transform input types to compiled types
214
+ - Create unified Layout structure
215
+ - Propagate default source to queries without explicit source
216
+
217
+ Args:
218
+ face: Input AuthoredFace from parsing/validation step.
219
+ face_id: Optional explicit ID for this face
220
+ parent_context: Context from parent face (for nested faces)
221
+ query_registry: Complete query registry for reference resolution
222
+ chart_registry: Complete chart registry for reference resolution
223
+ depth: Structural nesting depth (for cycle detection and root detection)
224
+ base_path: Base path for resolving external file references
225
+ parent_level: Semantic heading level of the parent face. The current
226
+ face's level = parent_level + (1 if face.title else 0).
227
+ Root call passes 0 so a titled root face gets level=1.
228
+
229
+ Returns:
230
+ Face with guarantees:
231
+ - All fields required by compiled types are present
232
+ - All references resolved to actual objects
233
+ - All IDs are unique and deterministic
234
+ - Layout is unified (rows/cols/grid/tabs → Layout type)
235
+
236
+ Raises:
237
+ ReferenceError: When a chart references an undefined query
238
+ CompilationError: When normalization fails
239
+
240
+ Example:
241
+ >>> face = parse_yaml(yaml_content)
242
+ >>> errors = validate_face(face)
243
+ >>> if not errors:
244
+ ... compiled = normalize_face(face)
245
+ ... print(compiled.charts['revenue'].id) # Guaranteed to exist
246
+ 'revenue'
247
+ """
248
+ # Prevent infinite recursion
249
+ MAX_DEPTH = 50
250
+ if depth > MAX_DEPTH:
251
+ raise CompilationError(f"Maximum nesting depth ({MAX_DEPTH}) exceeded")
252
+
253
+ if depth > 0 and face.card_gap:
254
+ raise CompilationError(
255
+ "card_gap can only be set on the root face, not on nested faces"
256
+ )
257
+
258
+ # Root-level faces must have renderable content (layout or text). A title-only
259
+ # face is valid at depth>0 (AuthoredFace.validate_layout allows it for section
260
+ # headers and style wrappers), but is not renderable as a standalone dashboard.
261
+ if (
262
+ depth == 0
263
+ and not face.text
264
+ and all(f is None for f in (face.rows, face.cols, face.grid, face.tabs))
265
+ ):
266
+ raise CompilationError("Face must have at least one layout type or text")
267
+
268
+ parent_context = parent_context or {}
269
+ if base_path is None:
270
+ base_path = parent_context.get("base_path")
271
+
272
+ # ════════════════════════════════════════════════════════════════════
273
+ # STEP 1: Setup — default source, registries, face ID
274
+ # ════════════════════════════════════════════════════════════════════
275
+ _cascade = get_cascade_context(face, parent_context)
276
+ default_source = _cascade.get("default_source")
277
+
278
+ # Build registries if not provided (allows direct normalize_face() calls in tests)
279
+ if query_registry is None:
280
+ from dataface.core.compile.compiler import build_query_registry
281
+
282
+ query_registry = build_query_registry(
283
+ face, base_dir=base_path, default_source=default_source
284
+ )
285
+
286
+ if chart_registry is None:
287
+ from dataface.core.compile.compiler import build_chart_registry
288
+
289
+ chart_registry = build_chart_registry(face, base_dir=base_path)
290
+
291
+ if not face_id:
292
+ face_id = _generate_face_id(face, depth, parent_context.get("parent_id"))
293
+
294
+ # ════════════════════════════════════════════════════════════════════
295
+ # STEP 2: Variables
296
+ # ════════════════════════════════════════════════════════════════════
297
+ # Variable names are globally unique across the face tree; the renderer
298
+ # builds the global registry at render time. Here we collect local defs only.
299
+ local_variables: dict[str, Variable] = {}
300
+ if face.variables:
301
+ for var_name, var_def in face.variables.items():
302
+ if isinstance(var_def, Variable):
303
+ local_variables[var_name] = var_def
304
+ else:
305
+ # VariableRef — cross-file reference (e.g., "file.variables.var_name")
306
+ from dataface.core.compile.compiler import load_from_reference
307
+
308
+ local_variables[var_name] = load_from_reference(
309
+ var_def, base_dir=base_path
310
+ )
311
+
312
+ # Auto-detect input type when set to "auto" (the default)
313
+ for var in local_variables.values():
314
+ was_auto = var.input == "auto"
315
+ var.input = _detect_variable_input_type(var)
316
+ # Only flag select as refinable — multiselect has multi-value semantics
317
+ # that can't map to datepicker/checkbox/slider, and strong structural
318
+ # signals (bool → checkbox, min/max → slider) shouldn't be overridden.
319
+ if was_auto and var.input == "select":
320
+ var.input_auto_detected = True
321
+
322
+ # Promote inline SQL in variable options.query to synthetic named queries
323
+ _promote_inline_option_queries(local_variables, query_registry, default_source)
324
+
325
+ # Compute cascading dropdown dependencies from Jinja variable references
326
+ _compute_variable_dependencies(local_variables, query_registry)
327
+
328
+ # Validate and collect local defaults (global defaults built by renderer)
329
+ variable_defaults: VariableValues = {}
330
+ for var_name, var in local_variables.items():
331
+ if var.default is not None:
332
+ _validate_variable_value(var_name, var, var.default)
333
+ variable_defaults[var_name] = var.default
334
+
335
+ # ════════════════════════════════════════════════════════════════════
336
+ # STEP 3: Queries
337
+ # ════════════════════════════════════════════════════════════════════
338
+ # Extract locally defined queries from the global registry (for Face.queries).
339
+ local_queries: dict[str, AnyQuery] = {}
340
+ if face.queries:
341
+ for query_name in face.queries:
342
+ if query_name in query_registry:
343
+ local_queries[query_name] = query_registry[query_name]
344
+ else:
345
+ raise CompilationError(
346
+ f"Query '{query_name}' not found in registry. "
347
+ "This may indicate a compilation error."
348
+ )
349
+
350
+ # ════════════════════════════════════════════════════════════════════
351
+ # STEP 4: Charts
352
+ # ════════════════════════════════════════════════════════════════════
353
+ # Normalize all charts from the global registry — layout resolution needs
354
+ # all of them by name. Then extract locally defined ones for Face.charts.
355
+ normalized_charts: dict[str, Chart] = {}
356
+ for chart_name, chart_def in chart_registry.items():
357
+ normalized_charts[chart_name] = _normalize_chart(
358
+ chart_name,
359
+ chart_def,
360
+ query_registry,
361
+ base_path,
362
+ default_source,
363
+ source_path=f"charts.{chart_name}",
364
+ )
365
+
366
+ local_charts: dict[str, Chart] = {}
367
+ if face.charts:
368
+ for chart_name in face.charts:
369
+ if chart_name in normalized_charts:
370
+ local_charts[chart_name] = normalized_charts[chart_name]
371
+ else:
372
+ raise CompilationError(
373
+ f"Chart '{chart_name}' not found in registry. "
374
+ "Charts must be defined in the face tree or imported."
375
+ )
376
+
377
+ charts = local_charts
378
+
379
+ # Expand transitive variable dependencies (A→B→C means chart depends on A, B, C)
380
+ _expand_chart_variable_dependencies(charts, local_variables)
381
+
382
+ # ════════════════════════════════════════════════════════════════════
383
+ # STEP 5: Style & theme
384
+ # ════════════════════════════════════════════════════════════════════
385
+ # Faces should inherit the same effective theme choice as their charts so
386
+ # nested boards resolve typography/palette consistently. The face canvas
387
+ # background itself is intentionally not copied from chart themes: board
388
+ # chrome follows face/style defaults (white in Playground review mode),
389
+ # while chart surfaces keep the theme-owned canvas/background colors.
390
+ # If YAML does not specify a theme explicitly, inherit the parent theme or
391
+ # fall back to the configured default theme.
392
+ from dataface.core.compile.config import get_default_theme_name
393
+
394
+ theme = _cascade.get("theme") or get_default_theme_name()
395
+
396
+ # Compile board-scoped resolved style before layout so nested faces can inherit
397
+ resolved_style = compile_board_resolved_style(
398
+ face.style,
399
+ parent_context.get("resolved_style"),
400
+ theme_name=theme,
401
+ )
402
+
403
+ # ════════════════════════════════════════════════════════════════════
404
+ # STEP 6: Layout
405
+ # ════════════════════════════════════════════════════════════════════
406
+ # parent_variables: inherited compile-time variables (foreach loop vars) from
407
+ # parent + local variable defaults. These are needed by foreach to generate
408
+ # static per-iteration content at compile time. Interactive variable defaults
409
+ # are included here so foreach items get them baked in (correct, since foreach
410
+ # produces static faces). For regular nested boards, _resolve_dict_templates is
411
+ # only called when _resolve_jinja=True, so interactive defaults don't get baked
412
+ # into regular board content at compile time.
413
+ parent_variables: dict[str, Any] = {}
414
+ if parent_context.get("variables"):
415
+ parent_variables.update(parent_context["variables"])
416
+ for var_name, var in local_variables.items():
417
+ if var.default is not None:
418
+ parent_variables[var_name] = var.default
419
+
420
+ # Semantic heading level: count only titled ancestors.
421
+ # Bare wrappers (title is empty/None) do not advance the level counter.
422
+ # If style.title.level is an integer override (from theme or face YAML), it wins;
423
+ # that value also becomes the parent_level for descendants (cascade as if structural truth).
424
+ style_level_override = resolved_style.title.level
425
+ if isinstance(style_level_override, int):
426
+ this_level = style_level_override
427
+ else:
428
+ this_level = parent_level + (1 if face.title else 0)
429
+
430
+ _resolve_jinja = parent_context.get("_resolve_jinja", False)
431
+ layout = build_unified_layout(
432
+ face,
433
+ normalized_charts,
434
+ query_registry,
435
+ face_id,
436
+ depth,
437
+ base_path,
438
+ default_source,
439
+ chart_registry,
440
+ theme,
441
+ parent_variables,
442
+ _resolve_jinja=_resolve_jinja,
443
+ resolved_style=resolved_style,
444
+ parent_level=this_level,
445
+ )
446
+
447
+ _propagate_resolved_style(layout, resolved_style)
448
+
449
+ # Auto-generate hidden variables for tabs/details widgets
450
+ auto_variables = _generate_layout_variables(layout)
451
+ local_variables.update(auto_variables)
452
+ for var_name, var in auto_variables.items():
453
+ if var.default is not None:
454
+ variable_defaults[var_name] = var.default
455
+
456
+ # Collect inline charts from layout so all charts are accessible via face.charts
457
+ # without traversing the layout tree.
458
+ all_charts = dict(charts)
459
+ _collect_charts_from_layout(layout, all_charts)
460
+ charts = all_charts
461
+
462
+ # ════════════════════════════════════════════════════════════════════
463
+ # STEP 7: Assemble Face
464
+ # ════════════════════════════════════════════════════════════════════
465
+
466
+ compiled_face = Face(
467
+ id=face_id,
468
+ title=face.title or "",
469
+ description=face.description or "",
470
+ tags=face.tags or [],
471
+ text=face.text or "",
472
+ variables=local_variables, # Local variables for UI controls
473
+ queries=local_queries,
474
+ charts=charts,
475
+ layout=layout,
476
+ variable_defaults=variable_defaults, # Local variable defaults
477
+ card_gap=face.card_gap,
478
+ authored_style=face.style,
479
+ theme=theme,
480
+ resolved_style=resolved_style,
481
+ level=this_level,
482
+ meta={
483
+ "compiled_at": datetime.now(timezone.utc).isoformat(),
484
+ "version": "0.1.0",
485
+ },
486
+ )
487
+
488
+ # ════════════════════════════════════════════════════════════════════
489
+ # STEP 8: Variable registry (root face only)
490
+ # ════════════════════════════════════════════════════════════════════
491
+ if depth == 0:
492
+ compiled_face.variable_registry = _build_variable_registry(compiled_face)
493
+
494
+ # Pre-compute global defaults to avoid repeated comprehension at render time
495
+ compiled_face.variable_defaults = {
496
+ name: var.default
497
+ for name, var in compiled_face.variable_registry.items()
498
+ if var.default is not None
499
+ }
500
+
501
+ # Validate that all referenced variables are defined
502
+ warnings = _validate_variable_references(compiled_face)
503
+ if warnings:
504
+ compiled_face.meta["variable_warnings"] = warnings
505
+
506
+ # ════════════════════════════════════════════════════════════════════
507
+ # STEP 9: Chart focus (root face only)
508
+ # ════════════════════════════════════════════════════════════════════
509
+ # Narrows the face to a single chart and its dependent variables.
510
+ if depth == 0 and face.chart_focus:
511
+ from dataface.core.compile.chart_focus import focus_on_chart
512
+
513
+ compiled_face = focus_on_chart(compiled_face, face.chart_focus)
514
+
515
+ return compiled_face
516
+
517
+
518
+ def _generate_face_id(
519
+ face: AuthoredFace,
520
+ depth: int,
521
+ parent_id: str | None = None,
522
+ ) -> str:
523
+ """Generate a unique face ID.
524
+
525
+ Args:
526
+ face: AuthoredFace to generate ID for
527
+ depth: Nesting depth
528
+ parent_id: Parent face ID if nested
529
+
530
+ Returns:
531
+ Unique face ID
532
+ """
533
+ if face.id:
534
+ return face.id
535
+
536
+ if parent_id:
537
+ return f"{parent_id}_nested{depth}"
538
+
539
+ if face.title:
540
+ slug = slugify(face.title)
541
+ return slug or "untitled_face"
542
+
543
+ return "untitled_face"