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,809 @@
1
+ """Predicate-based time-unit detection and smart-default labelExpr for temporal axes.
2
+
3
+ detect_time_unit: pure function; given distinct non-null x-field values,
4
+ returns the VL timeUnit string or None (continuous/sub-daily).
5
+
6
+ default_label_expr_for: returns a Vega expression for an
7
+ encoding time unit + label time unit pair.
8
+
9
+ Bucket string vocabulary (pattern → VL timeUnit):
10
+ YYYY-MM → yearmonth (2024-01)
11
+ Mon YYYY → yearmonth (Jan 2024)
12
+ MM/YYYY → yearmonth (01/2024, US-only)
13
+ YYYY-Qn → yearquarter (2024-Q1, canonical ISO quarter)
14
+ Qn YYYY → yearquarter (Q1 2024)
15
+ YYYYQn → yearquarter (2024Q1)
16
+ FYnnnn → year (FY2024 → Jan 1)
17
+ MM/DD/YYYY → yearmonthdate (01/15/2024, US-only)
18
+ Mon DD[,] YYYY → yearmonthdate (Jan 15, 2024)
19
+ YYYY-Www → yearweek (2024-W01, canonical ISO week)
20
+ W[eek ]N YYYY → yearweek (W32 2024, Week 32 2024)
21
+ Half-year (H1 YYYY, YYYY-H1): not supported; treated as unparseable.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import datetime as dt
27
+ import itertools
28
+ import re
29
+ from typing import Any
30
+
31
+ # ISO date: "2024-01-15"
32
+ _ISO_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
33
+ # ISO datetime with T separator ("2024-01-15T14:30:00") or space from DB driver str()
34
+ _ISO_DATETIME_RE = re.compile(r"^\d{4}-\d{2}-\d{2}[T ]")
35
+ # ISO week-year: "2024-W32" (weeks 01–53)
36
+ _ISO_WEEK_RE = re.compile(r"^\d{4}-W(0[1-9]|[1-4]\d|5[0-3])$")
37
+ # Calendar quarter: "2024-Q3"
38
+ _ISO_QUARTER_RE = re.compile(r"^\d{4}-Q[1-4]$")
39
+
40
+ # ── Bucket-string patterns ──────────────────────────────────────────────────
41
+ # YYYY-MM: 2024-01 (valid month 01-12)
42
+ _YEARMONTH_STR_RE = re.compile(r"^\d{4}-(0[1-9]|1[0-2])$")
43
+ # Mon YYYY: Jan 2024
44
+ _MON_YYYY_RE = re.compile(
45
+ r"^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d{4})$", re.IGNORECASE
46
+ )
47
+ # MM/YYYY: 01/2024 (US month/year)
48
+ _MM_YYYY_RE = re.compile(r"^(0[1-9]|1[0-2])/(\d{4})$")
49
+ # Qn YYYY: Q1 2024 (space optional)
50
+ _Q_YYYY_RE = re.compile(r"^Q([1-4])\s*(\d{4})$", re.IGNORECASE)
51
+ # YYYYQn: 2024Q1
52
+ _YYYY_Q_RE = re.compile(r"^(\d{4})Q([1-4])$", re.IGNORECASE)
53
+ # FYnnnn: FY2024
54
+ _FY_RE = re.compile(r"^FY(\d{4})$", re.IGNORECASE)
55
+ # MM/DD/YYYY: 01/15/2024 (US month/day/year)
56
+ _MM_DD_YYYY_RE = re.compile(r"^(0[1-9]|1[0-2])/(0[1-9]|[12]\d|3[01])/(\d{4})$")
57
+ # Mon DD[,] YYYY: Jan 15, 2024 or Jan 15 2024
58
+ _MON_DD_YYYY_RE = re.compile(
59
+ r"^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d{1,2}),?\s+(\d{4})$",
60
+ re.IGNORECASE,
61
+ )
62
+ # W[eek ]N YYYY: W32 2024, Week 32 2024
63
+ _WEEK_SPELLED_RE = re.compile(r"^W(?:eek\s*)?(\d{1,2})\s+(\d{4})$", re.IGNORECASE)
64
+
65
+ _MONTH_ABBR: dict[str, int] = {
66
+ "jan": 1,
67
+ "feb": 2,
68
+ "mar": 3,
69
+ "apr": 4,
70
+ "may": 5,
71
+ "jun": 6,
72
+ "jul": 7,
73
+ "aug": 8,
74
+ "sep": 9,
75
+ "oct": 10,
76
+ "nov": 11,
77
+ "dec": 12,
78
+ }
79
+ _QUARTER_FIRST_MONTH_LOOKUP: dict[int, int] = {1: 1, 2: 4, 3: 7, 4: 10}
80
+
81
+ # Ordered family list: (name, regex). First match wins in _classify_bucket.
82
+ _BUCKET_FAMILIES: list[tuple[str, re.Pattern[str]]] = [
83
+ ("iso_week", _ISO_WEEK_RE),
84
+ ("iso_quarter", _ISO_QUARTER_RE),
85
+ ("yearmonth_str", _YEARMONTH_STR_RE),
86
+ ("mon_yyyy", _MON_YYYY_RE),
87
+ ("mm_yyyy", _MM_YYYY_RE),
88
+ ("q_yyyy", _Q_YYYY_RE),
89
+ ("yyyy_q", _YYYY_Q_RE),
90
+ ("fy", _FY_RE),
91
+ ("mm_dd_yyyy", _MM_DD_YYYY_RE),
92
+ ("mon_dd_yyyy", _MON_DD_YYYY_RE),
93
+ ("week_spelled", _WEEK_SPELLED_RE),
94
+ ]
95
+
96
+
97
+ def _classify_bucket(v: str) -> str | None:
98
+ """Return the format-family name for a bucket string, or None."""
99
+ for name, pattern in _BUCKET_FAMILIES:
100
+ if pattern.match(v):
101
+ return name
102
+ return None
103
+
104
+
105
+ def _parse_bucket_string(value: str) -> dt.date | None:
106
+ """Return the anchor date (first instant of bucket) for a labeled format.
107
+
108
+ Returns None for unrecognized strings. Does NOT raise — invalid ISO weeks
109
+ that pass the regex (e.g. W53 in a 52-week year) return None here; the
110
+ error is raised only by the explicit ``_week_to_iso`` converter used in
111
+ ``normalize_labeled_temporal``.
112
+ """
113
+ if _YEARMONTH_STR_RE.match(value):
114
+ return dt.date(int(value[:4]), int(value[5:7]), 1)
115
+ m = _MON_YYYY_RE.match(value)
116
+ if m:
117
+ month = _MONTH_ABBR[m.group(1).lower()]
118
+ return dt.date(int(m.group(2)), month, 1)
119
+ m = _MM_YYYY_RE.match(value)
120
+ if m:
121
+ return dt.date(int(m.group(2)), int(m.group(1)), 1)
122
+ m = _Q_YYYY_RE.match(value)
123
+ if m:
124
+ return dt.date(int(m.group(2)), _QUARTER_FIRST_MONTH_LOOKUP[int(m.group(1))], 1)
125
+ m = _YYYY_Q_RE.match(value)
126
+ if m:
127
+ return dt.date(int(m.group(1)), _QUARTER_FIRST_MONTH_LOOKUP[int(m.group(2))], 1)
128
+ m = _FY_RE.match(value)
129
+ if m:
130
+ return dt.date(int(m.group(1)), 1, 1)
131
+ m = _MM_DD_YYYY_RE.match(value)
132
+ if m:
133
+ try:
134
+ return dt.date(int(m.group(3)), int(m.group(1)), int(m.group(2)))
135
+ except ValueError:
136
+ return None
137
+ m = _MON_DD_YYYY_RE.match(value)
138
+ if m:
139
+ month = _MONTH_ABBR[m.group(1).lower()]
140
+ try:
141
+ return dt.date(int(m.group(3)), month, int(m.group(2)))
142
+ except ValueError:
143
+ return None
144
+ m = _WEEK_SPELLED_RE.match(value)
145
+ if m:
146
+ week, year = int(m.group(1)), int(m.group(2))
147
+ try:
148
+ return dt.date.fromisocalendar(year, week, 1)
149
+ except ValueError:
150
+ return None
151
+ # ISO week/quarter: delegate to existing helpers (avoid duplication);
152
+ # return None on invalid week numbers (the convert path raises separately).
153
+ if _ISO_WEEK_RE.match(value):
154
+ year, week = int(value[:4]), int(value[6:])
155
+ try:
156
+ return dt.date.fromisocalendar(year, week, 1)
157
+ except ValueError:
158
+ return None
159
+ if _ISO_QUARTER_RE.match(value):
160
+ q = int(value[6])
161
+ return dt.date(int(value[:4]), _QUARTER_FIRST_MONTH_LOOKUP[q], 1)
162
+ return None
163
+
164
+
165
+ _TIME_UNIT_TO_VL: dict[str, str] = {
166
+ "monthofyear": "month",
167
+ "dayofweek": "day",
168
+ "dayofmonth": "date",
169
+ "dayofyear": "dayofyear",
170
+ "hourofday": "hours",
171
+ }
172
+
173
+ # Calendar-bucketed units → default ordinal scale.
174
+ # Distinct from time-part units (monthofyear etc.) which stay temporal.
175
+ BUCKETED_CALENDAR_UNITS: frozenset[str] = frozenset(
176
+ {"year", "yearquarter", "yearmonth", "yearweek", "yearmonthdate"}
177
+ )
178
+
179
+
180
+ def _parse_date(value: Any) -> dt.date | dt.datetime | None:
181
+ """Parse value to date/datetime. Returns None if unparseable."""
182
+ if isinstance(value, dt.datetime):
183
+ return value
184
+ if isinstance(value, dt.date):
185
+ return value
186
+ if isinstance(value, str):
187
+ if _ISO_DATETIME_RE.match(value):
188
+ try:
189
+ # Python <3.11: fromisoformat doesn't accept the trailing 'Z' UTC suffix.
190
+ v = value[:-1] + "+00:00" if value.endswith("Z") else value
191
+ return dt.datetime.fromisoformat(v)
192
+ except ValueError:
193
+ return None
194
+ if _ISO_DATE_RE.match(value):
195
+ try:
196
+ return dt.date.fromisoformat(value)
197
+ except ValueError:
198
+ return None
199
+ return _parse_bucket_string(value)
200
+ return None
201
+
202
+
203
+ def detect_time_unit(values: list[Any]) -> str | None:
204
+ """Detect VL timeUnit from distinct non-null x-field values.
205
+
206
+ Returns one of: "year", "yearquarter", "yearmonth", "yearweek",
207
+ "yearmonthdate", or None (sub-daily continuous or insufficient data).
208
+
209
+ Raises ValueError when ≥10% of distinct values are unparseable strings.
210
+ """
211
+ distinct = list({v for v in values if v is not None})
212
+ if len(distinct) < 2:
213
+ return None
214
+
215
+ parsed: list[dt.date | dt.datetime] = []
216
+ bad: list[Any] = []
217
+ for v in distinct:
218
+ p = _parse_date(v)
219
+ if p is None:
220
+ bad.append(v)
221
+ else:
222
+ parsed.append(p)
223
+
224
+ if bad and len(bad) / len(distinct) >= 0.1:
225
+ examples = bad[:5]
226
+ raise ValueError(
227
+ f"Couldn't auto-detect timeUnit: ≥10% unparseable date values: {examples}. "
228
+ "Set style.axis_x.time_unit explicitly or fix the query."
229
+ )
230
+
231
+ if not parsed or len(parsed) < 2:
232
+ return None
233
+
234
+ # Sub-daily fallthrough: any nonzero hms → continuous
235
+ for p in parsed:
236
+ if isinstance(p, dt.datetime) and (p.hour or p.minute or p.second):
237
+ return None
238
+
239
+ # Normalize to date for predicate checks
240
+ dates = [p.date() if isinstance(p, dt.datetime) else p for p in parsed]
241
+
242
+ # Predicate check: coarsest to finest
243
+ if all(d.month == 1 and d.day == 1 for d in dates):
244
+ return "year"
245
+ if all(d.day == 1 and d.month in (1, 4, 7, 10) for d in dates):
246
+ return "yearquarter"
247
+ if all(d.day == 1 for d in dates):
248
+ return "yearmonth"
249
+ # Weekly cadence: every distinct value falls on the same weekday. Pinning
250
+ # to Monday-only (ISO week-start) missed the common case of Sunday-
251
+ # aligned weekly data (and Sat / mid-week pay-period reports), all of
252
+ # which want yearmonth-style x-axis labels rather than per-day "7 Jan,
253
+ # 14 Jan, ..." chatter. Same-weekday already implies every pairwise gap
254
+ # is a multiple of 7 (calendar arithmetic), so no separate gap check.
255
+ if len({d.weekday() for d in dates}) == 1:
256
+ return "yearweek"
257
+ return "yearmonthdate"
258
+
259
+
260
+ _MIXED_LABEL_MSG = (
261
+ "Couldn't auto-detect timeUnit: column contains mixed label and "
262
+ "non-label values. "
263
+ "Set style.axis_x.time_unit explicitly."
264
+ )
265
+
266
+
267
+ def _week_to_iso(v: str) -> str:
268
+ year, week = int(v[:4]), int(v[6:])
269
+ try:
270
+ return dt.date.fromisocalendar(year, week, 1).isoformat()
271
+ except ValueError as exc:
272
+ raise ValueError(
273
+ f"Couldn't auto-detect timeUnit: '{v}' is not a valid ISO week "
274
+ f"(week {week} does not exist in year {year}). "
275
+ "Set style.axis_x.time_unit explicitly or fix the query."
276
+ ) from exc
277
+
278
+
279
+ def _quarter_to_iso(v: str) -> str:
280
+ return dt.date(int(v[:4]), _QUARTER_FIRST_MONTH_LOOKUP[int(v[6])], 1).isoformat()
281
+
282
+
283
+ def _bucket_to_iso(v: str) -> str:
284
+ """Convert any recognized bucket string to an ISO date string."""
285
+ if _ISO_WEEK_RE.match(v):
286
+ return _week_to_iso(v)
287
+ if _ISO_QUARTER_RE.match(v):
288
+ return _quarter_to_iso(v)
289
+ anchor = _parse_bucket_string(v)
290
+ if anchor is None:
291
+ raise ValueError(
292
+ f"Couldn't auto-detect timeUnit: unrecognized bucket format '{v}'. "
293
+ "Set style.axis_x.time_unit explicitly or fix the query."
294
+ )
295
+ return anchor.isoformat()
296
+
297
+
298
+ def normalize_labeled_temporal(
299
+ data: list[dict[str, Any]], field: str
300
+ ) -> list[dict[str, Any]]:
301
+ """Convert labeled bucket strings in *field* to ISO dates.
302
+
303
+ Supported formats: YYYY-Www, YYYY-Qn, Qn YYYY, YYYYQn, YYYY-MM,
304
+ Mon YYYY, MM/YYYY, FYnnnn, MM/DD/YYYY, Mon DD YYYY, W[eek ]N YYYY.
305
+
306
+ When all non-null values share the same format family, returns a copy
307
+ with values replaced by the ISO date for the first instant of each
308
+ bucket. Returns *data* unchanged when the field uses plain ISO dates
309
+ or date/datetime objects (no labeling needed).
310
+
311
+ Raises ValueError when:
312
+ - labeled values are mixed with ISO date strings (or other non-labeled)
313
+ - values span more than one format family (e.g. YYYY-Www with YYYY-Qn)
314
+ - an ISO week label encodes an invalid week number
315
+ """
316
+ values = [row[field] for row in data if field in row and row[field] is not None]
317
+ if not values:
318
+ return data
319
+
320
+ def _is_iso(v: Any) -> bool:
321
+ return isinstance(v, str) and bool(
322
+ _ISO_DATE_RE.match(v) or _ISO_DATETIME_RE.match(v)
323
+ )
324
+
325
+ def _is_labeled(v: Any) -> bool:
326
+ return isinstance(v, str) and not _is_iso(v) and _classify_bucket(v) is not None
327
+
328
+ labeled = [v for v in values if _is_labeled(v)]
329
+ if not labeled:
330
+ return data
331
+
332
+ # Any ISO dates mixed in → mixed label error
333
+ if any(_is_iso(v) for v in values):
334
+ raise ValueError(_MIXED_LABEL_MSG)
335
+
336
+ # Any unrecognized strings mixed in → mixed label error
337
+ if len(labeled) < len(values):
338
+ raise ValueError(_MIXED_LABEL_MSG)
339
+
340
+ # All are labeled — check they're all the same format family
341
+ families = {_classify_bucket(v) for v in labeled}
342
+ if len(families) > 1:
343
+ raise ValueError(_MIXED_LABEL_MSG)
344
+
345
+ return [
346
+ (
347
+ {**row, field: _bucket_to_iso(row[field])}
348
+ if field in row and row[field] is not None
349
+ else row
350
+ )
351
+ for row in data
352
+ ]
353
+
354
+
355
+ def ordinal_axis_values(
356
+ data: list[dict[str, Any]],
357
+ field: str,
358
+ time_unit: str,
359
+ label_time_unit: str | None,
360
+ ) -> list[Any] | None:
361
+ """Return sorted distinct x-values filtered to the label cadence.
362
+
363
+ For ordinal bucketed-time axes, returns the sorted distinct values from
364
+ data. When label_time_unit is coarser than encoding time_unit, filters
365
+ to values that open a new label period (e.g., first week of each month
366
+ for yearweek encoding with yearmonth labels).
367
+
368
+ Returns None when the field has no non-null values.
369
+ """
370
+ values = sorted(
371
+ {row[field] for row in data if field in row and row[field] is not None}
372
+ )
373
+ if not values:
374
+ return None
375
+
376
+ # axis.values rides directly into the VL spec; stringify date/datetime so
377
+ # vl_convert always receives JSON-safe values regardless of whether the
378
+ # caller (e.g. the per-layer-datasets path) normalized rows beforehand.
379
+ values = [
380
+ v.isoformat() if isinstance(v, (dt.date, dt.datetime)) else v for v in values
381
+ ]
382
+
383
+ if not label_time_unit or label_time_unit in ("auto", "none"):
384
+ return values
385
+
386
+ # Filter to label-period openers only.
387
+ # Values at this point are ISO date strings (from normalize_data_types
388
+ # or normalize_labeled_temporal) so we parse them to dates.
389
+ filtered: list[Any] = []
390
+ for v in values:
391
+ d = _parse_date(v)
392
+ if d is None:
393
+ # Non-parseable string → include (conservative)
394
+ filtered.append(v)
395
+ continue
396
+ date = d.date() if isinstance(d, dt.datetime) else d
397
+ if _is_label_opener(date, time_unit, label_time_unit):
398
+ filtered.append(v)
399
+ return filtered if filtered else values
400
+
401
+
402
+ def _next_bucket(date: dt.date, time_unit: str) -> dt.date:
403
+ """Return the first date of the next bucket at the given grain."""
404
+ if time_unit == "yearmonthdate":
405
+ return date + dt.timedelta(days=1)
406
+ if time_unit == "yearweek":
407
+ return date + dt.timedelta(weeks=1)
408
+ if time_unit == "yearmonth":
409
+ # Advance to first of the next month
410
+ if date.month == 12:
411
+ return dt.date(date.year + 1, 1, 1)
412
+ return dt.date(date.year, date.month + 1, 1)
413
+ if time_unit == "yearquarter":
414
+ # Advance by 3 months
415
+ new_month = date.month + 3
416
+ if new_month > 12:
417
+ return dt.date(date.year + 1, new_month - 12, 1)
418
+ return dt.date(date.year, new_month, 1)
419
+ if time_unit == "year":
420
+ return dt.date(date.year + 1, 1, 1)
421
+ raise ValueError(f"Unsupported time_unit for bucket stepping: {time_unit!r}")
422
+
423
+
424
+ def _enumerate_buckets(
425
+ min_date: dt.date, max_date: dt.date, time_unit: str
426
+ ) -> list[dt.date]:
427
+ """Return every bucket date from min_date to max_date inclusive."""
428
+ buckets: list[dt.date] = []
429
+ current = min_date
430
+ while current <= max_date:
431
+ buckets.append(current)
432
+ current = _next_bucket(current, time_unit)
433
+ return buckets
434
+
435
+
436
+ _GAP_FILL_HANDLINGS = frozenset(
437
+ {
438
+ "linear",
439
+ "step-after",
440
+ "step-before",
441
+ "step-center",
442
+ "curve",
443
+ }
444
+ )
445
+
446
+
447
+ def _smoothstep(t: float) -> float:
448
+ """Hermite ease: 0 at t=0, 1 at t=1, flat derivatives at endpoints."""
449
+ return t * t * (3.0 - 2.0 * t)
450
+
451
+
452
+ def _apply_gap_fill_handling(
453
+ rows: list[dict[str, Any]],
454
+ x_field: str,
455
+ dim_fields: list[str],
456
+ dim_combos: list[tuple[Any, ...]],
457
+ measure_cols: list[str],
458
+ mode: str,
459
+ ) -> None:
460
+ """Fill null measures on synthetic buckets; mutates ``rows`` in place."""
461
+ key_to_row: dict[tuple[Any, ...], dict[str, Any]] = {}
462
+ bucket_order: dict[str, int] = {}
463
+ for row in rows:
464
+ bkt = row[x_field]
465
+ if bkt not in bucket_order:
466
+ bucket_order[bkt] = len(bucket_order)
467
+ combo = tuple(row.get(d) for d in dim_fields)
468
+ key_to_row[(bkt, *combo)] = row
469
+
470
+ all_buckets_sorted = sorted(bucket_order, key=lambda b: bucket_order[b])
471
+
472
+ for combo in dim_combos:
473
+ group_rows: list[dict[str, Any]] = []
474
+ for bkt in all_buckets_sorted:
475
+ key = (bkt, *combo)
476
+ if key in key_to_row:
477
+ group_rows.append(key_to_row[key])
478
+
479
+ for col in measure_cols:
480
+ n = len(group_rows)
481
+ # Only observed (query) rows are anchors — not values filled in this pass.
482
+ # Any non-null value is a valid anchor; arithmetic below raises loudly if
483
+ # the value is non-numeric (e.g. a string) rather than silently skipping it.
484
+ anchors = [idx for idx in range(n) if group_rows[idx][col] is not None]
485
+ for i, row in enumerate(group_rows):
486
+ if row[col] is not None:
487
+ continue
488
+ left_anchors = [a for a in anchors if a < i]
489
+ right_anchors = [a for a in anchors if a > i]
490
+ left_idx = left_anchors[-1] if left_anchors else -1
491
+ right_idx = right_anchors[0] if right_anchors else -1
492
+
493
+ if mode == "step-after":
494
+ if left_idx >= 0:
495
+ row[col] = group_rows[left_idx][col]
496
+ continue
497
+
498
+ if mode == "step-before":
499
+ if right_idx >= 0:
500
+ row[col] = group_rows[right_idx][col]
501
+ continue
502
+
503
+ if left_idx < 0 or right_idx < 0:
504
+ continue
505
+
506
+ left_val = group_rows[left_idx][col]
507
+ right_val = group_rows[right_idx][col]
508
+ gap_width = right_idx - left_idx
509
+ gap_pos = i - left_idx
510
+ t = gap_pos / gap_width
511
+
512
+ if mode == "linear":
513
+ row[col] = left_val + (right_val - left_val) * t
514
+ elif mode == "step-center":
515
+ mid = left_idx + gap_width / 2
516
+ row[col] = left_val if i < mid else right_val
517
+ elif mode == "curve":
518
+ row[col] = left_val + (right_val - left_val) * _smoothstep(t)
519
+
520
+
521
+ def complete_ordinal_time_series(
522
+ data: list[dict[str, Any]],
523
+ x_field: str,
524
+ time_unit: str,
525
+ dim_fields: list[str],
526
+ fill: str,
527
+ ) -> list[dict[str, Any]]:
528
+ """Synthesize missing time-bucket rows so every bucket in [min, max] is present.
529
+
530
+ For ordinal bucketed-time charts the engine must supply every bucket
531
+ between the dataset's min and max so the ordinal x-axis has a slot for
532
+ each period. Without this, missing buckets simply disappear from the axis.
533
+
534
+ Args:
535
+ data: rows from the query (non-empty; caller must guard empty datasets).
536
+ x_field: the x-encoding column name (must be ISO date strings or
537
+ datetime.date objects after normalize_labeled_temporal runs).
538
+ time_unit: one of BUCKETED_CALENDAR_UNITS (year, yearquarter, yearmonth,
539
+ yearweek, yearmonthdate).
540
+ dim_fields: categorical dimension columns to cross-join over (e.g. the
541
+ color/series field). Engine only cross-joins over values actually
542
+ present in the data window.
543
+ fill: "null" fills missing measure columns with None;
544
+ "zero" fills with 0; interpolate-* modes fill interior synthetic
545
+ buckets (see ``_GAP_FILL_HANDLINGS``).
546
+
547
+ Returns:
548
+ A new list of dicts, sorted (bucket asc, dim_1 asc, …), with the
549
+ original rows merged in. When no buckets are missing, returns data
550
+ sorted by the same key. If data is empty, returns data unchanged.
551
+ """
552
+ if not data:
553
+ return data
554
+
555
+ # Collect all x values; parse to date for comparison
556
+ raw_x_values = [
557
+ row[x_field] for row in data if x_field in row and row[x_field] is not None
558
+ ]
559
+ if not raw_x_values:
560
+ return data
561
+
562
+ # Convert to ISO strings for uniform comparison (ordinal path uses strings)
563
+ def _to_iso(v: Any) -> str:
564
+ if isinstance(v, dt.datetime):
565
+ return v.date().isoformat()
566
+ if isinstance(v, dt.date):
567
+ return v.isoformat()
568
+ return str(v)
569
+
570
+ x_iso = [_to_iso(v) for v in raw_x_values]
571
+
572
+ # Parse to dates for arithmetic
573
+ parsed_dates: list[dt.date] = []
574
+ for iso in x_iso:
575
+ d = _parse_date(iso)
576
+ if d is not None:
577
+ parsed_dates.append(d.date() if isinstance(d, dt.datetime) else d)
578
+
579
+ if not parsed_dates:
580
+ return data
581
+
582
+ min_date = min(parsed_dates)
583
+ max_date = max(parsed_dates)
584
+ all_buckets = _enumerate_buckets(min_date, max_date, time_unit)
585
+
586
+ # Determine distinct dimension values from actual data
587
+ dim_values: list[list[Any]] = []
588
+ for dim in dim_fields:
589
+ seen: list[Any] = []
590
+ seen_set: set[Any] = set()
591
+ for row in data:
592
+ v = row.get(dim)
593
+ if v not in seen_set:
594
+ seen_set.add(v)
595
+ seen.append(v)
596
+ dim_values.append(sorted(seen, key=lambda x: (x is None, x)))
597
+
598
+ # Identify measure columns: all non-x, non-dim columns
599
+ sample_keys = list(data[0].keys())
600
+ dim_set = set(dim_fields) | {x_field}
601
+ measure_cols = [k for k in sample_keys if k not in dim_set]
602
+
603
+ # For fill modes other than "zero", synthesized rows start as None; the
604
+ # second pass (_apply_gap_fill_handling) overwrites them for interior gaps.
605
+ fill_value = 0 if fill == "zero" else None
606
+
607
+ # Build lookup from (bucket_str, *dim_vals) → row
608
+ def _row_key(row: dict[str, Any]) -> tuple[Any, ...]:
609
+ return (_to_iso(row.get(x_field)),) + tuple(row.get(d) for d in dim_fields)
610
+
611
+ existing: dict[tuple[Any, ...], dict[str, Any]] = {
612
+ _row_key(row): row for row in data
613
+ }
614
+
615
+ # Generate full scaffold via cross-product of buckets × dim combinations
616
+ if dim_fields:
617
+ dim_combos: list[tuple[Any, ...]] = list(itertools.product(*dim_values))
618
+ else:
619
+ dim_combos = [()]
620
+
621
+ result: list[dict[str, Any]] = []
622
+ for bucket in all_buckets:
623
+ bucket_str = bucket.isoformat()
624
+ for combo in dim_combos:
625
+ key = (bucket_str,) + combo
626
+ if key in existing:
627
+ result.append(existing[key])
628
+ else:
629
+ # Synthesize a row: bucket value + dim values + filled measures
630
+ synth: dict[str, Any] = {x_field: bucket_str}
631
+ for dim, val in zip(dim_fields, combo, strict=True):
632
+ synth[dim] = val
633
+ for col in measure_cols:
634
+ synth[col] = fill_value
635
+ result.append(synth)
636
+
637
+ if fill in _GAP_FILL_HANDLINGS:
638
+ _apply_gap_fill_handling(
639
+ result, x_field, dim_fields, dim_combos, measure_cols, fill
640
+ )
641
+
642
+ return result
643
+
644
+
645
+ def _is_label_opener(date: dt.date, encoding_unit: str, label_unit: str) -> bool:
646
+ """Return True when *date* opens a new label period."""
647
+ if label_unit == "year":
648
+ if encoding_unit in {"yearweek", "yearmonthdate"}:
649
+ return date.month == 1 and date.day <= 7
650
+ return date.month == 1 and date.day == 1
651
+ if label_unit == "yearquarter":
652
+ if encoding_unit == "yearweek":
653
+ return date.month % 3 == 1 and date.day <= 7
654
+ if encoding_unit == "yearmonthdate":
655
+ return date.month % 3 == 1 and date.day == 1
656
+ return date.month % 3 == 1 and date.day == 1 # yearmonth
657
+ if label_unit == "yearmonth":
658
+ if encoding_unit == "yearweek":
659
+ return date.day <= 7
660
+ if encoding_unit == "yearmonthdate":
661
+ return date.day == 1
662
+ return True # same cadence or unknown — include all
663
+
664
+
665
+ def vl_time_unit(time_unit: str) -> str:
666
+ """Return the Vega-Lite timeUnit for a Dataface time_unit value.
667
+
668
+ Chronological grains return their UTC variant (utcyearmonth etc.) so VL
669
+ bucketing stays UTC-aligned regardless of the renderer's TZ. Time-part
670
+ units (monthofyear, dayofweek, hourofday, …) are cyclic and have no utc*
671
+ sibling — they pass through via _TIME_UNIT_TO_VL.
672
+ """
673
+ if time_unit in BUCKETED_CALENDAR_UNITS:
674
+ return f"utc{time_unit}"
675
+ return _TIME_UNIT_TO_VL.get(time_unit, time_unit)
676
+
677
+
678
+ def resolve_label_time_unit(
679
+ encoding_time_unit: str | None, authored_label_time_unit: str | None
680
+ ) -> str | None:
681
+ """Return the label cadence for a resolved encoding time_unit.
682
+
683
+ ``None``/``auto`` inherit from the encoding cadence, except weekly buckets
684
+ default to month labels so dense weekly axes read Jan/Feb/Mar instead of
685
+ W01/W02/W03. ``none`` disables Dataface's smart label expression.
686
+ """
687
+ if authored_label_time_unit == "none":
688
+ return None
689
+ if authored_label_time_unit not in (None, "auto"):
690
+ return authored_label_time_unit
691
+ if encoding_time_unit in {"yearweek", "yearmonthdate"}:
692
+ return "yearmonth"
693
+ return encoding_time_unit
694
+
695
+
696
+ def _year_label(v: str, fmt: str, _month: str, _date: str) -> str:
697
+ return f"{fmt}({v}, '%Y')"
698
+
699
+
700
+ def _month_label(v: str, fmt: str, month: str, _date: str) -> str:
701
+ return (
702
+ f"{month}({v}) === 0"
703
+ f" ? [{fmt}({v}, '%b'), {fmt}({v}, '%Y')]"
704
+ f" : {fmt}({v}, '%b')"
705
+ )
706
+
707
+
708
+ def _quarter_label(v: str, fmt: str, month: str, _date: str) -> str:
709
+ return (
710
+ f"{month}({v}) === 0"
711
+ f" ? ['Q' + (floor({month}({v})/3) + 1), {fmt}({v}, '%Y')]"
712
+ f" : 'Q' + (floor({month}({v})/3) + 1)"
713
+ )
714
+
715
+
716
+ def _week_label(v: str, fmt: str, month: str, date: str) -> str:
717
+ return (
718
+ f"{date}({v}) <= 7 && {month}({v}) === 0"
719
+ f" ? [{fmt}({v}, 'W%V'), {fmt}({v}, '%Y')]"
720
+ f" : {fmt}({v}, 'W%V')"
721
+ )
722
+
723
+
724
+ def _day_label(v: str, fmt: str, month: str, date: str) -> str:
725
+ return (
726
+ f"{date}({v}) === 1 && {month}({v}) === 0"
727
+ f" ? [{fmt}({v}, '%-d %b'), {fmt}({v}, '%Y')]"
728
+ f" : {fmt}({v}, '%-d %b')"
729
+ )
730
+
731
+
732
+ def opens_label_period(
733
+ encoding_time_unit: str,
734
+ label_time_unit: str,
735
+ v: str = "datum.value",
736
+ month: str = "month",
737
+ date: str = "date",
738
+ ) -> str | None:
739
+ if label_time_unit == "yearquarter" and encoding_time_unit in {
740
+ "yearmonth",
741
+ "yearweek",
742
+ "yearmonthdate",
743
+ }:
744
+ clauses = [f"{month}({v}) % 3 === 0"]
745
+ if encoding_time_unit == "yearweek":
746
+ clauses.append(f"{date}({v}) <= 7")
747
+ elif encoding_time_unit == "yearmonthdate":
748
+ clauses.append(f"{date}({v}) === 1")
749
+ return " && ".join(clauses)
750
+ if label_time_unit == "yearmonth" and encoding_time_unit in {
751
+ "yearweek",
752
+ "yearmonthdate",
753
+ }:
754
+ return (
755
+ f"{date}({v}) <= 7"
756
+ if encoding_time_unit == "yearweek"
757
+ else f"{date}({v}) === 1"
758
+ )
759
+ if label_time_unit == "year" and encoding_time_unit not in {"year"}:
760
+ clauses = [f"{month}({v}) === 0"]
761
+ if encoding_time_unit in {"yearweek", "yearmonthdate"}:
762
+ clauses.append(
763
+ f"{date}({v}) <= 7"
764
+ if encoding_time_unit == "yearweek"
765
+ else f"{date}({v}) === 1"
766
+ )
767
+ return " && ".join(clauses)
768
+ return None
769
+
770
+
771
+ def default_label_expr_for(
772
+ encoding_time_unit: str | None,
773
+ label_time_unit: str | None,
774
+ ) -> str | None:
775
+ """Return a smart Vega labelExpr for an encoding/label cadence pair.
776
+
777
+ The gate (``opens_label_period``) filters to label-period openers because
778
+ VL's tick cadence is geometry-driven and may overshoot the label cadence —
779
+ producing duplicate Q-labels when monthly ticks land inside a quarter.
780
+
781
+ Always emits ``toDate(datum.value)`` + ``utcFormat`` / ``utcmonth`` /
782
+ ``utcdate``. Ordinal axes are string-domain (need ``toDate`` to parse);
783
+ temporal axes use ``scale.type: "utc"`` (so component extraction must
784
+ also be UTC, otherwise local-TZ ``month()`` shifts January UTC into
785
+ December local and the cadence gate stamps every tick blank). ``toDate``
786
+ is a no-op on Date values, so a single shape covers both paths without
787
+ local-TZ drift.
788
+ """
789
+ if not encoding_time_unit or not label_time_unit:
790
+ return None
791
+ if label_time_unit in {"auto", "none"}:
792
+ return None
793
+ label_exprs = {
794
+ "year": _year_label,
795
+ "yearquarter": _quarter_label,
796
+ "yearmonth": _month_label,
797
+ "yearweek": _week_label,
798
+ "yearmonthdate": _day_label,
799
+ }
800
+ label_fn = label_exprs.get(label_time_unit)
801
+ if label_fn is None:
802
+ return None
803
+ v = "toDate(datum.value)"
804
+ fmt = "utcFormat"
805
+ month = "utcmonth"
806
+ date = "utcdate"
807
+ expr = label_fn(v, fmt, month, date)
808
+ gate = opens_label_period(encoding_time_unit, label_time_unit, v, month, date)
809
+ return f"{gate} ? ({expr}) : ''" if gate else expr