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,370 @@
1
+ """Compact horizontal spark bar chart SVG rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html as html_module
6
+ from typing import Any
7
+
8
+ from dataface.core.compile.config import get_chart_rendering
9
+ from dataface.core.compile.models.primitives import FontStyle
10
+ from dataface.core.compile.models.style.merged import (
11
+ MergedChartsStyle,
12
+ MergedStyle,
13
+ )
14
+ from dataface.core.compile.typography import chart_title_spec
15
+ from dataface.core.render.utils import normalize_data_types
16
+
17
+ _DEFAULT_WIDTH = 200.0
18
+ _MIN_WIDTH = 150.0
19
+
20
+
21
+ def _auto_detect_spark_bar_fields(
22
+ data: list[dict[str, Any]],
23
+ x_field: str | None,
24
+ y_field: str | None,
25
+ ) -> tuple[str | None, str | None]:
26
+ """Auto-detect x (frequency) and y (category) fields for spark bar charts.
27
+
28
+ Args:
29
+ data: List of data rows
30
+ x_field: Explicitly specified x field (frequency)
31
+ y_field: Explicitly specified y field (category)
32
+
33
+ Returns:
34
+ Tuple of (x_field, y_field) with auto-detected values if not specified
35
+
36
+ """
37
+ if not data:
38
+ return x_field, y_field
39
+
40
+ # Auto-detect y field (category)
41
+ if not y_field:
42
+ for key in data[0]:
43
+ if key.lower() in ("value", "label", "category", "name"):
44
+ y_field = key
45
+ break
46
+ if not y_field:
47
+ # Use first string column as y
48
+ for key, val in data[0].items():
49
+ if isinstance(val, str):
50
+ y_field = key
51
+ break
52
+
53
+ # Auto-detect x field (frequency)
54
+ if not x_field:
55
+ for key in data[0]:
56
+ if key.lower() in ("frequency", "count", "freq", "n", "total"):
57
+ x_field = key
58
+ break
59
+ if not x_field:
60
+ # Use first numeric column as x
61
+ for key, val in data[0].items():
62
+ if isinstance(val, (int, float)) and not isinstance(val, bool):
63
+ x_field = key
64
+ break
65
+
66
+ return x_field, y_field
67
+
68
+
69
+ def _render_spark_bar_row(
70
+ row: dict[str, Any],
71
+ row_index: int,
72
+ row_y: float,
73
+ x_field: str | None,
74
+ y_field: str | None,
75
+ bar_height: int,
76
+ bar_area_width: float,
77
+ max_value: float,
78
+ left_padding: int,
79
+ chart_width: float,
80
+ text_color: str,
81
+ bar_color: str,
82
+ bar_background: str,
83
+ labels_visible: bool,
84
+ counts_visible: bool,
85
+ spark_config: Any,
86
+ spark_rendering: Any,
87
+ font: FontStyle,
88
+ ) -> list[str]:
89
+ """Render a single bar row for spark bar chart.
90
+
91
+ Args:
92
+ row: Data row dict
93
+ row_index: Index of this row (0-based)
94
+ row_y: Y position for this row
95
+ x_field: Field name for frequency/count values
96
+ y_field: Field name for category labels
97
+ bar_height: Height of each bar (may be overridden from default)
98
+ bar_area_width: Width of the bar area
99
+ max_value: Maximum value for scaling bars
100
+ left_padding: Left padding before bar starts
101
+ chart_width: Total chart width
102
+ text_color: Color for text labels
103
+ bar_color: Fill color for bars (may be overridden from default)
104
+ bar_background: Background color for bar track
105
+ labels_visible: Whether to show category labels
106
+ counts_visible: Whether to show count labels
107
+ spark_config: Config object for non-overridable values (border_radius, font sizes, label.width)
108
+ font: FontStyle for text elements (reads .family)
109
+
110
+ Returns:
111
+ List of SVG element strings for this row
112
+
113
+ """
114
+ svg_parts: list[str] = []
115
+
116
+ # Get values
117
+ label_value = str(row.get(y_field, "")) if y_field else f"Item {row_index + 1}"
118
+ count_value = row.get(x_field, 0) if x_field else 0
119
+ if not isinstance(count_value, (int, float)):
120
+ count_value = 0
121
+
122
+ # Calculate bar width
123
+ bar_width = (float(count_value) / max_value) * bar_area_width
124
+
125
+ # Truncate label if too long (use config for label.width)
126
+ max_label_chars = int(spark_config.label.width / spark_rendering.avg_char_width_px)
127
+ display_label = label_value
128
+ if len(display_label) > max_label_chars:
129
+ display_label = display_label[: max_label_chars - 2] + "..."
130
+ escaped_label = html_module.escape(display_label)
131
+
132
+ # Format count
133
+ if isinstance(count_value, float) and count_value == int(count_value):
134
+ display_count = str(int(count_value))
135
+ elif isinstance(count_value, float):
136
+ display_count = f"{count_value:,.1f}"
137
+ else:
138
+ display_count = f"{count_value:,}"
139
+
140
+ # Render label
141
+ if labels_visible:
142
+ label_y = row_y + (bar_height / 2) + spark_rendering.text_baseline_offset
143
+ svg_parts.append(
144
+ f'<text x="0" y="{label_y:.1f}" '
145
+ f'font-size="{spark_config.font.size}" fill="{text_color}" '
146
+ f'font-family="{font.family}">'
147
+ f"{escaped_label}</text>",
148
+ )
149
+
150
+ # Render bar background
151
+ bar_x = left_padding
152
+ svg_parts.append(
153
+ f'<rect x="{bar_x}" y="{row_y:.1f}" '
154
+ f'width="{bar_area_width}" height="{bar_height}" '
155
+ f'fill="{bar_background}" rx="{spark_config.border.radius}"/>',
156
+ )
157
+
158
+ # Render bar fill
159
+ if bar_width > 0:
160
+ svg_parts.append(
161
+ f'<rect x="{bar_x}" y="{row_y:.1f}" '
162
+ f'width="{bar_width:.1f}" height="{bar_height}" '
163
+ f'fill="{bar_color}" rx="{spark_config.border.radius}"/>',
164
+ )
165
+
166
+ # Render count
167
+ if counts_visible:
168
+ count_x = chart_width - spark_rendering.side_padding
169
+ count_y = row_y + (bar_height / 2) + spark_rendering.text_baseline_offset
170
+ svg_parts.append(
171
+ f'<text x="{count_x:.1f}" y="{count_y:.1f}" '
172
+ f'font-size="{spark_config.font.size}" fill="{text_color}" text-anchor="end" '
173
+ f'font-family="{font.family}" '
174
+ f'style="font-variant-numeric: tabular-nums lining-nums;">'
175
+ f"{display_count}</text>",
176
+ )
177
+
178
+ return svg_parts
179
+
180
+
181
+ def render_spark_bar_svg(
182
+ chart: Any,
183
+ data: list[dict[str, Any]],
184
+ width: float | None = None,
185
+ height: float | None = None,
186
+ is_placeholder: bool = False,
187
+ *,
188
+ resolved_style: MergedChartsStyle,
189
+ face_level: int = 1,
190
+ board_style: MergedStyle,
191
+ ) -> str:
192
+ """Render a spark bar chart as SVG.
193
+
194
+ Creates a compact horizontal bar chart for profiler column cards with:
195
+ - Category labels on the left
196
+ - Horizontal bars showing frequency/count
197
+ - Optional count labels on the right
198
+ - Truncated labels for long text
199
+
200
+ Args:
201
+ chart: Chart definition with x (frequency), y (category) fields
202
+ data: List of dicts containing bar data
203
+ width: Optional explicit width in pixels
204
+ height: Optional explicit height in pixels
205
+ is_placeholder: If True, render with placeholder styling
206
+ face_level: Heading level of the parent face (root=1, nested=2, …).
207
+ Chart title uses face_level + 1.
208
+ board_style: Board-level MergedStyle for theme color reads.
209
+
210
+ Returns:
211
+ SVG string representing the spark bar chart
212
+
213
+ """
214
+ data = normalize_data_types(data)
215
+
216
+ # spark_config comes from resolved_style, which is already merged with any
217
+ # chart-local ChartStylePatch.spark_bar overlay by _build_resolved_style.
218
+ spark_config = resolved_style.spark_bar
219
+ spark_rendering = (
220
+ get_chart_rendering().spark_bar
221
+ ) # designer-tunable layout constants
222
+
223
+ assert spark_config.font.family is not None, "style.font.family must be configured"
224
+ text_color = board_style.font.color
225
+ secondary_color = board_style.variables.font.color
226
+ subtitle_text = chart.subtitle or ""
227
+
228
+ # Extract overridable values (post-patch)
229
+ bar_height = spark_config.bar.height
230
+ max_bars = spark_config.max_bars
231
+ labels_visible = spark_config.label.visible
232
+ counts_visible = spark_config.count.visible
233
+ bar_color = spark_config.bar.color
234
+ assert (
235
+ bar_color is not None
236
+ ), "spark_bar.bar.color requires resolved style (pass resolved_style)"
237
+ # board_style.variables.input.background carries the active board theme's
238
+ # track color — always use it.
239
+ bar_background = board_style.variables.input.background
240
+
241
+ # Get field names from chart, auto-detecting if not specified
242
+ x_field, y_field = _auto_detect_spark_bar_fields(data, chart.x, chart.y)
243
+
244
+ # Limit data to max_bars
245
+ visible_data = data[:max_bars] if data else []
246
+
247
+ # Calculate dimensions
248
+ num_bars = len(visible_data)
249
+ row_height = bar_height + spark_config.bar.padding
250
+ chart_width = width or _DEFAULT_WIDTH
251
+ chart_width = max(chart_width, _MIN_WIDTH)
252
+ # Width-aware title spec: size, weight, family based on card width
253
+ chart_title_size, chart_title_weight, chart_title_family = chart_title_spec(
254
+ chart_width, level=face_level + 1, resolved_chart_style=resolved_style
255
+ )
256
+ title_height = spark_rendering.title_height if chart.title else 0
257
+ if chart.title and subtitle_text:
258
+ title_height += chart_title_size
259
+ chart_height = height or (
260
+ title_height + (num_bars * row_height) + spark_config.bar.padding
261
+ )
262
+
263
+ # Calculate bar area dimensions
264
+ left_padding = (
265
+ spark_config.label.width if labels_visible else spark_rendering.side_padding
266
+ )
267
+ right_padding = (
268
+ spark_config.count.width if counts_visible else spark_rendering.side_padding
269
+ )
270
+ bar_area_width = max(chart_width - left_padding - right_padding, 20)
271
+
272
+ # Find max value for scaling
273
+ max_value = 0.0
274
+ if visible_data and x_field:
275
+ for row in visible_data:
276
+ val = row.get(x_field, 0)
277
+ if isinstance(val, (int, float)):
278
+ max_value = max(max_value, float(val))
279
+ if max_value == 0:
280
+ max_value = 1.0 # Prevent division by zero
281
+
282
+ # Build SVG elements
283
+ svg_parts: list[str] = []
284
+ current_y = 0.0
285
+
286
+ # Title
287
+ if chart.title:
288
+ escaped_title = html_module.escape(str(chart.title))
289
+ svg_parts.append(
290
+ f'<text x="0" y="{spark_rendering.title_baseline_y}" '
291
+ f'font-size="{chart_title_size}" font-weight="{chart_title_weight}" fill="{text_color}" '
292
+ f'font-family="{chart_title_family}">'
293
+ f"{escaped_title}</text>",
294
+ )
295
+ if subtitle_text:
296
+ escaped_subtitle = html_module.escape(subtitle_text)
297
+ assert (
298
+ spark_config.subtitle.font.size is not None
299
+ ), "theme must supply spark_bar.subtitle.font.size"
300
+ subtitle_font_size = float(spark_config.subtitle.font.size)
301
+ svg_parts.append(
302
+ f'<text x="0" y="{spark_rendering.title_baseline_y + chart_title_size}" '
303
+ f'font-size="{subtitle_font_size}" fill="{secondary_color}" '
304
+ f'font-family="{spark_config.font.family}">'
305
+ f"{escaped_subtitle}</text>",
306
+ )
307
+ current_y = title_height
308
+
309
+ # Render bars
310
+ for i, row in enumerate(visible_data):
311
+ row_y = current_y + (i * row_height)
312
+ svg_parts.extend(
313
+ _render_spark_bar_row(
314
+ row=row,
315
+ row_index=i,
316
+ row_y=row_y,
317
+ x_field=x_field,
318
+ y_field=y_field,
319
+ bar_height=int(bar_height),
320
+ bar_area_width=bar_area_width,
321
+ max_value=max_value,
322
+ left_padding=int(left_padding),
323
+ chart_width=chart_width,
324
+ text_color=text_color,
325
+ bar_color=bar_color,
326
+ bar_background=bar_background,
327
+ labels_visible=labels_visible,
328
+ counts_visible=counts_visible,
329
+ spark_config=spark_config,
330
+ spark_rendering=spark_rendering,
331
+ font=spark_config.font,
332
+ ),
333
+ )
334
+
335
+ # Show "more" indicator if data was truncated
336
+ if len(data) > len(visible_data):
337
+ more_count = len(data) - len(visible_data)
338
+ more_y = (
339
+ current_y + (num_bars * row_height) + spark_rendering.more_rows_offset_y
340
+ )
341
+ svg_parts.append(
342
+ f'<text x="{chart_width / 2}" y="{more_y:.1f}" '
343
+ f'font-size="{spark_rendering.more_rows_font_size}" fill="{secondary_color}" text-anchor="middle" font-style="italic" '
344
+ f'font-family="{spark_config.font.family}" '
345
+ f'style="font-variant-numeric: tabular-nums lining-nums;">'
346
+ f"+ {more_count} more</text>",
347
+ )
348
+ chart_height = more_y + spark_rendering.more_rows_bottom_padding
349
+
350
+ # Wrap in SVG
351
+ svg_result = f"""<svg xmlns="http://www.w3.org/2000/svg" width="{chart_width}" height="{chart_height}" viewBox="0 0 {chart_width} {chart_height}">
352
+ {"".join(svg_parts)}
353
+ </svg>"""
354
+
355
+ # Apply placeholder styling if needed
356
+ if is_placeholder:
357
+ from dataface.core.render.placeholder import (
358
+ add_placeholder_overlay,
359
+ apply_placeholder_opacity,
360
+ )
361
+
362
+ svg_result = apply_placeholder_opacity(svg_result)
363
+ svg_result = add_placeholder_overlay(
364
+ svg_result,
365
+ chart_width,
366
+ chart_height,
367
+ font=spark_config.font,
368
+ )
369
+
370
+ return svg_result
@@ -0,0 +1,154 @@
1
+ """Shared Vega-Lite spec builder helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from dataface.core.compile.config import get_vega_config
8
+ from dataface.core.render.text.case import apply_case
9
+
10
+ if TYPE_CHECKING:
11
+ from dataface.core.compile.models.style.compiled import PaddingStyle
12
+ from dataface.core.compile.models.style.merged import MergedChartsStyle
13
+
14
+
15
+ def new_chart_spec(
16
+ data: list[dict[str, Any]],
17
+ *,
18
+ mark: dict[str, Any] | None = None,
19
+ layer: bool = False,
20
+ datasets: dict[str, list[dict[str, Any]]] | None = None,
21
+ ) -> dict[str, Any]:
22
+ """Build the common base for Vega-Lite chart specs.
23
+
24
+ When ``datasets`` is provided (per-layer queries), the spec uses
25
+ top-level ``"datasets"`` instead of ``"data": {"values": ...}``.
26
+ Each layer then references its dataset via ``"data": {"name": ...}``.
27
+ """
28
+ spec: dict[str, Any] = {
29
+ "$schema": get_vega_config().schema,
30
+ "background": None,
31
+ "autosize": {"type": "fit", "contains": "padding"},
32
+ # Zero-padding initial state. The board layer replaces this via padding=
33
+ # additive_padding(card_pad, charts.padding). On direct-render paths (no board
34
+ # layer), bump_padding_bottom() can safely increment the bottom field.
35
+ "padding": {"left": 0, "right": 0, "top": 0, "bottom": 0},
36
+ }
37
+ if datasets is not None:
38
+ spec["datasets"] = datasets
39
+ else:
40
+ spec["data"] = {"values": data}
41
+ if mark is not None:
42
+ spec["mark"] = mark
43
+ if layer:
44
+ spec["layer"] = []
45
+ else:
46
+ spec["encoding"] = {}
47
+ return spec
48
+
49
+
50
+ def uniform_padding(value: float) -> dict[str, float]:
51
+ """Return a Vega-Lite padding dict with equal inset on all sides."""
52
+ return {"left": value, "right": value, "top": value, "bottom": value}
53
+
54
+
55
+ def additive_padding(card_pad: float, chart_padding: PaddingStyle) -> dict[str, float]:
56
+ """Add card_padding to per-chart padding on each side independently.
57
+
58
+ Per-chart padding (style.charts.padding) stacks ON TOP of card_padding so
59
+ author-specified insets compose with the global card layout rather than
60
+ replacing it. The theme default is {0,0,0,0}, making this a no-op for
61
+ boards that don't override per-chart padding.
62
+ """
63
+ return {
64
+ "left": card_pad + chart_padding.left,
65
+ "right": card_pad + chart_padding.right,
66
+ "top": card_pad + chart_padding.top,
67
+ "bottom": card_pad + chart_padding.bottom,
68
+ }
69
+
70
+
71
+ def bump_padding_bottom(spec: dict[str, Any], add_px: float) -> None:
72
+ """Increase spec-level `padding.bottom` by add_px in place.
73
+
74
+ Assumes the spec already carries a 4-key padding dict, which is the
75
+ invariant ``new_chart_spec`` (the only Dataface entry point that
76
+ builds chart-body specs) sets. Other sides are left alone.
77
+ """
78
+ padding = dict(spec["padding"])
79
+ padding["bottom"] = float(padding.get("bottom", 0)) + add_px
80
+ spec["padding"] = padding
81
+
82
+
83
+ def bump_padding_top(spec: dict[str, Any], add_px: float) -> None:
84
+ """Increase spec-level `padding.top` by add_px in place.
85
+
86
+ Symmetric counterpart to ``bump_padding_bottom`` — used when a strip
87
+ is attached above the plot (``style.data_table.position: top``).
88
+ """
89
+ padding = dict(spec["padding"])
90
+ padding["top"] = float(padding.get("top", 0)) + add_px
91
+ spec["padding"] = padding
92
+
93
+
94
+ def set_chart_title(
95
+ spec: dict[str, Any],
96
+ title: str | None,
97
+ subtitle: str | None = None,
98
+ *,
99
+ style: MergedChartsStyle | None = None,
100
+ ) -> None:
101
+ """Apply a standard title block when a title is present.
102
+
103
+ When *style* is provided, ``style.title.font.case`` is applied to the
104
+ title text. Subtitle is not yet case-transformed here — chart subtitles
105
+ have always been emitted raw.
106
+ """
107
+ if title and style is not None:
108
+ case = style.title.font.case
109
+ if case is not None and case != "none":
110
+ title = apply_case(title, case)
111
+ if title or subtitle:
112
+ title_block: dict[str, Any] = {"text": title or ""}
113
+ if subtitle:
114
+ title_block["subtitle"] = subtitle
115
+ spec["title"] = title_block
116
+
117
+
118
+ def set_chart_dimensions(
119
+ spec: dict[str, Any],
120
+ width: float | None = None,
121
+ height: float | None = None,
122
+ *,
123
+ width_offset: float = 0,
124
+ min_width: float | None = None,
125
+ height_offset: float = 0,
126
+ min_height: float | None = None,
127
+ ) -> None:
128
+ """Apply width and height with optional offsets and minimums."""
129
+ if width is not None and width > 0:
130
+ computed_width = width - width_offset
131
+ spec["width"] = max(computed_width, min_width) if min_width else computed_width
132
+ if height is not None and height > 0:
133
+ computed_height = height - height_offset
134
+ spec["height"] = (
135
+ max(computed_height, min_height) if min_height else computed_height
136
+ )
137
+
138
+
139
+ def tooltip_entry(
140
+ field: str,
141
+ field_type: str,
142
+ *,
143
+ title: str | None = None,
144
+ format: str | None = None,
145
+ **extra: Any,
146
+ ) -> dict[str, Any]:
147
+ """Build a tooltip field definition."""
148
+ entry: dict[str, Any] = {"field": field, "type": field_type}
149
+ if title is not None:
150
+ entry["title"] = title
151
+ if format is not None:
152
+ entry["format"] = format
153
+ entry.update(extra)
154
+ return entry