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,268 @@
1
+ """Standard chart type Vega-Lite spec generators.
2
+
3
+ Each generator receives a ``MappedChart`` from ``profile.py`` and performs
4
+ mechanical Vega-Lite spec assembly — dimensions, title, tooltips. No
5
+ Dataface/VL encoding divergence logic lives here; that belongs in profile.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from dataface.core.compile.chart_resolved import ResolvedChart
13
+ from dataface.core.render.chart.profile import MappedChart
14
+ from dataface.core.render.chart.spec_builders import (
15
+ new_chart_spec,
16
+ set_chart_dimensions,
17
+ set_chart_title,
18
+ tooltip_entry,
19
+ )
20
+ from dataface.core.render.format_utils import resolve_format
21
+ from dataface.core.render.text.case import format_display_text
22
+ from dataface.core.render.utils import normalize_data_types
23
+
24
+
25
+ def _generate_layered_spec(
26
+ mapped: MappedChart,
27
+ chart: ResolvedChart,
28
+ data: list[dict[str, Any]],
29
+ width: float | None = None,
30
+ height: float | None = None,
31
+ min_height: float | None = None,
32
+ ) -> dict[str, Any]:
33
+ """Assemble layered chart spec from the profile-mapped layered contract."""
34
+ if mapped.data_override is not None:
35
+ data = mapped.data_override
36
+ data = normalize_data_types(data)
37
+ tooltip_number_format = resolve_format(
38
+ chart.resolved_style.tooltip.format, chart.resolved_style.formats
39
+ )
40
+
41
+ has_datasets = mapped.datasets is not None
42
+
43
+ # Collect y fields: from chart.y (multi-metric) or from layers (type: layered)
44
+ y_fields = chart.y or []
45
+ if not isinstance(y_fields, list):
46
+ y_fields = [y_fields]
47
+ if not y_fields and chart.layers:
48
+ y_fields = [layer.y for layer in chart.layers if layer.y]
49
+
50
+ # When layers use different datasets, there's no shared x field for
51
+ # a combined tooltip. Fall back to per-layer tooltips.
52
+ x_field = mapped.encoding.get("x", {}).get("field") if not has_datasets else None
53
+
54
+ # Build tooltip showing all metrics at once (only for shared-data layers)
55
+ tooltip_fields: list[dict[str, Any]] = []
56
+ if x_field:
57
+ tooltip_fields.append(
58
+ tooltip_entry(
59
+ x_field,
60
+ mapped.encoding.get("x", {}).get("type", "nominal"),
61
+ title=format_display_text(
62
+ x_field, from_slug=True, font=chart.resolved_style.axis_x.title.font
63
+ ),
64
+ ),
65
+ )
66
+ if not has_datasets:
67
+ for metric in y_fields:
68
+ tooltip_fields.append(
69
+ tooltip_entry(
70
+ metric,
71
+ "quantitative",
72
+ title=format_display_text(
73
+ metric,
74
+ from_slug=True,
75
+ font=chart.resolved_style.axis_y.title.font,
76
+ ),
77
+ format=tooltip_number_format,
78
+ ),
79
+ )
80
+
81
+ # Normalize datasets values when present
82
+ normalized_datasets = None
83
+ if mapped.datasets:
84
+ normalized_datasets = {
85
+ name: normalize_data_types(rows) for name, rows in mapped.datasets.items()
86
+ }
87
+
88
+ spec = new_chart_spec(data, layer=True, datasets=normalized_datasets)
89
+ set_chart_dimensions(spec, width, height, min_height=min_height)
90
+
91
+ if mapped.encoding:
92
+ spec["encoding"] = dict(mapped.encoding)
93
+
94
+ # Prevents future-date rows in secondary layers from stretching the axis
95
+ # beyond the primary query's range (Looker merged-result visual match).
96
+ first_dataset_name: str | None = None
97
+ first_layer_x_field: str | None = None
98
+ if chart.x_domain == "primary" and mapped.layers:
99
+ if not has_datasets:
100
+ raise ValueError(
101
+ f"Chart '{chart.id}': x_domain: primary requires per-layer datasets "
102
+ "but no datasets were resolved. Ensure each layer specifies its own query."
103
+ )
104
+ first_layer = mapped.layers[0]
105
+ first_dataset_name = first_layer.data_name
106
+ if first_dataset_name is None:
107
+ raise ValueError(
108
+ f"Chart '{chart.id}': x_domain: primary requires the first layer's "
109
+ "dataset to be present, but the first layer has no data_name. "
110
+ "Ensure the first layer's query name appears in the datasets dict."
111
+ )
112
+ first_layer_x_field = first_layer.encoding.get("x", {}).get("field")
113
+ if first_layer_x_field is None:
114
+ raise ValueError(
115
+ f"Chart '{chart.id}': x_domain: primary requires an x field on "
116
+ "the first layer's encoding, but none was resolved. "
117
+ "Add `x:` to the first layer or the chart level."
118
+ )
119
+
120
+ for layer_idx, layer in enumerate(mapped.layers):
121
+ layer_encoding = dict(layer.encoding)
122
+
123
+ # Combined tooltip shows all metrics at the shared x position.
124
+ # Only used when all layers share the same dataset.
125
+ use_combined = (
126
+ not has_datasets
127
+ and chart.chart_type in ("line", "area", "layered")
128
+ and x_field
129
+ and tooltip_fields
130
+ )
131
+ if use_combined:
132
+ layer_encoding["tooltip"] = tooltip_fields
133
+
134
+ # Inject scale.domain override on layer-0's x-encoding when x_domain is
135
+ # "primary". VL resolves the shared x-scale from the first mark's domain,
136
+ # so placing the override here anchors the axis to the primary dataset's
137
+ # x-values and clips secondary layers rather than extending the axis.
138
+ if layer_idx == 0 and first_dataset_name is not None:
139
+ x_enc = dict(layer_encoding.get("x", {}))
140
+ scale = dict(x_enc.get("scale", {}))
141
+ scale["domain"] = {"data": first_dataset_name, "field": first_layer_x_field}
142
+ x_enc["scale"] = scale
143
+ layer_encoding["x"] = x_enc
144
+
145
+ layer_spec: dict[str, Any] = {
146
+ "mark": dict(layer.mark),
147
+ "encoding": layer_encoding,
148
+ }
149
+
150
+ if layer.transform:
151
+ layer_spec["transform"] = [dict(t) for t in layer.transform]
152
+
153
+ # Per-layer data reference for multi-query layered charts. Inline
154
+ # ``layer.data`` (used by the zero/top-rule layers to halt parent
155
+ # data iteration) takes precedence — a layer that ships its own
156
+ # values shouldn't also pull from a named dataset.
157
+ if layer.data is not None:
158
+ layer_spec["data"] = dict(layer.data)
159
+ elif layer.data_name is not None:
160
+ layer_spec["data"] = {"name": layer.data_name}
161
+
162
+ spec["layer"].append(layer_spec)
163
+
164
+ if mapped.derived_resolve:
165
+ spec["resolve"] = mapped.derived_resolve
166
+
167
+ set_chart_title(spec, chart.title, chart.subtitle, style=chart.resolved_style)
168
+
169
+ return spec
170
+
171
+
172
+ def _generate_histogram_spec(
173
+ mapped: MappedChart,
174
+ chart: ResolvedChart,
175
+ data: list[dict[str, Any]],
176
+ width: float | None = None,
177
+ height: float | None = None,
178
+ ) -> dict[str, Any]:
179
+ """Assemble histogram spec from profile-mapped mark and encoding."""
180
+ data = normalize_data_types(data)
181
+
182
+ spec = new_chart_spec(data, mark=mapped.mark)
183
+ set_chart_dimensions(spec, width, height, min_height=100)
184
+ spec["encoding"] = dict(mapped.encoding)
185
+
186
+ set_chart_title(spec, chart.title, chart.subtitle, style=chart.resolved_style)
187
+ return spec
188
+
189
+
190
+ def _generate_arc_spec(
191
+ mapped: MappedChart,
192
+ chart: ResolvedChart,
193
+ data: list[dict[str, Any]],
194
+ width: float | None = None,
195
+ height: float | None = None,
196
+ ) -> dict[str, Any]:
197
+ """Assemble arc/pie spec from profile-mapped mark and encoding."""
198
+ if mapped.layers:
199
+ # Donut center total (and future per-slice labels) emit arc + text
200
+ # layers; defer to the shared layered assembler. ``min_height=100``
201
+ # matches the non-layered arc path's contract (small donuts otherwise
202
+ # collapse into unreadable blobs).
203
+ return _generate_layered_spec(
204
+ mapped, chart, data, width, height, min_height=100
205
+ )
206
+
207
+ data = normalize_data_types(data)
208
+
209
+ spec = new_chart_spec(data, mark=mapped.mark)
210
+ set_chart_dimensions(spec, width, height, min_height=100)
211
+ spec["encoding"] = dict(mapped.encoding)
212
+
213
+ set_chart_title(spec, chart.title, chart.subtitle, style=chart.resolved_style)
214
+ return spec
215
+
216
+
217
+ def _generate_rect_spec(
218
+ mapped: MappedChart,
219
+ chart: ResolvedChart,
220
+ data: list[dict[str, Any]],
221
+ width: float | None = None,
222
+ height: float | None = None,
223
+ ) -> dict[str, Any]:
224
+ """Assemble rect/square/heatmap spec from profile-mapped mark and encoding."""
225
+ data = normalize_data_types(data)
226
+
227
+ spec = new_chart_spec(data, mark=mapped.mark)
228
+ set_chart_dimensions(spec, width, height, min_width=150, min_height=100)
229
+ spec["encoding"] = dict(mapped.encoding)
230
+
231
+ set_chart_title(spec, chart.title, chart.subtitle, style=chart.resolved_style)
232
+ return spec
233
+
234
+
235
+ def _generate_boxplot_spec(
236
+ mapped: MappedChart,
237
+ chart: ResolvedChart,
238
+ data: list[dict[str, Any]],
239
+ width: float | None = None,
240
+ height: float | None = None,
241
+ ) -> dict[str, Any]:
242
+ """Assemble boxplot spec from profile-mapped mark and encoding."""
243
+ data = normalize_data_types(data)
244
+
245
+ spec = new_chart_spec(data, mark=mapped.mark)
246
+ set_chart_dimensions(spec, width, height)
247
+ spec["encoding"] = dict(mapped.encoding)
248
+
249
+ set_chart_title(spec, chart.title, chart.subtitle, style=chart.resolved_style)
250
+ return spec
251
+
252
+
253
+ def _generate_error_spec(
254
+ mapped: MappedChart,
255
+ chart: ResolvedChart,
256
+ data: list[dict[str, Any]],
257
+ width: float | None = None,
258
+ height: float | None = None,
259
+ ) -> dict[str, Any]:
260
+ """Assemble errorbar/errorband spec from profile-mapped mark and encoding."""
261
+ data = normalize_data_types(data)
262
+
263
+ spec = new_chart_spec(data, mark=mapped.mark)
264
+ set_chart_dimensions(spec, width, height)
265
+ spec["encoding"] = dict(mapped.encoding)
266
+
267
+ set_chart_title(spec, chart.title, chart.subtitle, style=chart.resolved_style)
268
+ return spec
@@ -0,0 +1,346 @@
1
+ """Vega-Lite field name maps and helper for snake_case → camelCase mapping."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from pydantic import BaseModel
8
+
9
+ SCALE_FIELD_MAP: dict[str, str] = {
10
+ # ``paddingInner`` / ``paddingOuter`` are the encoding-level VL field names
11
+ # for band-scale gutter and edge padding. The ``bandPaddingInner`` /
12
+ # ``bandPaddingOuter`` variants only work at the global config level
13
+ # (``config.scale.bandPaddingInner``) — they are silently ignored when
14
+ # placed on ``encoding.{x,y}.scale``.
15
+ "band_padding_inner": "paddingInner",
16
+ "band_padding_outer": "paddingOuter",
17
+ "point_padding": "pointPadding",
18
+ "rect_band_padding_inner": "rectBandPaddingInner",
19
+ "round": "round",
20
+ "zero": "zero",
21
+ "clamp": "clamp",
22
+ "continuous_padding": "continuousPadding",
23
+ "quantile_count": "quantileCount",
24
+ "quantize_count": "quantizeCount",
25
+ "use_unaggregated_domain": "useUnaggregatedDomain",
26
+ "x_reverse": "xReverse",
27
+ "type": "type",
28
+ }
29
+
30
+ # Encoding-level-only scale fields applied to encoding.x.scale / encoding.y.scale.
31
+ SCALE_ENCODING_FIELD_MAP: dict[str, str] = {
32
+ "domain": "domain",
33
+ "nice": "nice",
34
+ # Unified shorthand. VL dispatches based on scale type (bandPaddingOuter,
35
+ # pointPadding, continuousPadding). Encoding-level only.
36
+ "padding": "padding",
37
+ }
38
+
39
+ VIEW_FIELD_MAP: dict[str, str] = {
40
+ "fill": "fill",
41
+ "stroke": "stroke",
42
+ "stroke_width": "strokeWidth",
43
+ "continuous_width": "continuousWidth",
44
+ "continuous_height": "continuousHeight",
45
+ "corner_radius": "cornerRadius",
46
+ }
47
+
48
+
49
+ from typing import Any
50
+
51
+
52
+ def _n(obj: Any, *path: str) -> Any:
53
+ """Get nested attribute, returning None if any level is None or missing."""
54
+ for attr in path:
55
+ if obj is None:
56
+ return None
57
+ obj = getattr(obj, attr, None)
58
+ return obj
59
+
60
+
61
+ def _dasharray_to_vl(dasharray: str) -> list[float]:
62
+ """Convert SVG stroke-dasharray string (e.g. '4 2') to VL strokeDash number array."""
63
+ return [float(x) for x in dasharray.split()]
64
+
65
+
66
+ def _stroke_to_vl(stroke: Any) -> dict[str, Any]:
67
+ """Map a StrokeStyle → VL stroke properties (non-None fields only).
68
+
69
+ Single canonical primitive shared by all per-mark mappers and halo
70
+ path builders so new StrokeStyle fields automatically flow everywhere.
71
+ """
72
+ d: dict[str, Any] = {}
73
+ if (v := _n(stroke, "color")) is not None:
74
+ d["stroke"] = v
75
+ if (v := _n(stroke, "width")) is not None:
76
+ d["strokeWidth"] = v
77
+ if (v := _n(stroke, "cap")) is not None:
78
+ d["strokeCap"] = v
79
+ if (v := _n(stroke, "join")) is not None:
80
+ d["strokeJoin"] = v
81
+ if (v := _n(stroke, "dasharray")) is not None:
82
+ d["strokeDash"] = _dasharray_to_vl(v)
83
+ return d
84
+
85
+
86
+ def axis_to_vl(axis: Any) -> dict[str, Any]:
87
+ """Map any axis style object → VL axis config dict (non-None fields only).
88
+
89
+ Works for AxisStyle (all fields concrete, always emitted) and
90
+ AxisStylePatch (skip None). Single canonical mapper for all axis
91
+ variants — theme global/channel/type-conditional and chart-local patches.
92
+ """
93
+ if axis is None:
94
+ return {}
95
+ d: dict[str, Any] = {}
96
+
97
+ # Label font (nested via label.font)
98
+ if (v := _n(axis, "label", "font", "color")) is not None:
99
+ d["labelColor"] = v
100
+ if (v := _n(axis, "label", "font", "family")) is not None:
101
+ d["labelFont"] = v
102
+ if (v := _n(axis, "label", "font", "size")) is not None:
103
+ d["labelFontSize"] = v
104
+ if (v := _n(axis, "label", "font", "weight")) is not None:
105
+ d["labelFontWeight"] = v
106
+ if (v := _n(axis, "label", "padding")) is not None:
107
+ d["labelPadding"] = v
108
+ if (v := _n(axis, "label", "max_width")) is not None:
109
+ d["labelLimit"] = v
110
+ if (v := _n(axis, "label", "angle")) is not None:
111
+ d["labelAngle"] = v
112
+ if (v := _n(axis, "label", "align")) is not None:
113
+ d["labelAlign"] = v
114
+ if (v := _n(axis, "label", "baseline")) is not None:
115
+ d["labelBaseline"] = v
116
+ # "smart" → omit labelOverlap (VL applies per-scale adaptive default).
117
+ # "allow" → false (no reduction; labels may overlap).
118
+ # "parity" / "greedy" → pass through as-is.
119
+ _overlap = _n(axis, "label", "overlap")
120
+ if _overlap == "allow":
121
+ d["labelOverlap"] = False
122
+ elif _overlap in ("parity", "greedy"):
123
+ d["labelOverlap"] = _overlap
124
+ if (v := _n(axis, "label", "separation")) is not None:
125
+ d["labelSeparation"] = v
126
+ if (v := _n(axis, "label", "visible")) is not None:
127
+ d["labels"] = v
128
+ if (v := _n(axis, "label", "expr")) is not None:
129
+ d["labelExpr"] = v
130
+ if (v := _n(axis, "label", "bound")) is not None:
131
+ d["labelBound"] = v
132
+ if (v := _n(axis, "label", "flush")) is not None:
133
+ d["labelFlush"] = v
134
+ if (v := _n(axis, "label", "offset")) is not None:
135
+ d["labelOffset"] = v
136
+ if (v := _n(axis, "label", "line_height")) is not None:
137
+ d["labelLineHeight"] = v
138
+ if (v := _n(axis, "label", "anchor")) is not None:
139
+ d["labelAnchor"] = v
140
+
141
+ # Title font
142
+ if (v := _n(axis, "title", "font", "color")) is not None:
143
+ d["titleColor"] = v
144
+ if (v := _n(axis, "title", "font", "family")) is not None:
145
+ d["titleFont"] = v
146
+ if (v := _n(axis, "title", "font", "size")) is not None:
147
+ d["titleFontSize"] = v
148
+ if (v := _n(axis, "title", "font", "weight")) is not None:
149
+ d["titleFontWeight"] = v
150
+ if (v := _n(axis, "title", "padding")) is not None:
151
+ d["titlePadding"] = v
152
+
153
+ # Grid
154
+ if (v := _n(axis, "grid", "visible")) is not None:
155
+ d["grid"] = v
156
+ if (v := _n(axis, "grid", "opacity")) is not None:
157
+ d["gridOpacity"] = v
158
+ if (v := _n(axis, "grid", "width")) is not None:
159
+ d["gridWidth"] = v
160
+ if (v := _n(axis, "grid", "color")) is not None:
161
+ d["gridColor"] = v
162
+ if (v := _n(axis, "grid", "dash")) is not None:
163
+ d["gridDash"] = v
164
+
165
+ # Domain
166
+ if (v := _n(axis, "domain", "visible")) is not None:
167
+ d["domain"] = v
168
+ if (v := _n(axis, "domain", "width")) is not None:
169
+ d["domainWidth"] = v
170
+ if (v := _n(axis, "domain", "color")) is not None:
171
+ d["domainColor"] = v
172
+
173
+ # Ticks
174
+ if (v := _n(axis, "ticks", "visible")) is not None:
175
+ d["ticks"] = v
176
+ if (v := _n(axis, "ticks", "color")) is not None:
177
+ d["tickColor"] = v
178
+ if (v := _n(axis, "ticks", "size")) is not None:
179
+ d["tickSize"] = v
180
+ if (v := _n(axis, "ticks", "width")) is not None:
181
+ d["tickWidth"] = v
182
+
183
+ # Flat fields
184
+ if (v := _n(axis, "orient")) is not None:
185
+ d["orient"] = v
186
+ if (v := _n(axis, "offset")) is not None:
187
+ d["offset"] = v
188
+ if (v := _n(axis, "band_position")) is not None:
189
+ d["bandPosition"] = v
190
+ if (v := _n(axis, "format")) is not None:
191
+ d["format"] = v
192
+ if (v := _n(axis, "values")) is not None:
193
+ d["values"] = v
194
+
195
+ return d
196
+
197
+
198
+ def legend_to_vl(legend: Any) -> dict[str, Any] | None:
199
+ """Map any legend style object → VL legend config dict (non-None fields only).
200
+
201
+ Returns None when disable=True (VL legend: null disables the legend entirely).
202
+ Works for LegendStyle and LegendStylePatch.
203
+ """
204
+ if legend is None:
205
+ return {}
206
+ if _n(legend, "disable") is True:
207
+ return None # VL: encoding.color.legend = null
208
+
209
+ d: dict[str, Any] = {}
210
+ if (v := _n(legend, "orient")) is not None:
211
+ d["orient"] = v
212
+ if (v := _n(legend, "direction")) is not None:
213
+ d["direction"] = v
214
+ if (v := _n(legend, "label", "font", "color")) is not None:
215
+ d["labelColor"] = v
216
+ if (v := _n(legend, "label", "font", "family")) is not None:
217
+ d["labelFont"] = v
218
+ if (v := _n(legend, "label", "font", "size")) is not None:
219
+ d["labelFontSize"] = v
220
+ if (v := _n(legend, "label", "font", "weight")) is not None:
221
+ d["labelFontWeight"] = v
222
+ if (v := _n(legend, "title", "font", "color")) is not None:
223
+ d["titleColor"] = v
224
+ if (v := _n(legend, "title", "font", "family")) is not None:
225
+ d["titleFont"] = v
226
+ if (v := _n(legend, "title", "font", "size")) is not None:
227
+ d["titleFontSize"] = v
228
+ if (v := _n(legend, "title", "font", "weight")) is not None:
229
+ d["titleFontWeight"] = v
230
+ return d
231
+
232
+
233
+ # Per-type mark mappers are intentionally separate: each chart type exposes a different
234
+ # subset of mark properties with different VL field names (e.g. bar uses
235
+ # continuousBandSize, line uses interpolate, scatter uses size). A single generic mapper
236
+ # would need to union all fields and conditionally skip inapplicable ones — more complex
237
+ # with no benefit since callers always know the chart type.
238
+ def bar_mark_to_vl(
239
+ bar: Any, orientation: Literal["vertical", "horizontal"]
240
+ ) -> dict[str, Any]:
241
+ """Map BarStyle/Patch → VL extended mark dict (non-None only).
242
+
243
+ ``orientation`` controls which dimension receives the band fraction:
244
+ - ``"vertical"``: ``mark.width = {"band": band_width}`` — x categorical band.
245
+ - ``"horizontal"``: ``mark.height = {"band": band_width}`` — y categorical band.
246
+ """
247
+ if bar is None:
248
+ return {}
249
+ d: dict[str, Any] = {}
250
+ if (v := _n(bar, "border", "radius")) is not None:
251
+ # Round only the value-end corners (top for vertical bars, right for
252
+ # horizontal). Rounding all four corners carves wedges out of the
253
+ # baseline-meeting edge that expose chart background through the bold
254
+ # zero rule.
255
+ d["cornerRadiusEnd"] = v
256
+ if (v := _n(bar, "border", "color")) is not None:
257
+ d["stroke"] = v
258
+ if (v := _n(bar, "border", "width")) is not None:
259
+ d["strokeWidth"] = v
260
+ if (v := _n(bar, "size")) is not None:
261
+ d["continuousBandSize"] = v
262
+ if (v := _n(bar, "band_width")) is not None:
263
+ if orientation == "horizontal":
264
+ d["height"] = {"band": v}
265
+ else:
266
+ d["width"] = {"band": v}
267
+ return d
268
+
269
+
270
+ def _emit_point_mark(point: Any) -> dict[str, Any]:
271
+ """Build a VL point mark config dict from a MarkPointStyle/Patch (non-None fields only)."""
272
+ d: dict[str, Any] = {}
273
+ if (v := _n(point, "size")) is not None:
274
+ d["size"] = v
275
+ if (v := _n(point, "color")) is not None:
276
+ d["color"] = v
277
+ if (v := _n(point, "shape")) is not None:
278
+ d["shape"] = v
279
+ if (v := _n(point, "opacity")) is not None:
280
+ d["opacity"] = v
281
+ filled = _n(point, "filled")
282
+ if filled is not None:
283
+ d["filled"] = filled
284
+ # fill only applies when filled=false; encoding.color owns the fill channel otherwise.
285
+ if filled is False and (v := _n(point, "fill")) is not None:
286
+ d["fill"] = v
287
+ return d
288
+
289
+
290
+ def line_mark_to_vl(line: Any, point: Any = None) -> dict[str, Any]:
291
+ """Map LineMarkStyle → VL extended mark dict (non-None only).
292
+
293
+ ``point`` is an optional PointMarkStyle; when provided and point.size > 0,
294
+ emits a VL point overlay on the line.
295
+ """
296
+ if line is None:
297
+ return {}
298
+ d: dict[str, Any] = _stroke_to_vl(_n(line, "stroke"))
299
+ if (v := _n(line, "curve")) is not None:
300
+ d["interpolate"] = v
301
+ if point is not None:
302
+ pt_size = _n(point, "size")
303
+ if pt_size is not None and pt_size > 0:
304
+ d["point"] = _emit_point_mark(point)
305
+ return d
306
+
307
+
308
+ def area_mark_to_vl(area: Any) -> dict[str, Any]:
309
+ """Map AreaStyle/Patch → VL extended mark dict (non-None only)."""
310
+ if area is None:
311
+ return {}
312
+ d: dict[str, Any] = {}
313
+ if (v := _n(area, "opacity")) is not None:
314
+ d["opacity"] = v
315
+ d.update(_stroke_to_vl(_n(area, "stroke")))
316
+ return d
317
+
318
+
319
+ def scatter_mark_to_vl(point: Any) -> dict[str, Any]:
320
+ """Map PointMarkStyle → VL extended mark dict (non-None only)."""
321
+ if point is None:
322
+ return {}
323
+ return _emit_point_mark(point)
324
+
325
+
326
+ def slice_mark_to_vl(arc: Any) -> dict[str, Any]:
327
+ """Map SliceStyle/Patch → VL extended mark dict (non-None only)."""
328
+ if arc is None:
329
+ return {}
330
+ d: dict[str, Any] = {}
331
+ if (v := _n(arc, "gap")) is not None:
332
+ d["padAngle"] = v
333
+ if (v := _n(arc, "corner_radius")) is not None:
334
+ d["cornerRadius"] = v
335
+ d.update(_stroke_to_vl(_n(arc, "stroke")))
336
+ return d
337
+
338
+
339
+ def map_fields(source: BaseModel, field_map: dict[str, str]) -> dict[str, object]:
340
+ """Extract non-None fields from source, mapping snake_case to camelCase."""
341
+ result: dict[str, object] = {}
342
+ for src_name, dst_name in field_map.items():
343
+ value = getattr(source, src_name, None)
344
+ if value is not None:
345
+ result[dst_name] = value
346
+ return result
@@ -0,0 +1,24 @@
1
+ """Core-owned chart hover interactivity runtime."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING
6
+
7
+ from dataface.core.render.script_embedding import embed_svg_script
8
+
9
+ if TYPE_CHECKING:
10
+ from dataface.core.compile.models.style.merged import MergedStyle
11
+
12
+
13
+ def generate_svg_chart_interactivity_script(
14
+ resolved_style: "MergedStyle",
15
+ ) -> str:
16
+ """Embed the chart hover runtime into SVG output."""
17
+ assert resolved_style is not None
18
+ font_family = str(resolved_style.font.family)
19
+ script_path = (
20
+ Path(__file__).parent / "templates" / "scripts" / "chart_interactivity.js"
21
+ )
22
+ script = script_path.read_text()
23
+ script = script.replace('"__DATAFACE_FONT_FAMILY__"', json.dumps(font_family))
24
+ return embed_svg_script(script)