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,471 @@
1
+ """Per-chart style cascade — merges chart-local ChartStylePatch into MergedChartsStyle.
2
+
3
+ Stage: COMPILE (pure — no render imports)
4
+ Purpose: Given a board MergedStyle and a chart-local ChartStylePatch, produce
5
+ the fully-merged MergedChartsStyle that the render layer consumes.
6
+
7
+ Architectural directive: render code reads only resolved_style.* — it never sees
8
+ the chart-local Patch. After build_resolved_style runs, every authored override
9
+ (axis, legend, scale, mark, palette, title, …) is baked into the resolved
10
+ fields. The renderer reads `resolved_style.axis_x.label.padding` and gets the
11
+ final value with no authored-vs-cascaded discrimination.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import dataclasses
17
+ from typing import Any, Literal
18
+
19
+ from dataface.core.compile.config import get_config
20
+ from dataface.core.compile.models.style.authored import (
21
+ AreaChartStylePatch,
22
+ BarChartStylePatch,
23
+ ChartStylePatch,
24
+ LayeredChartStyle,
25
+ LegendStylePatch,
26
+ LineChartStylePatch,
27
+ )
28
+ from dataface.core.compile.models.style.compiled import (
29
+ AxisStyle,
30
+ AxisStylePatch,
31
+ InferenceStyle,
32
+ )
33
+
34
+ # Family sub-patch keys in author-precedence order. The cascade reads global
35
+ # fields (palette, background, inference, …) from the FIRST non-null sub-patch.
36
+ # For non-layered charts exactly one key is set; for layered charts the first
37
+ # non-null entry wins for shared fields.
38
+ _ALL_FAMILY_KEYS: tuple[str, ...] = (
39
+ "bar",
40
+ "line",
41
+ "area",
42
+ "scatter",
43
+ "pie",
44
+ "kpi",
45
+ "spark_bar",
46
+ "spark",
47
+ "table",
48
+ "heatmap",
49
+ "geoshape",
50
+ "point_map",
51
+ )
52
+ from dataface.core.compile.models.style.merged import (
53
+ MergedChartsStyle,
54
+ MergedLegendStyle,
55
+ MergedStyle,
56
+ _resolve_color_tokens,
57
+ deep_merge,
58
+ resolve_style,
59
+ )
60
+
61
+
62
+ def _get_primary_patch(chart_style: ChartStylePatch) -> Any:
63
+ """Return the first non-null family sub-patch, or None.
64
+
65
+ For non-layered charts exactly one family slot is set; that patch carries
66
+ both the shared fields (palette, background, inference, …) and any
67
+ family-specific overrides. For layered charts ``layered`` is checked first;
68
+ it carries all shared cartesian fields directly on the flat surface.
69
+ """
70
+ layered = getattr(chart_style, "layered", None)
71
+ if layered is not None:
72
+ return layered
73
+ for key in _ALL_FAMILY_KEYS:
74
+ patch = getattr(chart_style, key, None)
75
+ if patch is not None:
76
+ return patch
77
+ return None
78
+
79
+
80
+ def _chart_style_has_overrides(style: ChartStylePatch | None) -> bool:
81
+ if style is None:
82
+ return False
83
+ return any(v is not None for v in style.model_dump().values())
84
+
85
+
86
+ def _has_family_legend_patch(
87
+ base_charts: MergedChartsStyle, chart_type: str | None
88
+ ) -> bool:
89
+ """Return True if chart_type has a non-None legend patch in the theme family style.
90
+
91
+ Used to bypass the fast path when a per-family legend override exists in the theme
92
+ but no chart-local style override is authored.
93
+ """
94
+ if not chart_type:
95
+ return False
96
+ family = getattr(base_charts, chart_type, None)
97
+ if family is None:
98
+ return False
99
+ return getattr(family, "legend", None) is not None
100
+
101
+
102
+ def _merge_legend(
103
+ base: MergedLegendStyle, patch: LegendStylePatch | None
104
+ ) -> MergedLegendStyle:
105
+ """Merge a LegendStylePatch onto a MergedLegendStyle.
106
+
107
+ Patch is a typed all-Optional model — direct attribute access; no
108
+ ``getattr`` defensiveness. Non-None patch fields win; None preserves base.
109
+ """
110
+ if patch is None:
111
+ return base
112
+ updates: dict[str, Any] = {}
113
+ if patch.orient is not None:
114
+ updates["orient"] = patch.orient
115
+ if patch.direction is not None:
116
+ updates["direction"] = patch.direction
117
+ if patch.disable is not None:
118
+ updates["disable"] = patch.disable
119
+ for sub_name in ("label", "title"):
120
+ sub_patch = getattr(patch, sub_name)
121
+ if sub_patch is None:
122
+ continue
123
+ sub_base = getattr(base, sub_name)
124
+ sub_updates: dict[str, Any] = {}
125
+ if sub_patch.padding is not None:
126
+ sub_updates["padding"] = sub_patch.padding
127
+ if sub_patch.font is not None:
128
+ font_updates = {
129
+ f: v
130
+ for f in ("family", "color", "size", "weight")
131
+ if (v := getattr(sub_patch.font, f)) is not None
132
+ }
133
+ if font_updates:
134
+ sub_updates["font"] = dataclasses.replace(sub_base.font, **font_updates)
135
+ if sub_updates:
136
+ updates[sub_name] = dataclasses.replace(sub_base, **sub_updates)
137
+ return dataclasses.replace(base, **updates) if updates else base
138
+
139
+
140
+ def build_resolved_style(
141
+ board_resolved: MergedStyle | None,
142
+ chart_style: Any,
143
+ chart_type: str | None = None,
144
+ ) -> MergedChartsStyle:
145
+ """Build the fully-merged MergedChartsStyle for a single chart.
146
+
147
+ Fast path: chart has no local style overrides and no per-family theme patches
148
+ for chart_type → return board.charts unchanged.
149
+
150
+ Merge path: apply chart_style on top of board.charts. Every chart-local
151
+ override (axis_x/axis_y/axis/axis_quantitative/axis_band, legend, scale,
152
+ palette, range, inference, table, data_table, background, title, x_temporal_mode,
153
+ bar/line/area/scatter/arc/kpi/spark/spark_bar) is applied here so the renderer
154
+ reads a single, fully-resolved value without ever touching the Patch.
155
+
156
+ chart_type: the chart family name ("bar", "line", etc.) used to apply per-family
157
+ theme patches (e.g. editorial bar.legend.disable: false). When given, per-family
158
+ theme legend patches override the global resolved legend before face-local patches.
159
+ """
160
+ base_resolved = board_resolved or resolve_style(get_config().style)
161
+ base_charts = base_resolved.charts
162
+
163
+ # Coerce dict / unknown shape (MockChart in tests) to a typed patch.
164
+ if (
165
+ isinstance(chart_style, dict)
166
+ or chart_style is not None
167
+ and not isinstance(chart_style, ChartStylePatch)
168
+ ):
169
+ # LayeredChartStyle passed directly: wrap it in the monolithic container.
170
+ if isinstance(chart_style, LayeredChartStyle):
171
+ chart_style = ChartStylePatch(layered=chart_style)
172
+ else:
173
+ # Auto-wrap table-specific style fields in the table sub-patch if needed
174
+ if isinstance(chart_style, dict):
175
+ table_fields = {
176
+ "columns",
177
+ "pagination",
178
+ "column_defaults",
179
+ "row",
180
+ "header",
181
+ "row_numbers",
182
+ "wrap",
183
+ "background",
184
+ "color",
185
+ "rule",
186
+ "outer_padding",
187
+ "bottom_padding",
188
+ "title",
189
+ "border",
190
+ "header_overflow",
191
+ "column_layout",
192
+ }
193
+ has_table_fields = any(k in chart_style for k in table_fields)
194
+ if has_table_fields and "table" not in chart_style:
195
+ # Wrap table fields in table={...}
196
+ table_style = {}
197
+ other_style = {}
198
+ for k, v in chart_style.items():
199
+ if k in table_fields:
200
+ table_style[k] = v
201
+ else:
202
+ other_style[k] = v
203
+ if table_style:
204
+ other_style["table"] = table_style
205
+ chart_style = other_style
206
+ chart_style = ChartStylePatch.model_validate(chart_style)
207
+
208
+ # Resolve palette tokens (e.g. "dft-creams.cream-50" → "#918878") across
209
+ # all color-bearing fields of the chart-local style patch — before any field
210
+ # is stored as a sentinel or merged into the resolved tree. Raises
211
+ # UnknownColorError on unknown tokens, matching validate-and-error-fast.
212
+ if chart_style is not None:
213
+ chart_style = _resolve_color_tokens(chart_style)
214
+
215
+ if not _chart_style_has_overrides(chart_style) and not _has_family_legend_patch(
216
+ base_charts, chart_type
217
+ ):
218
+ return base_charts
219
+ assert chart_style is not None or chart_type is not None
220
+
221
+ overrides: dict[str, Any] = {}
222
+
223
+ # Primary family sub-patch: the single non-null family for non-layered charts,
224
+ # or the first non-null for layered charts. Shared global fields (palette,
225
+ # background, inference, axis overrides, …) are read from this patch.
226
+ primary = _get_primary_patch(chart_style)
227
+
228
+ # --- Palette / range ---
229
+ # Named palette (e.g. ``style.palette: dft-seq-rust``) resolves through
230
+ # the palette resolver into the stops list that MergedChartsStyle.palette
231
+ # holds. Explicit ``style.range.category`` (a list of CSS color strings)
232
+ # wins over the named form when both are authored.
233
+ _palette = getattr(primary, "palette", None)
234
+ if _palette is not None:
235
+ from dataface.core.compile.palette import palette as resolve_palette
236
+
237
+ # Patch models accept both string names and resolved lists; compiled
238
+ # models always materialise as list[str] via the field_validator.
239
+ overrides["palette"] = (
240
+ resolve_palette(_palette) if isinstance(_palette, str) else _palette
241
+ )
242
+ _range = getattr(primary, "range", None)
243
+ if _range is not None and _range.category is not None:
244
+ overrides["palette"] = _range.category
245
+
246
+ # --- Inference ---
247
+ _inference = getattr(primary, "inference", None)
248
+ if _inference is not None:
249
+ board_inf = base_charts.inference
250
+ merged_inf = {
251
+ f: (
252
+ pv
253
+ if (pv := getattr(_inference, f)) is not None
254
+ else getattr(board_inf, f)
255
+ )
256
+ for f in InferenceStyle.model_fields
257
+ }
258
+ overrides["inference"] = InferenceStyle(**merged_inf)
259
+
260
+ # --- data_table (cartesian families only via _CartesianChartStyle) ---
261
+ _data_table = getattr(primary, "data_table", None)
262
+ if _data_table is not None:
263
+ overrides["data_table"] = deep_merge(base_charts.data_table, _data_table)
264
+
265
+ # --- Per-chart-type overlays ---
266
+ # deep_merge handles every family-specific field including orientation, stack,
267
+ # color_scheme (geoshape), column_layout/columns/column_defaults (table), etc.
268
+ for ct in _ALL_FAMILY_KEYS:
269
+ patch = getattr(chart_style, ct, None)
270
+ if patch is not None:
271
+ overrides[ct] = deep_merge(getattr(base_charts, ct), patch)
272
+
273
+ # --- Layered mark distribution ---
274
+ # LayeredChartStyle.marks distributes per-mark-type overrides to the bar/line/area
275
+ # family slots that the renderer reads (eff.bar.marks.bar, eff.line.marks.line, etc.).
276
+ _layered = getattr(chart_style, "layered", None)
277
+ if _layered is not None and _layered.marks is not None:
278
+ _lm = _layered.marks
279
+ if _lm.bar is not None:
280
+ overrides["bar"] = deep_merge(
281
+ overrides.get("bar", base_charts.bar),
282
+ BarChartStylePatch.model_validate(
283
+ {"marks": {"bar": _lm.bar.model_dump(exclude_none=True)}}
284
+ ),
285
+ )
286
+ if _lm.line is not None or _lm.point is not None:
287
+ _line_marks: dict[str, Any] = {}
288
+ if _lm.line is not None:
289
+ _line_marks["line"] = _lm.line.model_dump(exclude_none=True)
290
+ if _lm.point is not None:
291
+ _line_marks["point"] = _lm.point.model_dump(exclude_none=True)
292
+ overrides["line"] = deep_merge(
293
+ overrides.get("line", base_charts.line),
294
+ LineChartStylePatch.model_validate({"marks": _line_marks}),
295
+ )
296
+ if _lm.area is not None or _lm.point is not None:
297
+ # Point marks are distributed to both line and area families because
298
+ # area+line combos can render overlaid point marks via either family slot.
299
+ _area_marks: dict[str, Any] = {}
300
+ if _lm.area is not None:
301
+ _area_marks["area"] = _lm.area.model_dump(exclude_none=True)
302
+ if _lm.point is not None:
303
+ _area_marks["point"] = _lm.point.model_dump(exclude_none=True)
304
+ overrides["area"] = deep_merge(
305
+ overrides.get("area", base_charts.area),
306
+ AreaChartStylePatch.model_validate({"marks": _area_marks}),
307
+ )
308
+
309
+ # --- Callout tone override ---
310
+ # MergedCalloutChartStyle is a frozen dataclass (not a BaseModel), so it
311
+ # cannot use the deep_merge loop above. Only tone is chart-locally authored.
312
+ _callout_patch = getattr(chart_style, "callout", None)
313
+ if _callout_patch is not None and _callout_patch.tone is not None:
314
+ overrides["callout"] = dataclasses.replace(
315
+ base_charts.callout, tone=_callout_patch.tone
316
+ )
317
+
318
+ # --- Top-level Vega-facing ---
319
+ _background = getattr(primary, "background", None)
320
+ if _background is not None:
321
+ overrides["background"] = _background
322
+ _color = getattr(primary, "color", None)
323
+ if isinstance(_color, str):
324
+ # Only string (static paint) flows to MergedChartsStyle.color.
325
+ # StyleColorConfig (gradient scale) is consumed by normalize_chart_channels
326
+ # and never stored in the style cascade.
327
+ overrides["color"] = _color
328
+ _title = getattr(primary, "title", None)
329
+ if _title is not None:
330
+ overrides["title"] = deep_merge(base_charts.title, _title)
331
+
332
+ # --- Chart-local axis overrides ---
333
+ # Stored as typed AxisStylePatch sentinels on resolved_chart_style
334
+ # so the renderer's resolved_axis_style helper can apply Layers 4/5/6 in
335
+ # the canonical cascade order (after Layer 3 axis_quantitative and Layer
336
+ # 3.5 chart-type axis). Baking them into base.axis_x/y here would invert
337
+ # the precedence relative to axis_quantitative — the renderer must keep
338
+ # control of the layering.
339
+ _axis = getattr(primary, "axis", None)
340
+ if _axis is not None:
341
+ overrides["axis_overrides_global"] = _axis
342
+ _axis_x = getattr(primary, "axis_x", None)
343
+ if _axis_x is not None:
344
+ overrides["axis_overrides_x"] = _axis_x
345
+ _axis_y = getattr(primary, "axis_y", None)
346
+ if _axis_y is not None:
347
+ overrides["axis_overrides_y"] = _axis_y
348
+ _axis_quantitative = getattr(primary, "axis_quantitative", None)
349
+ if _axis_quantitative is not None:
350
+ overrides["axis_overrides_quantitative"] = _axis_quantitative
351
+ _axis_band = getattr(primary, "axis_band", None)
352
+ if _axis_band is not None:
353
+ overrides["axis_overrides_band"] = _axis_band
354
+
355
+ # --- Legend ---
356
+ # Apply per-family theme patch first (e.g. editorial bar.legend.disable: false),
357
+ # then face-local patch on top. Merge order: global → family theme → face-local.
358
+ _effective_legend = base_charts.legend
359
+ if chart_type is not None:
360
+ family_style = getattr(base_charts, chart_type, None)
361
+ family_legend_patch = (
362
+ getattr(family_style, "legend", None) if family_style is not None else None
363
+ )
364
+ if family_legend_patch is not None:
365
+ _effective_legend = _merge_legend(base_charts.legend, family_legend_patch)
366
+ _legend = getattr(primary, "legend", None) if primary is not None else None
367
+ if _legend is not None:
368
+ _effective_legend = _merge_legend(_effective_legend, _legend)
369
+ if _effective_legend is not base_charts.legend:
370
+ overrides["legend"] = _effective_legend
371
+
372
+ # --- Scale (chart-level global; per-axis already lands on axis_*.scale above) ---
373
+ _scale = getattr(primary, "scale", None)
374
+ if _scale is not None:
375
+ overrides["scale"] = _scale
376
+
377
+ # --- Pagination (table charts expose this via TableChartStyle.pagination) ---
378
+ # Chart-local pagination layers on top of the face default. ``enabled``
379
+ # is required on the authored Patch so it always sets; ``page_size`` is
380
+ # optional — when unset the cascade falls back to the face-level
381
+ # page_size so the renderer reads a single resolved value off
382
+ # ``effective.pagination`` without re-running the merge.
383
+ _pagination = getattr(primary, "pagination", None)
384
+ if _pagination is not None:
385
+ face = base_charts.pagination
386
+ merged_pagination = _pagination
387
+ if (
388
+ merged_pagination.page_size is None
389
+ and face is not None
390
+ and face.page_size is not None
391
+ ):
392
+ merged_pagination = merged_pagination.model_copy(
393
+ update={"page_size": face.page_size}
394
+ )
395
+ overrides["pagination"] = merged_pagination
396
+
397
+ if not overrides:
398
+ return base_charts
399
+
400
+ return dataclasses.replace(base_charts, **overrides)
401
+
402
+
403
+ # ── Axis-cascade emit helper ──────────────────────────────────────────
404
+
405
+
406
+ def _chart_type_axis_patch(
407
+ effective: MergedChartsStyle,
408
+ chart_type: str,
409
+ axis_name: Literal["axis_x", "axis_y"],
410
+ ) -> AxisStylePatch | None:
411
+ """Return the chart-type-specific axis patch (Layer 3.5 of the cascade).
412
+
413
+ Pie charts have no cartesian axes, so this always returns None for them.
414
+ """
415
+ chart_type_style = getattr(effective, chart_type, None)
416
+ return (
417
+ getattr(chart_type_style, axis_name, None)
418
+ if chart_type_style is not None
419
+ else None
420
+ )
421
+
422
+
423
+ def resolved_axis_style(
424
+ effective: MergedChartsStyle,
425
+ axis_name: Literal["axis_x", "axis_y"],
426
+ channel_type: str,
427
+ chart_type_axis_patch: AxisStylePatch | None = None,
428
+ ) -> AxisStyle:
429
+ """Walk the 7-layer axis cascade and return a merged AxisStyle.
430
+
431
+ Single source of truth for axis state at emit time. The renderer reads
432
+ every axis field (ticks, grid, domain, orient, offset, ...) from this
433
+ return value — never from ``effective.axis_x`` / ``axis_y`` directly,
434
+ since those reflect only Layers 1+2 and silently miss type-conditional,
435
+ chart-type, and chart-local overrides.
436
+
437
+ Layer order (each overrides the previous):
438
+ 1+2. theme global + channel-specific (effective.axis_{x,y})
439
+ 3. theme type-conditional (effective.axis_quantitative / band)
440
+ 3.5. theme chart-type-specific (chart_type_axis_patch)
441
+ 4. chart-local global (effective.axis_overrides_global)
442
+ 5. chart-local type-conditional (effective.axis_overrides_quantitative / _band)
443
+ 6. chart-local channel-specific (effective.axis_overrides_{x,y}) — wins
444
+
445
+ Chart-local layers come from typed Patch sentinels stored on
446
+ ``effective`` by ``build_resolved_style`` — the chart's ChartStylePatch
447
+ is consumed by the cascade and never reaches the renderer.
448
+ """
449
+ base = AxisStyle.model_validate(dataclasses.asdict(getattr(effective, axis_name)))
450
+
451
+ if channel_type == "quantitative":
452
+ base = deep_merge( # type: ignore[assignment]
453
+ base,
454
+ AxisStyle.model_validate(dataclasses.asdict(effective.axis_quantitative)),
455
+ )
456
+ # axis_band has no theme-resolved representation; only chart-local applies.
457
+
458
+ base = deep_merge(base, chart_type_axis_patch) # type: ignore[assignment]
459
+
460
+ base = deep_merge(base, effective.axis_overrides_global) # type: ignore[assignment]
461
+ if channel_type == "quantitative":
462
+ base = deep_merge( # type: ignore[assignment]
463
+ base, effective.axis_overrides_quantitative
464
+ )
465
+ elif channel_type in ("ordinal", "nominal"):
466
+ base = deep_merge(base, effective.axis_overrides_band) # type: ignore[assignment]
467
+ base = deep_merge( # type: ignore[assignment]
468
+ base, getattr(effective, f"axis_overrides_{axis_name[-1]}")
469
+ )
470
+
471
+ return base