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,27 @@
1
+ """Chart rendering subpackage.
2
+
3
+ Contains all chart-type-specific renderers (KPI, table, spark bar),
4
+ Vega-Lite spec generation, chart orchestration, and auto-enrichment.
5
+ """
6
+
7
+ from dataface.core.render.chart.decisions import enrich_chart
8
+ from dataface.core.render.chart.kpi import render_kpi_svg
9
+ from dataface.core.render.chart.rendering import render_chart_item, render_layout_item
10
+ from dataface.core.render.chart.spark import render_spark
11
+ from dataface.core.render.chart.spark_bar import render_spark_bar_svg
12
+ from dataface.core.render.chart.table import render_table_svg
13
+ from dataface.core.render.chart.vega_lite import generate_vega_lite_spec, render_chart
14
+ from dataface.core.render.utils import slug_to_text
15
+
16
+ __all__ = [
17
+ "enrich_chart",
18
+ "generate_vega_lite_spec",
19
+ "render_chart",
20
+ "render_chart_item",
21
+ "render_kpi_svg",
22
+ "render_layout_item",
23
+ "render_spark",
24
+ "render_spark_bar_svg",
25
+ "render_table_svg",
26
+ "slug_to_text",
27
+ ]
@@ -0,0 +1,251 @@
1
+ """Donut/pie attached-table trigger + series extraction.
2
+
3
+ When an arc chart is too small or too crowded to carry direct labels
4
+ cleanly, the renderer swaps to a series-keyed mini-table beneath the
5
+ donut. The trigger is data-and-width aware: at the tiny tier we always
6
+ attach a table; otherwise we count the wedges that would actually carry
7
+ a label (share above WEDGE_LABEL_MIN_SHARE) and compare against a
8
+ per-tier maximum.
9
+
10
+ Decision rule (single source of truth):
11
+
12
+ visible = sum(share > 0.08 for share in shares)
13
+
14
+ if tier == "tiny": attach table
15
+ elif visible == 0: attach table (no labels would render)
16
+ elif visible > tier_max: attach table (too many to fit cleanly)
17
+ else: direct labels with small wedges suppressed
18
+
19
+ Constants are inline here — promote to theme cascade if a future ask
20
+ demands per-theme tuning. The current numbers were locked against visual
21
+ spike evidence (5-series narrow renders cleanly, 8 equal-mass series at
22
+ wide overflow).
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Any
28
+
29
+ from dataface.core.compile.models.chart.authored import TableColumnConfig
30
+ from dataface.core.compile.typography import width_tier
31
+
32
+ # Wedges with share at or below this value get no direct label — the
33
+ # wedge still renders, hover tooltip carries the value. 8% is the
34
+ # observed danger floor: 5-6% wedges next to each other vertically
35
+ # stack their labels at near-cardinal angles; 8% gives an honest buffer.
36
+ WEDGE_LABEL_MIN_SHARE: float = 0.08
37
+
38
+ # Per-tier ceiling on how many direct-label callouts fit comfortably
39
+ # around the disk. Tiny is absent because tiny always uses the table.
40
+ _TIER_MAX_VISIBLE_CALLOUTS: dict[str, int] = {
41
+ "narrow": 3,
42
+ "medium": 4,
43
+ "wide": 5,
44
+ }
45
+
46
+
47
+ def should_attach_table(width: float, shares: list[float]) -> bool:
48
+ """Return True when the arc chart should render as donut + attached table.
49
+
50
+ Args:
51
+ width: Final card pixel width the donut will be rendered into.
52
+ shares: Per-wedge fractional shares (each in [0, 1], normally summing
53
+ to 1.0). Order doesn't matter — only the count of visible
54
+ callouts matters for the decision.
55
+
56
+ Returns:
57
+ True → render donut SVG with no direct labels, plus a swatch-keyed
58
+ table beneath it. False → render donut SVG with direct labels for
59
+ wedges whose share exceeds ``WEDGE_LABEL_MIN_SHARE``.
60
+
61
+ Empty ``shares`` (data is empty or all-zero) returns False — that's
62
+ the placeholder case, handled by the standard renderer's existing
63
+ empty-data path, not by attaching a table.
64
+ """
65
+ if not shares:
66
+ return False
67
+ tier = width_tier(width)
68
+ if tier == "tiny":
69
+ return True
70
+ visible = sum(1 for s in shares if s > WEDGE_LABEL_MIN_SHARE)
71
+ if visible == 0:
72
+ return True
73
+ return visible > _TIER_MAX_VISIBLE_CALLOUTS[tier]
74
+
75
+
76
+ # Pixel gap between donut SVG and attached table SVG. Tuned visually;
77
+ # theme-cascadable if any future caller needs to vary it.
78
+ _DONUT_TABLE_GAP_PX: float = 12.0
79
+
80
+
81
+ def compute_shares(theta_field: str, data: list[dict[str, Any]]) -> list[float]:
82
+ """Compute per-row fractional shares from theta field values.
83
+
84
+ Returns shares as floats in [0, 1]. Returns an empty list when data
85
+ is empty, total is zero, or any row's theta value can't be coerced
86
+ to a number — callers treat all three cases as "can't decide
87
+ direct-vs-table from data alone" and route to the standard renderer
88
+ (which has its own placeholder + type-coercion handling).
89
+
90
+ Mirrors ``_augment_arc_label_data``'s tolerance for the CSV adapter
91
+ string-numeric shapes ("1,234"): runs ``normalize_data_types`` so a
92
+ row that the standard renderer would accept doesn't crash the
93
+ dispatcher on `float(...)`.
94
+ """
95
+ if not data:
96
+ return []
97
+ from dataface.core.render.utils import normalize_data_types
98
+
99
+ normalized = normalize_data_types(data)
100
+ try:
101
+ values = [float(row.get(theta_field, 0) or 0) for row in normalized]
102
+ except (TypeError, ValueError):
103
+ return []
104
+ total = sum(values)
105
+ if total <= 0:
106
+ return []
107
+ return [v / total for v in values]
108
+
109
+
110
+ def compose_attached_table_svg(
111
+ donut_svg: str,
112
+ table_svg: str,
113
+ donut_width: float,
114
+ donut_height: float,
115
+ table_width: float,
116
+ table_height: float,
117
+ gap: float = _DONUT_TABLE_GAP_PX,
118
+ ) -> tuple[str, float, float]:
119
+ """Stack a donut SVG above a table SVG into a single outer SVG.
120
+
121
+ Returns ``(outer_svg, outer_width, outer_height)``. Outer width is
122
+ ``max(donut_width, table_width)``; outer height = donut + gap + table.
123
+ The donut sits at top-left (y=0); the table is **horizontally
124
+ centered** beneath it. Centering is the right default for the
125
+ donut-attached use case — the table is meant to read as a legend
126
+ beneath the donut, not as a full-width data block.
127
+ """
128
+ outer_w = max(donut_width, table_width)
129
+ outer_h = donut_height + gap + table_height
130
+ table_y = donut_height + gap
131
+ table_x = (outer_w - table_width) / 2 # centered under the donut
132
+ donut_x = (outer_w - donut_width) / 2 # also center the donut if narrower
133
+ return (
134
+ f'<svg xmlns="http://www.w3.org/2000/svg" '
135
+ f'width="{outer_w}" height="{outer_h}" '
136
+ f'viewBox="0 0 {outer_w} {outer_h}">'
137
+ f'<g transform="translate({donut_x}, 0)">{donut_svg}</g>'
138
+ f'<g transform="translate({table_x}, {table_y})">{table_svg}</g>'
139
+ f"</svg>",
140
+ outer_w,
141
+ outer_h,
142
+ )
143
+
144
+
145
+ def _format_value_for_width(value: Any, value_format: Any) -> str:
146
+ """Render a value to the string the table will display so we can
147
+ measure its pixel width.
148
+
149
+ ``value_format`` is whatever the caller pulled off the chart:
150
+ ``None``, a d3-format string, or a ``FormatConfig`` (which may carry
151
+ ``prefix`` / ``suffix`` on top of a ``spec``). Width measurement
152
+ only needs an upper-bound string; the table renderer formats the
153
+ actual emitted text. Returns the empty string for None / unparseable
154
+ values (zero contribution to column width).
155
+ """
156
+ from dataface.core.compile.models.primitives import FormatConfig
157
+
158
+ if value is None:
159
+ return ""
160
+ try:
161
+ v = float(value)
162
+ except (TypeError, ValueError):
163
+ return str(value)
164
+
165
+ spec: str | None
166
+ prefix = ""
167
+ suffix = ""
168
+ if isinstance(value_format, FormatConfig):
169
+ spec = value_format.spec
170
+ prefix = value_format.prefix or ""
171
+ suffix = value_format.suffix or ""
172
+ elif isinstance(value_format, str):
173
+ spec = value_format
174
+ else:
175
+ spec = None
176
+
177
+ # Mirror a few common d3 specs for width approximation. Unknown
178
+ # specs fall back to integer-with-commas; the actual rendered text
179
+ # is the table renderer's job.
180
+ if spec == "$,.0f":
181
+ body = f"${v:,.0f}"
182
+ elif spec == ",.0f" or spec is None:
183
+ body = f"{v:,.0f}"
184
+ elif spec == ".0%":
185
+ body = f"{int(round(v * 100))}%"
186
+ else:
187
+ body = f"{v:,.0f}"
188
+ return f"{prefix}{body}{suffix}"
189
+
190
+
191
+ def build_attached_table_columns(
192
+ rows: list[dict[str, Any]],
193
+ *,
194
+ value_format: Any,
195
+ font_size: float,
196
+ font_family: str,
197
+ cell_pad: int = 6,
198
+ ) -> tuple[dict[str, TableColumnConfig], float]:
199
+ """Build per-column ``TableColumnConfig`` objects for the attached table
200
+ plus the total table width (sum of column widths).
201
+
202
+ Sizes each column via the same font measurer the table renderer
203
+ uses, threaded the same ``font_size`` and ``font_family`` the table
204
+ will paint with. Caller pulls both off the resolved table style
205
+ (``tc.font.size`` / ``tc.font.family``) so a theme that retunes
206
+ table type doesn't desync width-sizing from rendering.
207
+
208
+ ``value_format`` is the chart's format hint (``str``, ``FormatConfig``,
209
+ or ``None``); accepted as-is and passed through to
210
+ ``TableColumnConfig.format``.
211
+ """
212
+ from dataface.core.render.font_measurement import get_font_measurer
213
+
214
+ if not rows:
215
+ # Empty case shouldn't actually fire (caller's trigger gate
216
+ # rejects empty-shares before we get here) but return a sane
217
+ # default so misuse fails loudly rather than panics.
218
+ return ({}, 0.0)
219
+
220
+ measurer = get_font_measurer(font_family)
221
+
222
+ def _col_width(strings: list[str], floor: int) -> int:
223
+ widest = max(
224
+ (measurer.measure(s, font_size) for s in strings),
225
+ default=0,
226
+ )
227
+ # 3px buffer caps measurer rounding so a value that measures
228
+ # 25.6px doesn't end up in a 25px column that would clip it.
229
+ return max(floor, int(widest) + cell_pad * 2 + 3)
230
+
231
+ swatch_w = 24
232
+ share_strings = [str(r.get("share", "")) for r in rows]
233
+ share_w = _col_width(share_strings, floor=36)
234
+ name_strings = [str(r.get("name", "")) for r in rows]
235
+ name_w = _col_width(name_strings, floor=60)
236
+ value_strings = [
237
+ _format_value_for_width(r.get("value"), value_format) for r in rows
238
+ ]
239
+ value_w = _col_width(value_strings, floor=50)
240
+
241
+ columns = {
242
+ "swatch": TableColumnConfig(swatch=True, width=swatch_w),
243
+ "share": TableColumnConfig(align="right", width=share_w),
244
+ "name": TableColumnConfig(width=name_w),
245
+ "value": TableColumnConfig(
246
+ format=value_format,
247
+ align="right",
248
+ width=value_w,
249
+ ),
250
+ }
251
+ return columns, float(swatch_w + share_w + name_w + value_w)
@@ -0,0 +1,16 @@
1
+ """Render output types for the chart render pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Literal
7
+
8
+ ArtifactKind = Literal["vega_spec", "svg", "json"]
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class RenderArtifact:
13
+ """Chart-domain render output before transport conversion."""
14
+
15
+ kind: ArtifactKind
16
+ payload: dict[str, Any] | str
@@ -0,0 +1,225 @@
1
+ """Wrapped SVG renderer for callout charts (type: callout)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html
6
+
7
+ from dataface.core.compile.chart_resolved import ResolvedChart
8
+ from dataface.core.compile.config import get_config
9
+ from dataface.core.compile.models.style.merged import (
10
+ MergedStyle,
11
+ resolve_style,
12
+ )
13
+ from dataface.core.compile.palette import color as resolve_palette_color
14
+ from dataface.core.render.font_measurement import get_font_measurer
15
+ from mdsvg.fonts import wrap_text_precise
16
+
17
+ _MIN_HEIGHT = 60.0
18
+ _LINE_HEIGHT_RATIO = 1.35
19
+ # Keep cards readable without letting a long stack trace consume the full board.
20
+ _MAX_MESSAGE_LINES = 12
21
+
22
+
23
+ def render_callout_svg(
24
+ *,
25
+ chart_id: str,
26
+ message: str,
27
+ width: float,
28
+ height: float | None = None,
29
+ title: str | None = None,
30
+ resolved_style: MergedStyle | None = None,
31
+ code: str | None = None,
32
+ hint: str | None = None,
33
+ doc_url: str | None = None,
34
+ tone: str = "negative",
35
+ ) -> str:
36
+ """Render a wrapped inline callout card as SVG.
37
+
38
+ Colors are resolved from ``{tone}.*`` palette roles (bg, border, solid,
39
+ text) at render time so each tone gets the correct theme-defined semantic
40
+ color without storing per-tone style copies.
41
+ """
42
+ style = resolved_style or resolve_style(get_config().style)
43
+ inline = style.charts.callout
44
+ padding = inline.padding
45
+ section_gap = inline.section_gap
46
+
47
+ background = resolve_palette_color(f"{tone}.bg")
48
+ border_color = resolve_palette_color(f"{tone}.border")
49
+ title_color = resolve_palette_color(f"{tone}.solid")
50
+ message_color = resolve_palette_color(f"{tone}.text")
51
+
52
+ w = max(width, 120.0)
53
+ content_width = max(w - (2 * padding), 40.0)
54
+
55
+ title_rf = inline.title.font # MergedFontStyle — all fields concrete
56
+ message_rf = inline.message.font
57
+ title_text = title or f"Callout: {chart_id}"
58
+
59
+ title_measurer = get_font_measurer(title_rf.family)
60
+ message_measurer = get_font_measurer(message_rf.family)
61
+ title_lines = wrap_text_precise(
62
+ title_text,
63
+ content_width,
64
+ title_rf.size,
65
+ title_measurer,
66
+ max_lines=3,
67
+ ellipsis=True,
68
+ ) or [title_text]
69
+ message_lines = wrap_text_precise(
70
+ message,
71
+ content_width,
72
+ message_rf.size,
73
+ message_measurer,
74
+ max_lines=_MAX_MESSAGE_LINES,
75
+ ellipsis=True,
76
+ ) or [message]
77
+
78
+ # Optional extra lines: hint and doc_url
79
+ hint_lines: list[str] = []
80
+ if hint:
81
+ hint_lines = wrap_text_precise(
82
+ hint,
83
+ content_width,
84
+ message_rf.size,
85
+ message_measurer,
86
+ max_lines=3,
87
+ ellipsis=True,
88
+ ) or [hint]
89
+
90
+ doc_url_lines: list[str] = []
91
+ if doc_url:
92
+ doc_url_lines = wrap_text_precise(
93
+ doc_url,
94
+ content_width,
95
+ message_rf.size * 0.85,
96
+ message_measurer,
97
+ max_lines=4,
98
+ ellipsis=True,
99
+ ) or [doc_url]
100
+
101
+ title_line_height = title_rf.size * _LINE_HEIGHT_RATIO
102
+ message_line_height = message_rf.size * _LINE_HEIGHT_RATIO
103
+ doc_url_line_height = message_rf.size * 0.85 * _LINE_HEIGHT_RATIO
104
+
105
+ # Code badge height (small monospace text above title)
106
+ code_badge_height = (title_rf.size * 0.75 * _LINE_HEIGHT_RATIO) if code else 0.0
107
+ title_block_height = len(title_lines) * title_line_height
108
+ message_block_height = len(message_lines) * message_line_height
109
+ hint_block_height = len(hint_lines) * message_line_height if hint_lines else 0.0
110
+ doc_url_block_height = (
111
+ len(doc_url_lines) * doc_url_line_height if doc_url_lines else 0.0
112
+ )
113
+
114
+ natural_height = (
115
+ (2 * padding)
116
+ + code_badge_height
117
+ + (section_gap if code else 0.0)
118
+ + title_block_height
119
+ + section_gap
120
+ + message_block_height
121
+ + (section_gap + hint_block_height if hint_lines else 0.0)
122
+ + (section_gap + doc_url_block_height if doc_url_lines else 0.0)
123
+ )
124
+ h = max(_MIN_HEIGHT, height or 0.0, natural_height)
125
+
126
+ current_y = padding
127
+
128
+ # Code badge
129
+ code_svg = ""
130
+ if code:
131
+ code_font_size = title_rf.size * 0.75
132
+ code_y = current_y + code_font_size
133
+ code_svg = (
134
+ f'<text x="{padding}" y="{code_y}" '
135
+ f'font-size="{code_font_size}" font-weight="normal" '
136
+ f'font-family="monospace" '
137
+ f'fill="{message_color}">{html.escape(code)}</text>'
138
+ )
139
+ current_y += code_badge_height + section_gap
140
+
141
+ title_start_y = current_y + title_rf.size + inline.title.y_offset
142
+ current_y = (
143
+ title_start_y
144
+ + title_block_height
145
+ - title_rf.size
146
+ + section_gap
147
+ + inline.message.y_offset
148
+ )
149
+ message_start_y = current_y
150
+
151
+ title_lines_svg = "".join(
152
+ f'<text x="{padding}" y="{title_start_y + (index * title_line_height)}" '
153
+ f'font-size="{title_rf.size}" font-weight="{title_rf.weight}" '
154
+ f'font-family="{html.escape(title_rf.family)}" '
155
+ f'fill="{title_color}">{html.escape(line)}</text>'
156
+ for index, line in enumerate(title_lines)
157
+ )
158
+ message_lines_svg = "".join(
159
+ f'<text x="{padding}" y="{message_start_y + (index * message_line_height)}" '
160
+ f'font-size="{message_rf.size}" font-weight="{message_rf.weight}" '
161
+ f'font-family="{html.escape(message_rf.family)}" '
162
+ f'fill="{message_color}">{html.escape(line)}</text>'
163
+ for index, line in enumerate(message_lines)
164
+ )
165
+
166
+ hint_svg = ""
167
+ if hint_lines:
168
+ hint_start_y = message_start_y + message_block_height + section_gap
169
+ hint_svg = "".join(
170
+ f'<text x="{padding}" y="{hint_start_y + (i * message_line_height)}" '
171
+ f'font-size="{message_rf.size}" font-weight="normal" font-style="italic" '
172
+ f'font-family="{html.escape(message_rf.family)}" '
173
+ f'fill="{message_color}">{html.escape(line)}</text>'
174
+ for i, line in enumerate(hint_lines)
175
+ )
176
+
177
+ doc_svg = ""
178
+ if doc_url_lines:
179
+ doc_url_font_size = message_rf.size * 0.85
180
+ prev_height = message_block_height + (
181
+ section_gap + hint_block_height if hint_lines else 0.0
182
+ )
183
+ doc_start_y = message_start_y + prev_height + section_gap
184
+ doc_svg = "".join(
185
+ f'<text x="{padding}" y="{doc_start_y + (i * doc_url_line_height)}" '
186
+ f'font-size="{doc_url_font_size}" font-weight="normal" '
187
+ f'font-family="{html.escape(message_rf.family)}" '
188
+ f'fill="{message_color}">{html.escape(line)}</text>'
189
+ for i, line in enumerate(doc_url_lines)
190
+ )
191
+
192
+ return (
193
+ f'<svg xmlns="http://www.w3.org/2000/svg" class="dft-chart-callout" width="{w}" height="{h}" viewBox="0 0 {w} {h}">'
194
+ f'<rect x="0" y="0" width="{w}" height="{h}" '
195
+ f'fill="{background}" stroke="{border_color}" '
196
+ f'stroke-width="{inline.border.width}" rx="{inline.border.radius}"/>'
197
+ f"{code_svg}{title_lines_svg}{message_lines_svg}{hint_svg}{doc_svg}</svg>"
198
+ )
199
+
200
+
201
+ def render_callout_chart_svg(
202
+ chart: ResolvedChart,
203
+ data: list[dict[str, object]],
204
+ width: float | None = None,
205
+ height: float | None = None,
206
+ is_placeholder: bool = False,
207
+ *,
208
+ resolved_style: MergedStyle | None = None,
209
+ face_level: int = 1,
210
+ ) -> str:
211
+ """Render an authored ``type: callout`` chart."""
212
+ _ = data, is_placeholder, face_level
213
+ chart_id = chart.id
214
+ tone = chart.resolved_style.callout.tone or "negative"
215
+ title = chart.title or f"Callout: {chart_id}"
216
+ assert chart.message, "callout charts require a message"
217
+ return render_callout_svg(
218
+ chart_id=chart_id,
219
+ message=chart.message,
220
+ width=width or 320.0,
221
+ height=height,
222
+ title=title,
223
+ resolved_style=resolved_style,
224
+ tone=tone,
225
+ )