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,976 @@
1
+ /**
2
+ * Variable interactivity for datafaces.
3
+ *
4
+ * This script handles:
5
+ * - Variable change events (updateVariable function)
6
+ * - URL parameter updates
7
+ * - Chart loading states
8
+ * - Parent frame communication (for playground/suite embedding)
9
+ * - URL parameter initialization on load
10
+ * - Variable hover highlighting
11
+ * - Date range chip+popover control (makeChip factory)
12
+ *
13
+ * Note: Chart menus are handled by Suite's JavaScript (init.js), not here.
14
+ *
15
+ */
16
+ (function() {
17
+ // Guard against double execution (e.g., if script runs both on parse and via executeEmbeddedScripts)
18
+ if (window.__dfVariablesInitialized) return;
19
+ window.__dfVariablesInitialized = true;
20
+
21
+ function setVariableIfMissing(vars, name, value) {
22
+ if (Object.prototype.hasOwnProperty.call(vars, name)) {
23
+ return;
24
+ }
25
+ vars[name] = value;
26
+ }
27
+
28
+ function markDependentChartsLoading(name) {
29
+ var charts = document.querySelectorAll('[data-var-' + name + ']');
30
+ for (var i = 0; i < charts.length; i++) {
31
+ var chart = charts[i];
32
+ // Add loading class
33
+ var currentClass = chart.getAttribute('class') || '';
34
+ if (currentClass.indexOf('loading') === -1) {
35
+ chart.setAttribute('class', currentClass + (currentClass ? ' ' : '') + 'loading');
36
+ }
37
+
38
+ // Add spinner if not already present
39
+ if (!chart.querySelector('.dft-chart-spinner')) {
40
+ var spinner = document.createElementNS('http://www.w3.org/2000/svg', 'g');
41
+ spinner.setAttribute('class', 'dft-chart-spinner');
42
+ // Get chart bounds for centering spinner
43
+ var bbox = chart.getBBox ? chart.getBBox() : {width: 200, height: 200};
44
+ var centerX = bbox.x + bbox.width / 2;
45
+ var centerY = bbox.y + bbox.height / 2;
46
+
47
+ var circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
48
+ circle.setAttribute('cx', centerX);
49
+ circle.setAttribute('cy', centerY);
50
+ circle.setAttribute('r', '14');
51
+ circle.setAttribute('fill', 'none');
52
+ circle.setAttribute('stroke', '#e0e0e0');
53
+ circle.setAttribute('stroke-width', '3');
54
+ circle.setAttribute('stroke-dasharray', '20 60');
55
+ circle.setAttribute('stroke-dashoffset', '0');
56
+ circle.style.animation = 'dft-spin 0.8s linear infinite';
57
+
58
+ spinner.appendChild(circle);
59
+ chart.appendChild(spinner);
60
+ }
61
+ }
62
+
63
+ return charts;
64
+ }
65
+
66
+ function scheduleLoadingCleanup(charts) {
67
+ setTimeout(function() {
68
+ for (var i = 0; i < charts.length; i++) {
69
+ var chart = charts[i];
70
+ var currentClass = chart.getAttribute('class') || '';
71
+ chart.setAttribute('class', currentClass.replace(/\s*loading\s*/g, ' ').trim());
72
+ var spinner = chart.querySelector('.dft-chart-spinner');
73
+ if (spinner) {
74
+ spinner.remove();
75
+ }
76
+ }
77
+ }, 5000);
78
+ }
79
+
80
+ // Update URL and reload (or notify parent)
81
+ function updateVariable(name, value) {
82
+ // Mark dependent charts as loading (add loading class and spinner)
83
+ var charts = markDependentChartsLoading(name);
84
+
85
+ // Restore after timeout (fallback if reload fails or is prevented)
86
+ scheduleLoadingCleanup(charts);
87
+
88
+ // Update URL
89
+ var url = new URL(window.location);
90
+ if (value === '' || value === null || value === false) {
91
+ url.searchParams.delete(name);
92
+ } else {
93
+ url.searchParams.set(name, String(value));
94
+ }
95
+
96
+ // Notify parent (suite/playground) or reload (standalone)
97
+ // Using '*' is safe here because:
98
+ // 1. Iframe content is generated by us (not user input)
99
+ // 2. Parent checks message.type before processing
100
+ // 3. Blob URLs have null origin, so specific origin targeting doesn't work
101
+ if (window.parent !== window) {
102
+ var vars = getAllVariableValues();
103
+ // Form controls haven't re-rendered yet, so patch the outgoing
104
+ // snapshot with the pending value before notifying the parent.
105
+ setVariableIfMissing(vars, name, value);
106
+ window.parent.postMessage({
107
+ type: 'dft-variable-change',
108
+ variables: vars
109
+ }, '*');
110
+ } else if (typeof window.__dfHandleVariableUpdate === 'function') {
111
+ // Suite registers this hook to re-render in place (no page reload).
112
+ window.__dfHandleVariableUpdate(url);
113
+ } else {
114
+ // Plain dft serve: full page reload. Save scroll so initializeFromURL
115
+ // can restore it after the new page renders.
116
+ try {
117
+ sessionStorage.setItem('__dfScrollY_' + window.location.pathname, String(window.scrollY));
118
+ } catch (e) { /* sessionStorage may be unavailable */ }
119
+ window.location.href = url.toString();
120
+ }
121
+ }
122
+
123
+ // Expose functions to window for inline HTML event handlers (onchange="updateVariable(...)")
124
+ // These are used by foreignObject controls rendered by render_html_control_for_svg
125
+ window.updateVariable = updateVariable;
126
+
127
+ // ── Date range chip+popover ──────────────────────────────────────────────
128
+
129
+ // 8 presets: Today / Last 7 / Last 28 / Last 90 / Last 12 months / MTD / YTD / Custom
130
+ var PRESETS = [
131
+ { id: 'today', label: 'Today' },
132
+ { id: 'last_7_days', label: 'Last 7 days' },
133
+ { id: 'last_28_days', label: 'Last 28 days' },
134
+ { id: 'last_90_days', label: 'Last 90 days' },
135
+ { id: 'last_12_months', label: 'Last 12 months' },
136
+ { id: 'divider' },
137
+ { id: 'mtd', label: 'Month to date' },
138
+ { id: 'ytd', label: 'Year to date' },
139
+ { id: 'divider' },
140
+ { id: 'custom', label: 'Custom' },
141
+ ];
142
+
143
+ // All date math is relative to "today at midnight local time" so that
144
+ // a frozen today in tests resolves deterministically.
145
+ function resolvePreset(id) {
146
+ var now = new Date();
147
+ var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
148
+ var end = new Date(today);
149
+ var start = new Date(today);
150
+ switch (id) {
151
+ case 'today': break;
152
+ case 'last_7_days': start.setDate(end.getDate() - 6); break;
153
+ case 'last_28_days': start.setDate(end.getDate() - 27); break;
154
+ case 'last_90_days': start.setDate(end.getDate() - 89); break;
155
+ // Compute start as one year back from today + 1 day in a single constructor call
156
+ // to avoid the two-step setFullYear+setDate rollover on Feb 29 leap years.
157
+ case 'last_12_months': start = new Date(end.getFullYear() - 1, end.getMonth(), end.getDate() + 1); break;
158
+ case 'mtd': start = new Date(end.getFullYear(), end.getMonth(), 1); break;
159
+ case 'ytd': start = new Date(end.getFullYear(), 0, 1); break;
160
+ case 'custom': return null;
161
+ default: return null;
162
+ }
163
+ return [start, end];
164
+ }
165
+
166
+ // ISO date string from a Date object (YYYY-MM-DD local time).
167
+ function toISO(d) {
168
+ var m = String(d.getMonth() + 1).padStart(2, '0');
169
+ var day = String(d.getDate()).padStart(2, '0');
170
+ return d.getFullYear() + '-' + m + '-' + day;
171
+ }
172
+
173
+ // ISO YYYY-MM-DD shape check (does not check calendar validity, just structure).
174
+ var _ISO_RE = /^\d{4}-\d{2}-\d{2}$/;
175
+
176
+ // Date from an ISO string without timezone shift (treat as local midnight).
177
+ // Returns null when the string is not a well-formed YYYY-MM-DD literal.
178
+ function fromISO(s) {
179
+ if (!_ISO_RE.test(s)) return null;
180
+ var parts = s.split('-');
181
+ var d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
182
+ // Reject dates that JS rolled over (e.g. Feb 30 → Mar 2).
183
+ if (d.getFullYear() !== parseInt(parts[0], 10) ||
184
+ d.getMonth() !== parseInt(parts[1], 10) - 1 ||
185
+ d.getDate() !== parseInt(parts[2], 10)) return null;
186
+ return d;
187
+ }
188
+
189
+ // Human-readable chip label from a [start, end] Date pair.
190
+ // Collapses same-day / same-month / same-year / cross-year ranges.
191
+ var MONTHS_SHORT = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
192
+ function fmtDate(d) { return d.getDate() + ' ' + MONTHS_SHORT[d.getMonth()] + ' ' + d.getFullYear(); }
193
+ function fmtDateNoYr(d) { return d.getDate() + ' ' + MONTHS_SHORT[d.getMonth()]; }
194
+
195
+ function formatRange(start, end) {
196
+ var sameDay = start.getTime() === end.getTime();
197
+ var sameMonth = start.getFullYear() === end.getFullYear() && start.getMonth() === end.getMonth();
198
+ var sameYear = start.getFullYear() === end.getFullYear();
199
+ if (sameDay) return fmtDate(start);
200
+ if (sameMonth) return start.getDate() + '–' + end.getDate() + ' ' + MONTHS_SHORT[start.getMonth()] + ' ' + start.getFullYear();
201
+ if (sameYear) return fmtDateNoYr(start) + ' – ' + fmtDate(end);
202
+ return fmtDate(start) + ' – ' + fmtDate(end);
203
+ }
204
+
205
+ function buildRail(railEl, onPick) {
206
+ railEl.innerHTML = '';
207
+ PRESETS.forEach(function(p) {
208
+ if (p.id === 'divider') {
209
+ var div = document.createElement('div');
210
+ div.className = 'dft-preset-divider';
211
+ railEl.appendChild(div);
212
+ } else {
213
+ var btn = document.createElement('button');
214
+ btn.type = 'button';
215
+ btn.textContent = p.label;
216
+ btn.dataset.preset = p.id;
217
+ btn.addEventListener('click', function() { onPick(p); });
218
+ railEl.appendChild(btn);
219
+ }
220
+ });
221
+ }
222
+
223
+ function markRailActive(railEl, presetId) {
224
+ railEl.querySelectorAll('button').forEach(function(b) {
225
+ b.classList.toggle('dft-preset-active', b.dataset.preset === presetId);
226
+ });
227
+ }
228
+
229
+ // Given a [start, end] pair, return the preset id whose resolved range
230
+ // matches, or 'custom' for a non-matching range, or null for an empty range.
231
+ // Used to restore the active-preset highlight after an iframe re-render
232
+ // wipes the rail's local mark.
233
+ function matchPreset(range) {
234
+ if (!range[0] || !range[1]) return null;
235
+ var s = toISO(range[0]), e = toISO(range[1]);
236
+ for (var i = 0; i < PRESETS.length; i++) {
237
+ var p = PRESETS[i];
238
+ if (!p.id || p.id === 'divider' || p.id === 'custom') continue;
239
+ var r = resolvePreset(p.id);
240
+ if (r && toISO(r[0]) === s && toISO(r[1]) === e) return p.id;
241
+ }
242
+ return 'custom';
243
+ }
244
+
245
+ // Per-instance chip+popover factory.
246
+ // rootEl is the .dft-chip-host element; varName is the DFT variable id.
247
+ // Returns a handle so the global outside-click + Esc handlers can reach in.
248
+ function makeChip(rootEl, varName) {
249
+ var trigger = rootEl.querySelector('.dft-chip');
250
+ var labelEl = rootEl.querySelector('.dft-chip-label');
251
+ var clearBtn = rootEl.querySelector('.dft-chip-clear');
252
+ var popover = rootEl.querySelector('.dft-popover');
253
+ var rail = rootEl.querySelector('.dft-preset-rail');
254
+ var calEl = rootEl.querySelector('.dft-calendar-area');
255
+
256
+ var range = [null, null];
257
+ var hoverDate = null;
258
+ var viewMonth = new Date();
259
+ var now = new Date();
260
+ var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
261
+
262
+ // Restore server-rendered initial value if present (data-start / data-end).
263
+ var initStart = calEl.dataset.start;
264
+ var initEnd = calEl.dataset.end;
265
+ if (initStart && initEnd) {
266
+ var parsedStart = fromISO(initStart);
267
+ var parsedEnd = fromISO(initEnd);
268
+ if (parsedStart && parsedEnd) {
269
+ range = [parsedStart, parsedEnd];
270
+ viewMonth = new Date(range[1].getFullYear(), range[1].getMonth(), 1);
271
+ }
272
+ // Chip label and state were server-rendered; don't repaint here.
273
+ }
274
+
275
+ // Persist the resolved [start, end] pair as ISO strings via the standard variable pipeline.
276
+ function commitRange() {
277
+ if (range[0] && range[1]) {
278
+ calEl.dataset.start = toISO(range[0]);
279
+ calEl.dataset.end = toISO(range[1]);
280
+ updateVariable(varName, JSON.stringify([toISO(range[0]), toISO(range[1])]));
281
+ }
282
+ }
283
+
284
+ function setChipValue(text) {
285
+ labelEl.textContent = text;
286
+ trigger.setAttribute('data-active', 'true');
287
+ trigger.removeAttribute('data-placeholder');
288
+ }
289
+
290
+ function setChipEmpty() {
291
+ labelEl.textContent = 'All dates';
292
+ trigger.removeAttribute('data-active');
293
+ trigger.setAttribute('data-placeholder', 'true');
294
+ }
295
+
296
+ function clearAll() {
297
+ range = [null, null];
298
+ hoverDate = null;
299
+ calEl.dataset.start = '';
300
+ calEl.dataset.end = '';
301
+ setChipEmpty();
302
+ markRailActive(rail, null);
303
+ rebuildCalendar();
304
+ updateVariable(varName, '');
305
+ }
306
+
307
+ // ── Calendar render pipeline ─────────────────────────────────────────
308
+ // rebuildCalendar — full DOM rebuild. Called on pickDay, month nav, preset.
309
+ // paintCalendarState — class-only mutation on existing cells. Called on hover.
310
+ // Re-rendering on hover replaces the hovered cell and the next mouseenter
311
+ // doesn't fire until the user moves away and back — sticky-hover bug.
312
+ // Class mutation leaves the DOM stable and avoids this.
313
+
314
+ function rebuildCalendar() {
315
+ var year = viewMonth.getFullYear();
316
+ var month = viewMonth.getMonth();
317
+ var firstDay = new Date(year, month, 1);
318
+ var startWeek = firstDay.getDay();
319
+ var monthName = firstDay.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
320
+
321
+ // Build via DOM API (not innerHTML) so structural nesting errors
322
+ // become type errors rather than silent mis-parses.
323
+ calEl.innerHTML = '';
324
+
325
+ // Header: ‹ Month YYYY ›
326
+ var header = document.createElement('div');
327
+ header.className = 'dft-cal-header';
328
+
329
+ var prevBtn = document.createElement('button');
330
+ prevBtn.type = 'button';
331
+ prevBtn.className = 'dft-cal-nav';
332
+ prevBtn.setAttribute('aria-label', 'Previous month');
333
+ prevBtn.textContent = '‹'; // ‹
334
+ prevBtn.addEventListener('click', function() {
335
+ viewMonth = new Date(year, month - 1, 1);
336
+ rebuildCalendar();
337
+ });
338
+
339
+ var monthSpan = document.createElement('span');
340
+ monthSpan.className = 'dft-cal-month';
341
+ monthSpan.textContent = monthName;
342
+
343
+ var nextBtn = document.createElement('button');
344
+ nextBtn.type = 'button';
345
+ nextBtn.className = 'dft-cal-nav';
346
+ nextBtn.setAttribute('aria-label', 'Next month');
347
+ nextBtn.textContent = '›'; // ›
348
+ nextBtn.addEventListener('click', function() {
349
+ viewMonth = new Date(year, month + 1, 1);
350
+ rebuildCalendar();
351
+ });
352
+
353
+ header.appendChild(prevBtn);
354
+ header.appendChild(monthSpan);
355
+ header.appendChild(nextBtn);
356
+ calEl.appendChild(header);
357
+
358
+ // Grid: 7 day-of-week labels + 42 cells (6 rows × 7 cols, fixed height)
359
+ var grid = document.createElement('div');
360
+ grid.className = 'dft-cal-grid';
361
+
362
+ var DOW = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
363
+ DOW.forEach(function(d) {
364
+ var dow = document.createElement('div');
365
+ dow.className = 'dft-cal-dow';
366
+ dow.textContent = d;
367
+ grid.appendChild(dow);
368
+ });
369
+
370
+ var cursor = new Date(year, month, 1 - startWeek);
371
+ for (var i = 0; i < 42; i++) {
372
+ var dt = new Date(cursor);
373
+ var isOther = dt.getMonth() !== month;
374
+ var isToday = dt.getTime() === today.getTime();
375
+ var cell = document.createElement('button');
376
+ cell.type = 'button';
377
+ cell.className = 'dft-cal-cell';
378
+ if (isOther) {
379
+ cell.classList.add('dft-other-month');
380
+ cell.tabIndex = -1;
381
+ }
382
+ if (isToday) cell.classList.add('dft-today');
383
+ cell.textContent = dt.getDate();
384
+ cell.dataset.day = String(dt.getTime());
385
+ cell.setAttribute('aria-label', dt.toLocaleDateString('en-US', {weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'}));
386
+ // Capture dt for closure
387
+ (function(cellEl, cellDate) {
388
+ cellEl.addEventListener('click', function() { pickDay(cellDate.getTime()); });
389
+ cellEl.addEventListener('mouseenter', function() {
390
+ if (range[0] && !range[1]) {
391
+ hoverDate = new Date(cellDate.getTime());
392
+ paintCalendarState();
393
+ }
394
+ });
395
+ }(cell, dt));
396
+ grid.appendChild(cell);
397
+ cursor.setDate(cursor.getDate() + 1);
398
+ }
399
+ calEl.appendChild(grid);
400
+
401
+ // Footer: [Clear] | [Apply]. Apply is always present; Clear only
402
+ // when there's a selection to clear.
403
+ //
404
+ // Lingering-popover pattern: adopt the auto-commit half (range[1]
405
+ // pick writes the URL), drop the auto-close half (popover stays
406
+ // open until Apply). Apply's only job is closing the popover; the
407
+ // commit already happened on pickDay. Matches Stripe's auto-commit
408
+ // + Linear's lingering popover.
409
+ var footer = document.createElement('div');
410
+ footer.className = 'dft-cal-footer';
411
+ if (range[0] !== null) {
412
+ var clearAction = document.createElement('button');
413
+ clearAction.type = 'button';
414
+ clearAction.className = 'dft-cal-action';
415
+ clearAction.setAttribute('data-action', 'clear');
416
+ clearAction.textContent = 'Clear';
417
+ // Push Clear to the left; Apply pins to the right via the
418
+ // footer's justify-content: flex-end. CSS-side `:not()` and
419
+ // quoted attribute selectors trip resvg's parser in the PNG
420
+ // converter, so we set this inline rather than in a stylesheet.
421
+ clearAction.style.marginRight = 'auto';
422
+ footer.appendChild(clearAction);
423
+ }
424
+ var applyAction = document.createElement('button');
425
+ applyAction.type = 'button';
426
+ applyAction.className = 'dft-cal-action dft-cal-action-primary';
427
+ applyAction.setAttribute('data-action', 'apply');
428
+ applyAction.textContent = 'Apply';
429
+ footer.appendChild(applyAction);
430
+ // Single dispatch on the footer — matches the prototype's
431
+ // doAction() pattern. Binding to the footer (recreated each
432
+ // rebuild) avoids re-attaching per-button listeners.
433
+ footer.addEventListener('click', function(e) {
434
+ var btn = e.target.closest('[data-action]');
435
+ if (!btn) return;
436
+ var action = btn.getAttribute('data-action');
437
+ if (action === 'clear') clearAll();
438
+ else if (action === 'apply') closePopover();
439
+ });
440
+ calEl.appendChild(footer);
441
+
442
+ paintCalendarState();
443
+ markRailActive(rail, matchPreset(range));
444
+ }
445
+
446
+ function paintCalendarState() {
447
+ var startTs = range[0] && range[0].getTime();
448
+ var endTs = range[1] && range[1].getTime();
449
+ var previewing = startTs && !endTs && hoverDate;
450
+ var hoverTs = previewing && hoverDate.getTime();
451
+ var lo = previewing ? Math.min(startTs, hoverTs) : null;
452
+ var hi = previewing ? Math.max(startTs, hoverTs) : null;
453
+
454
+ calEl.querySelectorAll('.dft-cal-cell').forEach(function(c) {
455
+ var ts = parseInt(c.dataset.day, 10);
456
+ var isStart = startTs && ts === startTs;
457
+ var isEnd = endTs && ts === endTs;
458
+ var inRange = startTs && endTs && ts > startTs && ts < endTs;
459
+ var inPreview = previewing && ts > lo && ts < hi;
460
+ var isPreviewEnd = previewing && ts === hoverTs && ts !== startTs;
461
+
462
+ c.classList.toggle('dft-selected', Boolean(isStart || isEnd));
463
+ c.classList.toggle('dft-range-start', Boolean(isStart));
464
+ c.classList.toggle('dft-range-end', Boolean(isEnd));
465
+ c.classList.toggle('dft-in-range', Boolean(inRange));
466
+ c.classList.toggle('dft-preview-in-range', Boolean(inPreview));
467
+ c.classList.toggle('dft-preview-end', Boolean(isPreviewEnd));
468
+ });
469
+ }
470
+
471
+ function pickDay(timestamp) {
472
+ var dt = new Date(timestamp);
473
+ if (dt.getMonth() !== viewMonth.getMonth()) {
474
+ viewMonth = new Date(dt.getFullYear(), dt.getMonth(), 1);
475
+ }
476
+ if (!range[0] || (range[0] && range[1])) {
477
+ range = [dt, null];
478
+ } else if (dt < range[0]) {
479
+ range = [dt, range[0]];
480
+ } else if (dt.getTime() === range[0].getTime()) {
481
+ range = [dt, dt];
482
+ } else {
483
+ range = [range[0], dt];
484
+ }
485
+ hoverDate = null;
486
+ if (range[0] && range[1]) {
487
+ setChipValue(formatRange(range[0], range[1]));
488
+ markRailActive(rail, 'custom');
489
+ commitRange();
490
+ } else {
491
+ markRailActive(rail, null);
492
+ }
493
+ rebuildCalendar();
494
+ }
495
+
496
+ // ── Wire up listeners ────────────────────────────────────────────────
497
+ clearBtn.addEventListener('click', function(e) {
498
+ e.stopPropagation();
499
+ clearAll();
500
+ });
501
+
502
+ buildRail(rail, function(p) {
503
+ var r = resolvePreset(p.id);
504
+ if (r) {
505
+ range = r;
506
+ viewMonth = new Date(r[1].getFullYear(), r[1].getMonth(), 1);
507
+ setChipValue(p.label);
508
+ markRailActive(rail, p.id);
509
+ commitRange();
510
+ } else {
511
+ markRailActive(rail, p.id);
512
+ }
513
+ rebuildCalendar();
514
+ });
515
+
516
+ // Popover lives in the SVG foreignObject by default — but SVG paints in
517
+ // document order, so later rows paint over the popover (overflow:visible
518
+ // can't escape paint order). On open, reparent the popover to document.body
519
+ // and switch to position:fixed so it floats above the SVG entirely.
520
+ var popoverOriginalParent = popover.parentNode;
521
+ // CSS custom properties for theme colors are set inline on .dft-variables.
522
+ // Moving the popover to document.body breaks that cascade, so we copy
523
+ // the props that should track the theme onto the popover itself.
524
+ //
525
+ // We deliberately do NOT carry --dft-text-color / --dft-muted-color /
526
+ // --dft-input-bg: the popover is an always-light card (--dft-popover-bg
527
+ // defaults to white), and copying dark-theme text colors onto a white
528
+ // card produces invisible-on-white ghost text. The popover's CSS
529
+ // fallbacks (#333 ink, #6c757d muted, #ced4da border) read correctly
530
+ // on the white card across every shipped theme. We do carry:
531
+ // - --dft-accent-color so Apply, range tint, and focus rings track
532
+ // the theme;
533
+ // - --dft-popover-rail-bg so the preset rail tracks the theme's
534
+ // subtle-surface step (cream on editorial-cream, gray-50 on the
535
+ // grays-scaffold themes).
536
+ var THEME_PROPS = ['--dft-accent-color', '--dft-popover-rail-bg'];
537
+ function snapshotThemePropsFromVariables() {
538
+ var src = popoverOriginalParent.closest('.dft-variables');
539
+ if (!src) return;
540
+ THEME_PROPS.forEach(function(p) {
541
+ var v = src.style.getPropertyValue(p);
542
+ if (v) popover.style.setProperty(p, v);
543
+ });
544
+ }
545
+ // Position the popover relative to the chip's viewport-coords, auto-
546
+ // flipping to a right-edge anchor when the default left-anchor would
547
+ // push the popover past the viewport's right edge.
548
+ //
549
+ // The popover stays at native pixel size regardless of the SVG's
550
+ // render scale — this is chrome, not content. Matches native browser
551
+ // dropdown / datepicker behavior; chrome doesn't zoom with the page.
552
+ // A prior QA round confirmed this stance over the alternative (apply
553
+ // transform: scale() to match the SVG scale). Don't reintroduce
554
+ // scaling without re-litigating.
555
+ function positionPopoverFixed() {
556
+ var rect = trigger.getBoundingClientRect();
557
+ popover.style.top = (rect.bottom + 6) + 'px';
558
+ var popoverWidth = popover.getBoundingClientRect().width;
559
+ var margin = 8;
560
+ if (rect.left + popoverWidth > window.innerWidth - margin) {
561
+ // Right-anchor: align popover's right edge with chip's right edge.
562
+ // Clamp to a minimum of margin so a popover wider than the
563
+ // viewport still respects the left gutter.
564
+ popover.style.left = Math.max(margin, rect.right - popoverWidth) + 'px';
565
+ } else {
566
+ popover.style.left = rect.left + 'px';
567
+ }
568
+ }
569
+ function openPopover() {
570
+ // Standalone-SVG documents (no <body>) can't be reparented to —
571
+ // skip the escape hatch and let the popover render in place.
572
+ if (document.body && popover.parentNode !== document.body) {
573
+ snapshotThemePropsFromVariables();
574
+ document.body.appendChild(popover);
575
+ popover.style.position = 'fixed';
576
+ }
577
+ popover.classList.add('dft-popover-open');
578
+ trigger.setAttribute('aria-expanded', 'true');
579
+ popover.setAttribute('aria-hidden', 'false');
580
+ // Position AFTER applying dft-popover-open: the class flips
581
+ // display from none to flex, so getBoundingClientRect inside
582
+ // positionPopoverFixed returns the popover's real width for the
583
+ // right-edge collision check.
584
+ positionPopoverFixed();
585
+ // Mark this chip as open so the prototype's lingering-popover
586
+ // intent survives an iframe re-render after commit. The new chip
587
+ // in the re-rendered iframe checks this flag on init.
588
+ try { sessionStorage.setItem('__dfChipOpen_' + varName, '1'); }
589
+ catch (e) { /* sessionStorage may be unavailable */ }
590
+ }
591
+ function closePopover() {
592
+ popover.classList.remove('dft-popover-open');
593
+ trigger.setAttribute('aria-expanded', 'false');
594
+ popover.setAttribute('aria-hidden', 'true');
595
+ if (popover.parentNode === document.body) {
596
+ popover.style.position = '';
597
+ popover.style.top = '';
598
+ popover.style.left = '';
599
+ popoverOriginalParent.appendChild(popover);
600
+ }
601
+ try { sessionStorage.removeItem('__dfChipOpen_' + varName); }
602
+ catch (e) { /* sessionStorage may be unavailable */ }
603
+ }
604
+ trigger.addEventListener('click', function(e) {
605
+ e.stopPropagation();
606
+ if (popover.classList.contains('dft-popover-open')) {
607
+ closePopover();
608
+ } else {
609
+ openPopover();
610
+ }
611
+ });
612
+ // Reposition (or close) when the page scrolls / resizes — a fixed popover
613
+ // doesn't track its anchor on its own.
614
+ window.addEventListener('scroll', function() {
615
+ if (popover.classList.contains('dft-popover-open')) positionPopoverFixed();
616
+ }, true);
617
+ window.addEventListener('resize', function() {
618
+ if (popover.classList.contains('dft-popover-open')) positionPopoverFixed();
619
+ });
620
+
621
+ // Stop clicks inside the popover from bubbling to document — prevents
622
+ // the outside-click handler from firing on calendar cell clicks that
623
+ // rebuild the DOM (the just-clicked cell would become an orphan node
624
+ // and trigger.contains(target) would return false, closing the popover).
625
+ popover.addEventListener('click', function(e) { e.stopPropagation(); });
626
+
627
+ // Clear hover preview when the cursor leaves the calendar area.
628
+ calEl.addEventListener('mouseleave', function() {
629
+ if (hoverDate) {
630
+ hoverDate = null;
631
+ paintCalendarState();
632
+ }
633
+ });
634
+
635
+ rebuildCalendar();
636
+
637
+ return {
638
+ trigger: trigger,
639
+ popover: popover,
640
+ varName: varName,
641
+ open: openPopover,
642
+ close: closePopover,
643
+ isOpen: function() { return popover.classList.contains('dft-popover-open'); },
644
+ // Restore chip state from a [startISO, endISO] pair (used by initializeFromURL).
645
+ // Silently ignores malformed ISO strings — leaves chip in placeholder state.
646
+ restoreRange: function(startISO, endISO) {
647
+ if (!startISO || !endISO) return;
648
+ var s = fromISO(startISO);
649
+ var e = fromISO(endISO);
650
+ if (!s || !e) return; // invalid ISO — leave chip in placeholder state
651
+ range = [s, e];
652
+ viewMonth = new Date(range[1].getFullYear(), range[1].getMonth(), 1);
653
+ calEl.dataset.start = startISO;
654
+ calEl.dataset.end = endISO;
655
+ setChipValue(formatRange(range[0], range[1]));
656
+ rebuildCalendar();
657
+ },
658
+ };
659
+ }
660
+
661
+ // ── Global chip registration + outside-click + Esc ──────────────────────
662
+
663
+ var _chipInstances = [];
664
+
665
+ // The controls stylesheet ships embedded in the SVG's <defs><style>, which
666
+ // browsers scope to the SVG subtree — so once the popover is reparented to
667
+ // document.body it loses every rule. Clone the embedded stylesheet content
668
+ // into document.head once so the popover renders correctly anywhere.
669
+ function _liftControlsStylesIntoHead() {
670
+ if (!document.head) return;
671
+ if (document.head.querySelector('style[data-dft-controls-lifted]')) return;
672
+ var styleEls = document.querySelectorAll('svg style');
673
+ if (!styleEls.length) return;
674
+ var css = '';
675
+ styleEls.forEach(function(s) { css += s.textContent + '\n'; });
676
+ var lifted = document.createElement('style');
677
+ lifted.setAttribute('data-dft-controls-lifted', 'true');
678
+ lifted.textContent = css;
679
+ document.head.appendChild(lifted);
680
+ }
681
+
682
+ function _initAllChips() {
683
+ // Find all chip hosts that have not been initialized yet.
684
+ var hosts = document.querySelectorAll('.dft-chip-host:not([data-chip-initialized])');
685
+ if (hosts.length) _liftControlsStylesIntoHead();
686
+ hosts.forEach(function(host) {
687
+ var calArea = host.querySelector('.dft-calendar-area');
688
+ if (!calArea) return;
689
+ var varName = calArea.getAttribute('data-variable');
690
+ if (!varName) return;
691
+ host.setAttribute('data-chip-initialized', 'true');
692
+ var inst = makeChip(host, varName);
693
+ _chipInstances.push(inst);
694
+ // If this chip was open right before an auto-commit triggered an
695
+ // iframe re-render, reopen it so the prototype's "lingering
696
+ // popover" intent survives the playground's blob-URL swap.
697
+ try {
698
+ if (sessionStorage.getItem('__dfChipOpen_' + varName) === '1') {
699
+ inst.open();
700
+ }
701
+ } catch (e) { /* sessionStorage may be unavailable */ }
702
+ });
703
+ }
704
+
705
+ // Outside-click closes all open popovers.
706
+ document.addEventListener('click', function(e) {
707
+ _chipInstances.forEach(function(inst) {
708
+ if (!inst.trigger.contains(e.target)) inst.close();
709
+ });
710
+ });
711
+
712
+ // Esc closes whichever instance is open and returns focus to its trigger.
713
+ document.addEventListener('keydown', function(e) {
714
+ if (e.key !== 'Escape') return;
715
+ _chipInstances.forEach(function(inst) {
716
+ if (inst.isOpen()) {
717
+ inst.close();
718
+ inst.trigger.focus();
719
+ }
720
+ });
721
+ });
722
+
723
+ // Opening one popover closes all others.
724
+ // Implemented by attaching a listener after _initAllChips so we have all instances.
725
+ function _wireExclusiveOpen() {
726
+ _chipInstances.forEach(function(inst, i) {
727
+ inst.trigger.addEventListener('click', function() {
728
+ _chipInstances.forEach(function(other, j) {
729
+ if (j !== i) other.close();
730
+ });
731
+ });
732
+ });
733
+ }
734
+
735
+ // ── Intercept clicks on SVG <a href="?..."> links ────────────────────────
736
+ // Blob URL iframes can't navigate to query-string URLs, so we parse the
737
+ // params ourselves and send a single postMessage with the merged variable
738
+ // snapshot instead of navigating.
739
+ document.addEventListener('click', function(event) {
740
+ var link = event.target.closest('a[href]');
741
+ if (!link) return;
742
+ if (!link.ownerSVGElement && link.namespaceURI !== 'http://www.w3.org/2000/svg') return;
743
+ var href = link.getAttribute('href');
744
+ if (!href || href.charAt(0) !== '?') return;
745
+ var inIframe = window.parent !== window;
746
+ var hasHook = typeof window.__dfHandleVariableUpdate === 'function';
747
+ var params = new URLSearchParams(href.slice(1));
748
+ if (!params.toString()) {
749
+ if (inIframe || hasHook) event.preventDefault();
750
+ return;
751
+ }
752
+
753
+ if (!inIframe && !hasHook) {
754
+ // Standalone dft serve: let the browser navigate, but save scroll
755
+ // first so restoreScrollIfSaved() can recover position after reload.
756
+ try {
757
+ sessionStorage.setItem('__dfScrollY_' + window.location.pathname, String(window.scrollY));
758
+ } catch (e) { /* sessionStorage may be unavailable */ }
759
+ return; // browser follows the link naturally
760
+ }
761
+
762
+ event.preventDefault();
763
+ var vars = getAllVariableValues();
764
+ var tabUrl = inIframe ? null : new URL(window.location);
765
+ var chartsToCleanup = [];
766
+ params.forEach(function(value, name) {
767
+ var charts = markDependentChartsLoading(name);
768
+ for (var i = 0; i < charts.length; i++) {
769
+ chartsToCleanup.push(charts[i]);
770
+ }
771
+ vars[name] = value;
772
+ if (tabUrl) { tabUrl.searchParams.set(name, value); }
773
+ });
774
+ scheduleLoadingCleanup(chartsToCleanup);
775
+ if (inIframe) {
776
+ window.parent.postMessage({
777
+ type: 'dft-variable-change',
778
+ variables: vars
779
+ }, '*');
780
+ } else {
781
+ window.__dfHandleVariableUpdate(tabUrl);
782
+ }
783
+ });
784
+
785
+ // Collect all variable values from form controls
786
+ function getAllVariableValues() {
787
+ var allVars = {};
788
+ var controls = document.querySelectorAll('[data-variable]');
789
+ for (var i = 0; i < controls.length; i++) {
790
+ var control = controls[i];
791
+ var name = control.getAttribute('data-variable');
792
+
793
+ // Date range chip — read committed ISO values from data-start/end attrs.
794
+ if (control.classList.contains('dft-calendar-area')) {
795
+ if (!Object.prototype.hasOwnProperty.call(allVars, name)) {
796
+ var s = control.dataset.start || '';
797
+ var en = control.dataset.end || '';
798
+ allVars[name] = [s, en];
799
+ }
800
+ continue;
801
+ }
802
+
803
+ // Skip chip trigger button — value is read via dft-calendar-area above.
804
+ if (control.classList.contains('dft-chip')) continue;
805
+
806
+ // Regular controls
807
+ if (control.type === 'checkbox') {
808
+ allVars[name] = control.checked;
809
+ } else if (control.type === 'range') {
810
+ allVars[name] = parseFloat(control.value);
811
+ } else if (control.type === 'number') {
812
+ allVars[name] = control.value ? parseFloat(control.value) : null;
813
+ } else {
814
+ allVars[name] = control.value;
815
+ }
816
+ }
817
+ return allVars;
818
+ }
819
+
820
+ // Restore scroll position saved before a standalone page reload (dft serve).
821
+ function restoreScrollIfSaved() {
822
+ try {
823
+ var key = '__dfScrollY_' + window.location.pathname;
824
+ var saved = sessionStorage.getItem(key);
825
+ if (saved !== null) {
826
+ sessionStorage.removeItem(key);
827
+ window.scrollTo(0, parseInt(saved, 10));
828
+ }
829
+ } catch (e) { /* sessionStorage may be unavailable */ }
830
+ }
831
+
832
+ // Initialize controls from URL params on load
833
+ // Wrapped in DOMContentLoaded check for dynamic SVG insertion scenarios
834
+ function initializeFromURL() {
835
+ var params = new URLSearchParams(window.location.search);
836
+ var entries = params.entries();
837
+ var entry = entries.next();
838
+ while (!entry.done) {
839
+ var name = entry.value[0];
840
+ var value = entry.value[1];
841
+
842
+ // Find chip instance for this variable name (daterange).
843
+ var chipInst = null;
844
+ for (var ci = 0; ci < _chipInstances.length; ci++) {
845
+ if (_chipInstances[ci].varName === name) {
846
+ chipInst = _chipInstances[ci];
847
+ break;
848
+ }
849
+ }
850
+
851
+ if (chipInst) {
852
+ try {
853
+ var rangeValue = JSON.parse(value);
854
+ if (Array.isArray(rangeValue) && rangeValue.length >= 2) {
855
+ chipInst.restoreRange(rangeValue[0], rangeValue[1]);
856
+ }
857
+ } catch (e) {
858
+ // Malformed URL param — ignore; leave chip in placeholder state.
859
+ }
860
+ } else {
861
+ // Regular control
862
+ var control = document.querySelector('[data-variable="' + name + '"]:not(.dft-chip):not(.dft-calendar-area)');
863
+ if (control) {
864
+ if (control.type === 'checkbox') {
865
+ control.checked = value === 'true' || value === '1';
866
+ } else if (control.type === 'range') {
867
+ control.value = value;
868
+ var valueSpan = control.nextElementSibling;
869
+ if (valueSpan && valueSpan.classList.contains('dft-variable-slider-value')) {
870
+ valueSpan.textContent = value;
871
+ }
872
+ } else {
873
+ control.value = value;
874
+ }
875
+ }
876
+ }
877
+ entry = entries.next();
878
+ }
879
+ }
880
+
881
+ // Run initialization when DOM is ready
882
+ if (document.readyState === 'loading') {
883
+ document.addEventListener('DOMContentLoaded', function() {
884
+ _initAllChips();
885
+ _wireExclusiveOpen();
886
+ initializeFromURL();
887
+ restoreScrollIfSaved();
888
+ });
889
+ } else {
890
+ _initAllChips();
891
+ _wireExclusiveOpen();
892
+ initializeFromURL();
893
+ restoreScrollIfSaved();
894
+ }
895
+
896
+ // Setup variable hover highlighting
897
+ function setupVariableHoverHighlighting() {
898
+ var controls = document.querySelectorAll('.variable-control');
899
+ for (var i = 0; i < controls.length; i++) {
900
+ var control = controls[i];
901
+ var input = control.querySelector('[data-variable]');
902
+ if (!input) continue;
903
+
904
+ var varName = input.getAttribute('data-variable');
905
+ if (!varName) continue;
906
+
907
+ control.addEventListener('mouseenter', function(vName) {
908
+ return function() {
909
+ var charts = document.querySelectorAll('[data-var-' + vName + ']');
910
+ for (var j = 0; j < charts.length; j++) {
911
+ var chart = charts[j];
912
+
913
+ // Use transparent overlay for highlighting (cleaner than offset rects)
914
+ var existingOverlay = chart.querySelector('.dft-chart-highlight-overlay');
915
+ if (!existingOverlay) {
916
+ try {
917
+ // Use allocated dimensions from data attributes (set by renderer)
918
+ var chartWidth = parseFloat(chart.getAttribute('data-chart-width')) || 0;
919
+ var chartHeight = parseFloat(chart.getAttribute('data-chart-height')) || 0;
920
+
921
+ // Fallback to getBBox if data attributes not available
922
+ if (chartWidth === 0 || chartHeight === 0) {
923
+ var bbox = chart.getBBox ? chart.getBBox() : null;
924
+ if (bbox) {
925
+ chartWidth = bbox.width;
926
+ chartHeight = bbox.height;
927
+ }
928
+ }
929
+
930
+ if (chartWidth > 0 && chartHeight > 0) {
931
+ // Create overlay rect that covers entire chart area
932
+ var overlay = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
933
+ overlay.setAttribute('class', 'dft-chart-highlight-overlay');
934
+ overlay.setAttribute('x', '0');
935
+ overlay.setAttribute('y', '0');
936
+ overlay.setAttribute('width', chartWidth);
937
+ overlay.setAttribute('height', chartHeight);
938
+ overlay.setAttribute('rx', '4'); // Slight rounding
939
+ // Insert at end so it's on top (but pointer-events: none allows interaction)
940
+ chart.appendChild(overlay);
941
+ }
942
+ } catch (e) {
943
+ // Dimension calculation may fail, ignore silently
944
+ }
945
+ }
946
+ }
947
+ };
948
+ }(varName));
949
+
950
+ control.addEventListener('mouseleave', function(vName) {
951
+ return function() {
952
+ var charts = document.querySelectorAll('[data-var-' + vName + ']');
953
+ for (var j = 0; j < charts.length; j++) {
954
+ var chart = charts[j];
955
+
956
+ // Remove highlight overlay
957
+ var highlightOverlay = chart.querySelector('.dft-chart-highlight-overlay');
958
+ if (highlightOverlay) {
959
+ highlightOverlay.remove();
960
+ }
961
+ }
962
+ };
963
+ }(varName));
964
+ }
965
+ }
966
+
967
+ // Initialize interactions when DOM is ready
968
+ // Note: Chart menus are handled by Suite's JavaScript (init.js)
969
+ if (document.readyState === 'loading') {
970
+ document.addEventListener('DOMContentLoaded', function() {
971
+ setupVariableHoverHighlighting();
972
+ });
973
+ } else {
974
+ setupVariableHoverHighlighting();
975
+ }
976
+ })();