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,946 @@
1
+ """Variable control rendering for SVG/HTML output.
2
+
3
+ Stage: RENDER
4
+ Purpose: Render interactive and read-only variable controls.
5
+
6
+ This module provides functions for rendering variable controls in SVG:
7
+ - Interactive controls using foreignObject for HTML form elements
8
+ - Read-only labels for PNG/PDF export
9
+ - JavaScript for variable interactivity
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import datetime
15
+ import html
16
+ import json
17
+ import logging
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ from dataface.core.compile.models.face.compiled import VariableValues
21
+ from dataface.core.compile.models.style.merged import MergedStyle, resolve_cascaded_font
22
+
23
+ if TYPE_CHECKING:
24
+ from dataface.core.compile.models.variable.authored import Variable
25
+
26
+ from dataface.core.compile.jinja import resolve_jinja_template
27
+ from dataface.core.compile.models.query.compiled import SqlQuery
28
+ from dataface.core.compile.models.style.compiled import font_weight_as_css
29
+ from dataface.core.compile.models.variable.authored import SingleRowBoolProbe
30
+ from dataface.core.compile.sizing import compute_variable_controls_height
31
+ from dataface.core.execute.executor import Executor
32
+ from dataface.core.render.script_embedding import (
33
+ embed_svg_script,
34
+ )
35
+ from dataface.core.render.text.case import format_display_text
36
+ from dataface.core.render.utils import is_valid_sql_identifier
37
+ from dataface.core.render.variable_input_refinement import refine_input_type_from_data
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ _READ_ONLY_UNSET_SELECT = "All"
42
+ _READ_ONLY_UNSET_DATERANGE = "All dates"
43
+
44
+
45
+ def _read_only_unset_label(var_def: Variable, *, default: str) -> str:
46
+ if var_def.placeholder:
47
+ return var_def.placeholder
48
+ return default
49
+
50
+
51
+ def _select_value_is_unset(value: Any) -> bool:
52
+ return value is None or value == ""
53
+
54
+
55
+ def _daterange_is_unset(value: Any) -> bool:
56
+ if value is None:
57
+ return True
58
+ if not isinstance(value, (list, tuple)):
59
+ return False
60
+ if len(value) != 2:
61
+ return False
62
+ return value[0] in (None, "") or value[1] in (None, "")
63
+
64
+
65
+ def format_variable_display_value(var_def: Variable, value: Any) -> str:
66
+ """Human-readable filter state for read-only variable strips (PNG/PDF/static SVG)."""
67
+ input_type = var_def.input
68
+
69
+ if input_type in ("select", "radio"):
70
+ if _select_value_is_unset(value):
71
+ return _read_only_unset_label(var_def, default=_READ_ONLY_UNSET_SELECT)
72
+ return str(value)
73
+
74
+ if input_type == "multiselect":
75
+ if value is None or value == []:
76
+ return _read_only_unset_label(var_def, default=_READ_ONLY_UNSET_SELECT)
77
+ if not isinstance(value, (list, tuple)):
78
+ raise ValueError(
79
+ f"multiselect variable expects a list, got {type(value).__name__}"
80
+ )
81
+ return ", ".join(str(v) for v in value)
82
+
83
+ if input_type == "daterange":
84
+ if _daterange_is_unset(value):
85
+ return _read_only_unset_label(var_def, default=_READ_ONLY_UNSET_DATERANGE)
86
+ if not isinstance(value, (list, tuple)):
87
+ raise ValueError(
88
+ f"daterange variable expects a 2-element list, got {type(value).__name__}"
89
+ )
90
+ if len(value) != 2:
91
+ raise ValueError(
92
+ f"daterange variable expects exactly 2 elements, got {len(value)}"
93
+ )
94
+ start, end = value[0], value[1]
95
+ if start in (None, "") or end in (None, ""):
96
+ return _read_only_unset_label(var_def, default=_READ_ONLY_UNSET_DATERANGE)
97
+ return _format_daterange_label(str(start), str(end))
98
+
99
+ if input_type == "checkbox":
100
+ if value is None:
101
+ return "No"
102
+ if isinstance(value, str):
103
+ return "Yes" if value.lower().strip() in ("true", "yes", "1") else "No"
104
+ return "Yes" if value else "No"
105
+
106
+ if input_type in (
107
+ "text",
108
+ "input",
109
+ "textarea",
110
+ "number",
111
+ "slider",
112
+ "range",
113
+ "date",
114
+ "datepicker",
115
+ ):
116
+ if value is None or value == "":
117
+ return var_def.placeholder or ""
118
+ return str(value)
119
+
120
+ if input_type == "auto":
121
+ raise ValueError(
122
+ "read-only variable display requires a resolved input type, not 'auto'"
123
+ )
124
+
125
+ raise ValueError(
126
+ f"unsupported variable input type for read-only display: {input_type!r}"
127
+ )
128
+
129
+
130
+ def render_variables_svg(
131
+ variable_defs: dict[str, Any],
132
+ current_values: VariableValues,
133
+ width: float,
134
+ *,
135
+ resolved_style: MergedStyle,
136
+ ) -> tuple[str, float]:
137
+ """Render variables as read-only labels in SVG.
138
+
139
+ Used for PNG/PDF export where foreignObject is not supported.
140
+
141
+ Args:
142
+ variable_defs: Dictionary of variable definitions
143
+ current_values: Current variable values
144
+ width: Available width
145
+ resolved_style: MergedStyle for color and font resolution.
146
+
147
+ Returns:
148
+ Tuple of (SVG string, height used)
149
+ """
150
+ if not variable_defs:
151
+ return "", 0.0
152
+
153
+ variables_style = resolved_style.variables
154
+ _font_family = variables_style.label.font.family
155
+ assert (
156
+ _font_family is not None
157
+ ), "cascade should have populated variables.label.font.family"
158
+
159
+ _var_font_size = variables_style.font.size
160
+ assert _var_font_size is not None, "style.variables.font.size must be configured"
161
+ font_size = float(_var_font_size)
162
+ line_height = variables_style.line_height
163
+ padding = variables_style.padding
164
+ font_family = html.escape(str(_font_family))
165
+ items: list[str] = []
166
+ current_x = 0.0
167
+ current_y = font_size + padding
168
+
169
+ for var_name, var_def in variable_defs.items():
170
+ current_value = current_values.get(var_name, var_def.default)
171
+ display_value = format_variable_display_value(var_def, current_value)
172
+
173
+ label = var_def.label or format_display_text(
174
+ var_name,
175
+ from_slug=True,
176
+ font=resolve_cascaded_font(
177
+ variables_style.label.font, "variables.label.font"
178
+ ),
179
+ )
180
+ escaped_label = html.escape(label)
181
+ escaped_value = html.escape(display_value)
182
+
183
+ # Create label: value text
184
+ label_text = f"{escaped_label}:"
185
+ value_text = f" {escaped_value}"
186
+
187
+ # Estimate width (rough calculation)
188
+ label_width = len(label_text) * font_size * 0.6
189
+ value_width = len(value_text) * font_size * 0.55
190
+
191
+ # Check if we need to wrap to next line
192
+ if current_x + label_width + value_width > width and current_x > 0:
193
+ current_x = 0
194
+ current_y += line_height
195
+
196
+ label_weight_raw = variables_style.label.font.weight
197
+ assert (
198
+ label_weight_raw is not None
199
+ ), "cascade should have populated variables.label.font.weight"
200
+ label_weight = font_weight_as_css(label_weight_raw)
201
+ # Render the label and value with theme-appropriate colors
202
+ items.append(
203
+ f'<text x="{current_x}" y="{current_y}" font-size="{font_size}" fill="{resolved_style.variables.font.color}" '
204
+ f'font-family="{font_family}">'
205
+ f'<tspan font-weight="{label_weight}">{label_text}</tspan>'
206
+ f'<tspan fill="{resolved_style.font.color}">{value_text}</tspan>'
207
+ f"</text>"
208
+ )
209
+
210
+ current_x += label_width + value_width + variables_style.gap
211
+
212
+ if not items:
213
+ return "", 0.0
214
+
215
+ total_height = current_y + padding
216
+ svg_content = "\n".join(items)
217
+
218
+ # Wrap in a rect background with theme color
219
+ return (
220
+ f'<rect x="0" y="0" width="{width}" height="{total_height}" fill="{resolved_style.variables.input.background}" rx="{variables_style.border.radius}"/>'
221
+ f"{svg_content}",
222
+ total_height,
223
+ )
224
+
225
+
226
+ def resolve_query_options_for_svg(
227
+ query_ref: str, executor: Executor, variables: dict[str, Any]
228
+ ) -> list[str]:
229
+ """Resolve options from a named query for SVG rendering.
230
+
231
+ Args:
232
+ query_ref: Query name reference (e.g., "queries.city_options" or "city_options")
233
+ executor: Executor for running queries
234
+ variables: Current variable values for query execution
235
+
236
+ Returns:
237
+ List of option values from the query
238
+ """
239
+ try:
240
+ # Strip "queries." prefix if present
241
+ query_name = query_ref.replace("queries.", "")
242
+
243
+ # Execute the query
244
+ result = executor.execute_query(query_name, variables)
245
+
246
+ if result and len(result) > 0:
247
+ # Look for 'value' column first, then 'label', then first column
248
+ first_row = result[0]
249
+ value_key = None
250
+ for key in ["value", "label", "name", "id"]:
251
+ if key in first_row:
252
+ value_key = key
253
+ break
254
+ if value_key is None:
255
+ value_key = list(first_row.keys())[0]
256
+
257
+ return [
258
+ str(row[value_key]) for row in result if row.get(value_key) is not None
259
+ ]
260
+
261
+ return []
262
+ except (ValueError, KeyError, TypeError, RuntimeError):
263
+ # Log as warning with context to make failures visible for debugging
264
+ # Don't raise to keep rendering, but make the failure actionable
265
+ logger.warning(
266
+ "Failed to resolve query options for SVG variable control",
267
+ exc_info=True,
268
+ )
269
+ return []
270
+
271
+
272
+ def resolve_column_options_for_svg(column_ref: str, executor: Executor) -> list[str]:
273
+ """Resolve distinct values from a column for variable options in SVG.
274
+
275
+ For column binding to work, the table must be defined in sources or be
276
+ an actual database table. For CSV-based data loaded via read_csv(),
277
+ use options.query with a dedicated query instead.
278
+
279
+ Args:
280
+ column_ref: Column reference in format "table.column" or just "column"
281
+ executor: Executor for running queries
282
+
283
+ Returns:
284
+ List of distinct values from the column, or empty list if unavailable
285
+ """
286
+ try:
287
+ # Parse the column reference (table.column or just column)
288
+ if "." not in column_ref:
289
+ return []
290
+
291
+ table_name, column_name = column_ref.rsplit(".", 1)
292
+
293
+ # Validate identifiers to prevent SQL injection
294
+ if not is_valid_sql_identifier(table_name) or not is_valid_sql_identifier(
295
+ column_name
296
+ ):
297
+ logger.warning(f"Invalid SQL identifier in column reference: {column_ref}")
298
+ return []
299
+
300
+ # Build a query to get distinct values
301
+ sql = f"SELECT DISTINCT {column_name} FROM {table_name} ORDER BY {column_name}"
302
+
303
+ try:
304
+ result = executor.adapter_registry.execute(SqlQuery(sql=sql))
305
+ if result.is_success and result.data:
306
+ first_key = list(result.data[0].keys())[0]
307
+ if first_key:
308
+ return [
309
+ str(row[first_key])
310
+ for row in result.data
311
+ if row[first_key] is not None
312
+ ]
313
+ except (ValueError, KeyError, TypeError, RuntimeError):
314
+ # Column binding requires the table to exist in the database
315
+ # For CSV-based data, use options.query instead
316
+ logger.warning(
317
+ f"Failed to bind column '{column_ref}' for variable options. "
318
+ "For CSV-based data, use options.query instead.",
319
+ exc_info=True,
320
+ )
321
+
322
+ return []
323
+ except (ValueError, KeyError, TypeError):
324
+ logger.warning(
325
+ "Failed to resolve column options for SVG variable control",
326
+ exc_info=True,
327
+ )
328
+ return []
329
+
330
+
331
+ def render_interactive_variables_svg(
332
+ variable_defs: dict[str, Any],
333
+ current_values: VariableValues,
334
+ width: float,
335
+ executor: Executor | None = None,
336
+ *,
337
+ resolved_style: MergedStyle,
338
+ ) -> tuple[str, float]:
339
+ """Render interactive variable controls in SVG using foreignObject.
340
+
341
+ Uses real HTML form elements embedded via foreignObject for:
342
+ - Native keyboard/accessibility support
343
+ - Mobile-friendly controls
344
+ - Less custom code to maintain
345
+
346
+ Variables with visible=False are filtered out and not rendered.
347
+
348
+ Args:
349
+ variable_defs: Dictionary of variable definitions
350
+ current_values: Current variable values
351
+ width: Available width
352
+ executor: Executor for resolving query-based options
353
+ resolved_style: MergedStyle for color and font resolution.
354
+
355
+ Returns:
356
+ Tuple of (SVG string with foreignObject, height used)
357
+ """
358
+ if not variable_defs:
359
+ return "", 0.0
360
+
361
+ # Filter out non-visible variables — they can be used in queries but not rendered
362
+ visible_defs = {
363
+ name: var_def for name, var_def in variable_defs.items() if var_def.visible
364
+ }
365
+
366
+ if not visible_defs:
367
+ return "", 0.0
368
+
369
+ # Use filtered visible_defs for rendering
370
+ variable_defs = visible_defs
371
+
372
+ variables_style = resolved_style.variables
373
+ _font_family = variables_style.label.font.family
374
+ assert (
375
+ _font_family is not None
376
+ ), "cascade should have populated variables.label.font.family"
377
+
378
+ height = compute_variable_controls_height(
379
+ variable_defs, width, variables_style=resolved_style.variables
380
+ )
381
+ font_family = html.escape(str(_font_family))
382
+
383
+ # Build HTML controls.
384
+ # Shared icon symbols (e.g. calendar for daterange) are prepended once so
385
+ # multiple daterange controls on the same face don't duplicate the id.
386
+ controls_html: list[str] = []
387
+ icons_html = generate_svg_variable_icons(variable_defs)
388
+ if icons_html:
389
+ controls_html.append(icons_html)
390
+ for var_name, var_def in variable_defs.items():
391
+ control = render_html_control_for_svg(
392
+ var_name,
393
+ var_def,
394
+ current_values,
395
+ executor,
396
+ resolved_style=resolved_style,
397
+ )
398
+ controls_html.append(control)
399
+
400
+ label_font = variables_style.label.font
401
+ label_color = label_font.color
402
+ assert (
403
+ label_color is not None
404
+ ), "cascade should have populated variables.label.font.color"
405
+ label_weight_raw = label_font.weight
406
+ assert (
407
+ label_weight_raw is not None
408
+ ), "cascade should have populated variables.label.font.weight"
409
+ label_weight = font_weight_as_css(label_weight_raw)
410
+ label_size_raw = label_font.size
411
+ assert (
412
+ label_size_raw is not None
413
+ ), "cascade should have populated variables.label.font.size"
414
+ value_font = variables_style.value.font
415
+ value_weight_raw = value_font.weight
416
+ assert (
417
+ value_weight_raw is not None
418
+ ), "cascade should have populated variables.value.font.weight"
419
+ value_weight = font_weight_as_css(value_weight_raw)
420
+ value_color = value_font.color
421
+ assert (
422
+ value_color is not None
423
+ ), "cascade should have populated variables.value.font.color"
424
+ value_family_raw = value_font.family
425
+ assert (
426
+ value_family_raw is not None
427
+ ), "cascade should have populated variables.value.font.family"
428
+ value_family = html.escape(str(value_family_raw))
429
+ value_size_raw = value_font.size
430
+ assert (
431
+ value_size_raw is not None
432
+ ), "cascade should have populated variables.value.font.size"
433
+ value_numeric_variant = variables_style.value.numeric_variant
434
+ placeholder_color = variables_style.placeholder.font.color
435
+ assert (
436
+ placeholder_color is not None
437
+ ), "cascade should have populated variables.placeholder.font.color"
438
+ accent = resolved_style.accent
439
+ # Semi-transparent focus ring derived from accent color.
440
+ # Inline hex/rgb8 → rgba string so we don't pull in a color library.
441
+ focus_ring = f"color-mix(in srgb, {accent} 25%, transparent)"
442
+
443
+ # Wrap in foreignObject with proper namespace using theme colors.
444
+ # CSS custom properties on .dft-variables are consumed by controls/_styles.css
445
+ # (included in svg/styles.css) for hover, focus, disabled interaction states.
446
+ # overflow:visible on the foreignObject lets absolute-positioned popovers (daterange
447
+ # chip) escape the element's clipping rectangle in Chromium and Safari.
448
+ _vars = resolved_style.variables
449
+ # Title-inline packs controls against the right edge of the band so they
450
+ # sit flush with the board's right margin opposite the title. Other
451
+ # positions (top/bottom) leave them flex-start so they flow naturally from
452
+ # the leading edge of the content area.
453
+ _justify = (
454
+ "flex-end" if variables_style.position == "title-inline" else "flex-start"
455
+ )
456
+ svg = f"""<foreignObject x="0" y="0" width="{width}" height="{height}" style="overflow: visible;">
457
+ <div xmlns="http://www.w3.org/1999/xhtml" class="dft-variables"
458
+ style="display: flex; flex-wrap: wrap; justify-content: {_justify}; gap: {variables_style.gap}px; align-items: center;
459
+ background: {_vars.input.background}; padding: {variables_style.padding}px {variables_style.container_padding}px; border-radius: {variables_style.border.radius}px;
460
+ font-size: {variables_style.font.size}px; box-sizing: border-box; height: 100%;
461
+ overflow: visible;
462
+ --dft-font-family: {font_family};
463
+ --dft-label-color: {label_color};
464
+ --dft-label-weight: {label_weight};
465
+ --dft-label-size: {label_size_raw}px;
466
+ --dft-text-color: {resolved_style.font.color};
467
+ --dft-value-color: {value_color};
468
+ --dft-value-family: {value_family};
469
+ --dft-value-size: {value_size_raw}px;
470
+ --dft-value-weight: {value_weight};
471
+ --dft-value-numeric-variant: {value_numeric_variant};
472
+ --dft-placeholder-color: {placeholder_color};
473
+ --dft-input-bg: {_vars.input.background};
474
+ --dft-input-border: {_vars.border.color};
475
+ --dft-accent-color: {accent};
476
+ --dft-focus-ring: {focus_ring};
477
+ --dft-muted-color: {_vars.font.color};
478
+ --dft-popover-rail-bg: {_vars.popover_rail_background};">
479
+ {"".join(controls_html)}
480
+ </div>
481
+ </foreignObject>"""
482
+ return svg, height
483
+
484
+
485
+ def _resolve_option_values(
486
+ var_def: Variable,
487
+ executor: Executor | None,
488
+ current_values: dict[str, Any],
489
+ ) -> list[str]:
490
+ """Resolve option values from static list, query, or column binding."""
491
+ if var_def.options and var_def.options.static:
492
+ return [str(opt) for opt in var_def.options.static]
493
+ if var_def.options and var_def.options.query and executor:
494
+ return resolve_query_options_for_svg(
495
+ var_def.options.query, executor, current_values
496
+ )
497
+ if var_def.options and var_def.options.column and executor:
498
+ return resolve_column_options_for_svg(var_def.options.column, executor)
499
+ if var_def.column and executor:
500
+ return resolve_column_options_for_svg(var_def.column, executor)
501
+ return []
502
+
503
+
504
+ def _coerce_bool(value: Any) -> bool:
505
+ """Coerce value to bool; raise ValueError for unrecognized inputs."""
506
+ if isinstance(value, bool):
507
+ return value
508
+ if isinstance(value, (int, float)):
509
+ if value == 1:
510
+ return True
511
+ if value == 0:
512
+ return False
513
+ if isinstance(value, str):
514
+ lower = value.strip().lower()
515
+ if lower in ("true", "1", "yes"):
516
+ return True
517
+ if lower in ("false", "0", "no"):
518
+ return False
519
+ raise ValueError(f"Cannot coerce {value!r} to bool; expected true/false/1/0/yes/no")
520
+
521
+
522
+ def _eval_bool_condition(
523
+ expr: bool | str | SingleRowBoolProbe | None,
524
+ *,
525
+ if_none: bool,
526
+ current_values: dict[str, Any],
527
+ executor: Executor | None,
528
+ context: str,
529
+ ) -> bool:
530
+ """Evaluate a bool/str/probe condition and return the result.
531
+
532
+ Shared core for Variable.disabled and LayoutItem.visible; callers pass
533
+ ``if_none`` to control the meaning of a missing (None) value.
534
+
535
+ - None: return ``if_none`` (disabled → False, visible → True).
536
+ - bool: return as-is.
537
+ - str: auto-wrap in ``{{ }}`` if needed, evaluate as Jinja, coerce to bool.
538
+ - SingleRowBoolProbe: execute the named query; expect exactly 1 row; coerce
539
+ the named column to bool.
540
+
541
+ Args:
542
+ expr: The condition value (from YAML or a compiled model field).
543
+ if_none: Value to return when expr is None.
544
+ current_values: Current variable values for Jinja resolution.
545
+ executor: Required when expr is a SingleRowBoolProbe.
546
+ context: Human-readable label for error messages (e.g. "Variable 'region' disabled").
547
+
548
+ Raises:
549
+ ValueError: Wrong row count, missing column, non-bool-coercible value, or
550
+ no executor for a query-backed condition.
551
+ """
552
+ if expr is None:
553
+ return if_none
554
+ if isinstance(expr, bool):
555
+ return expr
556
+ if isinstance(expr, str):
557
+ # Auto-wrap bare variable names / Jinja expressions (no {{ }} required).
558
+ jinja_expr = expr if "{{" in expr else f"{{{{ {expr} }}}}"
559
+ rendered = resolve_jinja_template(jinja_expr, variables=current_values)
560
+ return _coerce_bool(rendered)
561
+ if isinstance(expr, SingleRowBoolProbe):
562
+ if executor is None:
563
+ raise ValueError(
564
+ f"{context} query '{expr.query}' requires an executor but none was provided"
565
+ )
566
+ rows = executor.execute_query(expr.query, current_values)
567
+ if not rows:
568
+ raise ValueError(
569
+ f"{context} query '{expr.query}' returned no rows; expected exactly 1"
570
+ )
571
+ if len(rows) > 1:
572
+ raise ValueError(
573
+ f"{context} query '{expr.query}' returned {len(rows)} rows; expected exactly 1"
574
+ )
575
+ row = rows[0]
576
+ if expr.column not in row:
577
+ raise ValueError(
578
+ f"{context} query '{expr.query}' has no column '{expr.column}'; "
579
+ f"available: {list(row.keys())}"
580
+ )
581
+ return _coerce_bool(row[expr.column])
582
+ raise TypeError(f"unsupported visible/disabled value: {expr!r}")
583
+
584
+
585
+ def _evaluate_disabled(
586
+ var_def: Variable,
587
+ current_values: dict[str, Any],
588
+ executor: Executor | None,
589
+ ) -> bool:
590
+ """Return True when the variable control should be disabled."""
591
+ return _eval_bool_condition(
592
+ var_def.disabled,
593
+ if_none=False,
594
+ current_values=current_values,
595
+ executor=executor,
596
+ context="Variable disabled",
597
+ )
598
+
599
+
600
+ def evaluate_visible(
601
+ expr: bool | str | SingleRowBoolProbe | None,
602
+ current_values: dict[str, Any],
603
+ executor: Executor | None,
604
+ context: str = "layout item",
605
+ ) -> bool:
606
+ """Return True when the layout item should be rendered.
607
+
608
+ Args:
609
+ expr: The visible value from the layout item.
610
+ current_values: Current variable values for Jinja resolution.
611
+ executor: Required when expr is a SingleRowBoolProbe.
612
+ context: Human-readable label for error messages (e.g. "layout item 'kpi_a'").
613
+ """
614
+ return _eval_bool_condition(
615
+ expr,
616
+ if_none=True,
617
+ current_values=current_values,
618
+ executor=executor,
619
+ context=f"{context} visible",
620
+ )
621
+
622
+
623
+ _MONTHS_SHORT = [
624
+ "Jan",
625
+ "Feb",
626
+ "Mar",
627
+ "Apr",
628
+ "May",
629
+ "Jun",
630
+ "Jul",
631
+ "Aug",
632
+ "Sep",
633
+ "Oct",
634
+ "Nov",
635
+ "Dec",
636
+ ]
637
+
638
+
639
+ def _format_daterange_label(start_iso: str, end_iso: str) -> str:
640
+ """Human-readable chip label from ISO date strings.
641
+
642
+ Mirrors JS formatRange() so the server-rendered initial label matches
643
+ what the client renders post-interaction. Falls back to raw ISO on parse error.
644
+ """
645
+ try:
646
+ s = datetime.date.fromisoformat(start_iso)
647
+ e = datetime.date.fromisoformat(end_iso)
648
+ except ValueError:
649
+ return f"{start_iso} – {end_iso}"
650
+
651
+ def fmt(d: datetime.date) -> str:
652
+ return f"{d.day} {_MONTHS_SHORT[d.month - 1]} {d.year}"
653
+
654
+ def fmt_no_yr(d: datetime.date) -> str:
655
+ return f"{d.day} {_MONTHS_SHORT[d.month - 1]}"
656
+
657
+ if s == e:
658
+ return fmt(s)
659
+ if s.year == e.year and s.month == e.month:
660
+ return f"{s.day}–{e.day} {_MONTHS_SHORT[s.month - 1]} {s.year}"
661
+ if s.year == e.year:
662
+ return f"{fmt_no_yr(s)} – {fmt(e)}"
663
+ return f"{fmt(s)} – {fmt(e)}"
664
+
665
+
666
+ def render_html_control_for_svg(
667
+ var_name: str,
668
+ var_def: Variable,
669
+ current_values: dict[str, Any],
670
+ executor: Executor | None = None,
671
+ *,
672
+ resolved_style: MergedStyle,
673
+ ) -> str:
674
+ """Render a single HTML form control for embedding in SVG foreignObject.
675
+
676
+ Colors come from CSS custom properties on the .dft-variables container
677
+ (set by render_interactive_variables_svg); per-instance widths/padding
678
+ are still inline.
679
+
680
+ Args:
681
+ var_name: Variable name
682
+ var_def: Variable definition object
683
+ current_values: Current variable values dict
684
+ executor: Executor for resolving query-based options
685
+ resolved_style: Resolved style for visual properties
686
+
687
+ Returns:
688
+ HTML string for the control
689
+ """
690
+ variables_style = resolved_style.variables
691
+
692
+ input_config = variables_style.input
693
+ control_gap = variables_style.control_gap
694
+ input_padding = input_config.padding
695
+ input_border_radius = input_config.border.radius
696
+
697
+ current = current_values.get(var_name, var_def.default)
698
+ label = var_def.label or format_display_text(
699
+ var_name,
700
+ from_slug=True,
701
+ font=resolve_cascaded_font(variables_style.label.font, "variables.label.font"),
702
+ )
703
+ escaped_label = html.escape(label)
704
+ description = var_def.description
705
+ description_attr = f' title="{html.escape(description)}"' if description else ""
706
+ # Use html.escape for HTML attribute context (name, data-variable)
707
+ escaped_name = html.escape(var_name)
708
+ # Use json.dumps for JavaScript string context, then escape single quotes
709
+ # since we use single-quoted JS strings in HTML attributes
710
+ js_safe_name = json.dumps(var_name)[1:-1].replace(
711
+ "'", "\\'"
712
+ ) # Escape for JS single-quoted strings
713
+
714
+ # Build data attribute for variable dependencies (for cascading dropdowns)
715
+ # This tells the UI which other variables this variable depends on
716
+ depends_on_attr = ""
717
+ if var_def.variable_dependencies:
718
+ deps_list = sorted(var_def.variable_dependencies)
719
+ deps_json = json.dumps(deps_list)
720
+ depends_on_attr = f' data-depends-on="{html.escape(deps_json)}"'
721
+
722
+ # Evaluate disabled state before building any HTML.
723
+ is_disabled = _evaluate_disabled(var_def, current_values, executor)
724
+ disabled_attr = " disabled" if is_disabled else ""
725
+ disabled_wrapper_attrs = (
726
+ ' aria-disabled="true" data-disabled="true"' if is_disabled else ""
727
+ )
728
+
729
+ input_type = var_def.input
730
+
731
+ # Slider bounds from data-aware refinement (set below, used in slider branch)
732
+ slider_min_override: int | float | None = None
733
+ slider_max_override: int | float | None = None
734
+ slider_step_override: int | float | None = None
735
+
736
+ # Resolve options for select/multiselect — needed for both rendering and
737
+ # data-aware type refinement
738
+ option_values: list[str] = []
739
+ if input_type in ("select", "multiselect"):
740
+ option_values = list(
741
+ dict.fromkeys(_resolve_option_values(var_def, executor, current_values))
742
+ )
743
+
744
+ # Data-aware refinement: inspect resolved options to pick a better widget
745
+ if var_def.input_auto_detected and option_values:
746
+ refined = refine_input_type_from_data(var_def, option_values)
747
+ input_type = refined.input_type
748
+ # Apply auto-computed slider bounds (without mutating the compiled var_def)
749
+ if refined.slider_min is not None:
750
+ slider_min_override = refined.slider_min
751
+ slider_max_override = refined.slider_max
752
+ slider_step_override = refined.slider_step
753
+
754
+ if input_type in ("select", "multiselect"):
755
+ options = ['<option value="">-- All --</option>']
756
+
757
+ is_placeholder = current is None or current == ""
758
+ for opt in option_values:
759
+ escaped_opt = html.escape(opt)
760
+ selected = " selected" if str(current) == opt else ""
761
+ options.append(
762
+ f'<option value="{escaped_opt}"{selected}>{escaped_opt}</option>'
763
+ )
764
+ placeholder_attr = ' data-placeholder="true"' if is_placeholder else ""
765
+
766
+ return f"""<div class="variable-control dft-variable" data-variable-id="{escaped_name}" data-variable-label="{escaped_label}"{depends_on_attr}{description_attr}{disabled_wrapper_attrs} style="display: flex; align-items: center; gap: {control_gap}px;">
767
+ <label class="dft-variable-label">{escaped_label}:</label>
768
+ <select name="{escaped_name}" class="dft-variable-input" data-variable="{escaped_name}" onchange="updateVariable('{js_safe_name}', this.value)"{disabled_attr}{placeholder_attr}
769
+ style="padding: {input_padding.top}px {input_padding.right}px; border-radius: {input_border_radius}px; font-size: {variables_style.font.size}px;">
770
+ {"".join(options)}
771
+ </select>
772
+ </div>"""
773
+
774
+ elif input_type == "checkbox":
775
+ # Handle string booleans from data-aware refinement (select → checkbox)
776
+ if isinstance(current, str):
777
+ current = current.lower().strip() in ("true", "yes", "1")
778
+ checked = " checked" if current else ""
779
+ return f"""<div class="variable-control dft-variable" data-variable-id="{escaped_name}" data-variable-label="{escaped_label}"{depends_on_attr}{description_attr}{disabled_wrapper_attrs} style="display: flex; align-items: center; gap: {control_gap}px; cursor: pointer;">
780
+ <input type="checkbox" name="{escaped_name}" class="dft-variable-input" data-variable="{escaped_name}"
781
+ onchange="updateVariable('{js_safe_name}', this.checked)"{checked}{disabled_attr}
782
+ style="width: {input_config.widths.checkbox}px; height: {input_config.widths.checkbox}px;"/>
783
+ <span class="dft-variable-label">{escaped_label}</span>
784
+ </div>"""
785
+
786
+ elif input_type in ("slider", "range"):
787
+ # Data-aware overrides (all-or-none from RefinedType) take precedence
788
+ if slider_min_override is not None:
789
+ min_val = slider_min_override
790
+ max_val = slider_max_override
791
+ step_val = slider_step_override
792
+ else:
793
+ min_val = (
794
+ var_def.min
795
+ if var_def.min is not None
796
+ else input_config.range.default_min
797
+ )
798
+ max_val = (
799
+ var_def.max
800
+ if var_def.max is not None
801
+ else input_config.range.default_max
802
+ )
803
+ step_val = (
804
+ var_def.step
805
+ if var_def.step is not None
806
+ else input_config.range.default_step
807
+ )
808
+ raw_value = current if current is not None else min_val
809
+ # Escape value for HTML attribute context to prevent XSS
810
+ escaped_value = html.escape(str(raw_value))
811
+ return f"""<div class="variable-control dft-variable" data-variable-id="{escaped_name}" data-variable-label="{escaped_label}"{depends_on_attr}{description_attr}{disabled_wrapper_attrs} style="display: flex; align-items: center; gap: {control_gap}px;">
812
+ <label class="dft-variable-label">{escaped_label}:</label>
813
+ <input type="range" name="{escaped_name}" class="dft-variable-input" data-variable="{escaped_name}"
814
+ min="{min_val}" max="{max_val}" step="{step_val}" value="{escaped_value}"
815
+ oninput="this.nextElementSibling.textContent = this.value"
816
+ onchange="updateVariable('{js_safe_name}', this.value)"{disabled_attr}
817
+ style="width: {input_config.widths.range}px;"/>
818
+ <span class="dft-variable-slider-value" style="min-width: {input_config.widths.slider_value_min}px;">{escaped_value}</span>
819
+ </div>"""
820
+
821
+ elif input_type in ("text", "input"):
822
+ value = html.escape(str(current or ""))
823
+ placeholder_attr = (
824
+ ' data-placeholder="true"' if current is None or current == "" else ""
825
+ )
826
+ return f"""<div class="variable-control dft-variable" data-variable-id="{escaped_name}" data-variable-label="{escaped_label}"{depends_on_attr}{description_attr}{disabled_wrapper_attrs} style="display: flex; align-items: center; gap: {control_gap}px;">
827
+ <label class="dft-variable-label">{escaped_label}:</label>
828
+ <input type="text" name="{escaped_name}" class="dft-variable-input" data-variable="{escaped_name}" value="{value}"
829
+ onchange="updateVariable('{js_safe_name}', this.value)"{disabled_attr}{placeholder_attr}
830
+ style="padding: {input_padding.top}px {input_padding.right}px; border-radius: {input_border_radius}px; width: {input_config.widths.text}px;"/>
831
+ </div>"""
832
+
833
+ elif input_type == "number":
834
+ # Escape value for HTML attribute context to prevent XSS
835
+ value = html.escape(str(current)) if current is not None else ""
836
+ placeholder_attr = ' data-placeholder="true"' if current is None else ""
837
+ min_attr = f' min="{var_def.min}"' if var_def.min is not None else ""
838
+ max_attr = f' max="{var_def.max}"' if var_def.max is not None else ""
839
+ step_attr = f' step="{var_def.step}"' if var_def.step is not None else ""
840
+ return f"""<div class="variable-control dft-variable" data-variable-id="{escaped_name}" data-variable-label="{escaped_label}"{depends_on_attr}{description_attr}{disabled_wrapper_attrs} style="display: flex; align-items: center; gap: {control_gap}px;">
841
+ <label class="dft-variable-label">{escaped_label}:</label>
842
+ <input type="number" name="{escaped_name}" class="dft-variable-input" data-variable="{escaped_name}" value="{value}"{min_attr}{max_attr}{step_attr}
843
+ onchange="updateVariable('{js_safe_name}', this.value)"{disabled_attr}{placeholder_attr}
844
+ style="padding: {input_padding.top}px {input_padding.right}px; border-radius: {input_border_radius}px; width: {input_config.widths.number}px;"/>
845
+ </div>"""
846
+
847
+ elif input_type in ("date", "datepicker"):
848
+ # Escape value for HTML attribute context to prevent XSS
849
+ value = html.escape(str(current or ""))
850
+ placeholder_attr = (
851
+ ' data-placeholder="true"' if current is None or current == "" else ""
852
+ )
853
+ return f"""<div class="variable-control dft-variable" data-variable-id="{escaped_name}" data-variable-label="{escaped_label}"{depends_on_attr}{description_attr}{disabled_wrapper_attrs} style="display: flex; align-items: center; gap: {control_gap}px;">
854
+ <label class="dft-variable-label">{escaped_label}:</label>
855
+ <input type="date" name="{escaped_name}" class="dft-variable-input" data-variable="{escaped_name}" value="{value}"
856
+ onchange="updateVariable('{js_safe_name}', this.value)"{disabled_attr}{placeholder_attr}
857
+ style="padding: {input_padding.top}px {input_padding.right}px; border-radius: {input_border_radius}px;"/>
858
+ </div>"""
859
+
860
+ elif input_type == "daterange":
861
+ # Resolve initial chip state from pre-loaded values.
862
+ start_value = ""
863
+ end_value = ""
864
+ if isinstance(current, list) and len(current) >= 2:
865
+ start_value = html.escape(str(current[0]))
866
+ end_value = html.escape(str(current[1]))
867
+ chip_active = bool(start_value and end_value)
868
+ chip_label = (
869
+ _format_daterange_label(start_value, end_value)
870
+ if chip_active
871
+ else _READ_ONLY_UNSET_DATERANGE
872
+ )
873
+ chip_state = 'data-active="true"' if chip_active else 'data-placeholder="true"'
874
+
875
+ return f"""<div class="variable-control dft-variable" data-variable-id="{escaped_name}" data-variable-label="{escaped_label}"{depends_on_attr}{description_attr}{disabled_wrapper_attrs} style="display: flex; align-items: center; gap: {control_gap}px;">
876
+ <label class="dft-variable-label">{escaped_label}:</label>
877
+ <div class="dft-chip-host">
878
+ <button type="button" class="dft-chip" data-variable="{escaped_name}" {chip_state} aria-haspopup="true" aria-expanded="false"{disabled_attr}>
879
+ <span class="dft-chip-icon" aria-hidden="true">
880
+ <svg width="16" height="16"><use href="#dft-icon-calendar"/></svg>
881
+ </span>
882
+ <span class="dft-chip-label">{chip_label}</span>
883
+ </button>
884
+ <button type="button" class="dft-chip-clear" title="Clear" aria-label="Clear date range"{disabled_attr}>×</button>
885
+ <div class="dft-popover" aria-hidden="true">
886
+ <div class="dft-preset-rail"></div>
887
+ <div class="dft-calendar-area" data-variable="{escaped_name}" data-start="{start_value}" data-end="{end_value}"></div>
888
+ </div>
889
+ </div>
890
+ </div>"""
891
+
892
+ # Fallback: read-only display
893
+ return f"""<div class="variable-control dft-variable" data-variable-id="{escaped_name}" data-variable-label="{escaped_label}"{depends_on_attr}{description_attr}{disabled_wrapper_attrs} style="display: flex; align-items: center; gap: {control_gap}px;">
894
+ <span class="dft-variable-label">{escaped_label}:</span>
895
+ <span class="dft-variable-readonly-value">{html.escape(str(current or ""))}</span>
896
+ </div>"""
897
+
898
+
899
+ def generate_svg_variable_script() -> str:
900
+ """Generate JavaScript for SVG variable interactions.
901
+
902
+ This script is embedded in the SVG and handles:
903
+ - Variable change events (updateVariable function)
904
+ - URL parameter updates
905
+ - Chart loading states
906
+ - Parent frame communication (for playground/suite embedding)
907
+ - URL parameter initialization on load
908
+
909
+ Returns:
910
+ SVG script element with embedded JavaScript
911
+ """
912
+ from pathlib import Path
913
+
914
+ script_path = Path(__file__).parent / "templates/scripts/variables.js"
915
+ return embed_svg_script(script_path.read_text())
916
+
917
+
918
+ # Calendar icon SVG symbol — emitted once per face via generate_svg_variable_icons().
919
+ # Each daterange chip references it via <use href="#dft-icon-calendar">.
920
+ _CALENDAR_ICON_SYMBOL = (
921
+ '<svg width="0" height="0" style="position:absolute;overflow:hidden" aria-hidden="true">'
922
+ '<symbol id="dft-icon-calendar" viewBox="0 0 24 24" fill="none"'
923
+ ' stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">'
924
+ '<rect x="3.5" y="5.5" width="17" height="4.5" rx="1" fill="currentColor" opacity="0.13" stroke="none"/>'
925
+ '<rect x="3" y="5" width="18" height="16" rx="2" stroke-width="1.75"/>'
926
+ '<line x1="3" y1="10" x2="21" y2="10" stroke-width="1.75"/>'
927
+ '<line x1="8" y1="3" x2="8" y2="7" stroke-width="2.25"/>'
928
+ '<line x1="16" y1="3" x2="16" y2="7" stroke-width="2.25"/>'
929
+ "</symbol></svg>"
930
+ )
931
+
932
+
933
+ def generate_svg_variable_icons(variable_defs: dict[str, Any]) -> str:
934
+ """Generate shared SVG icon symbols for variable controls.
935
+
936
+ Emits the calendar symbol when at least one daterange control is present.
937
+ Emitting it once avoids duplicate id='dft-icon-calendar' when multiple
938
+ daterange variables appear on the same face.
939
+
940
+ Returns:
941
+ HTML string with hidden SVG symbol definitions, or empty string.
942
+ """
943
+ needs_calendar = any(
944
+ getattr(v, "input", None) == "daterange" for v in variable_defs.values()
945
+ )
946
+ return _CALENDAR_ICON_SYMBOL if needs_calendar else ""