@joewinke/jatui 0.36.3 → 0.38.0

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 (271) hide show
  1. package/dist/artifact-blocks/AttachmentChip.svelte +66 -0
  2. package/dist/artifact-blocks/AttachmentChip.svelte.d.ts +7 -0
  3. package/dist/artifact-blocks/BlockComments.svelte +309 -0
  4. package/dist/artifact-blocks/BlockComments.svelte.d.ts +33 -0
  5. package/dist/artifact-blocks/BlockRenderer.svelte +674 -0
  6. package/dist/artifact-blocks/BlockRenderer.svelte.d.ts +55 -0
  7. package/dist/artifact-blocks/BlockVerdict.svelte +151 -0
  8. package/dist/artifact-blocks/BlockVerdict.svelte.d.ts +13 -0
  9. package/dist/artifact-blocks/BlocksArtifactView.svelte +1616 -0
  10. package/dist/artifact-blocks/BlocksArtifactView.svelte.d.ts +113 -0
  11. package/dist/artifact-blocks/CONTRACT.md +225 -0
  12. package/dist/artifact-blocks/CommentPill.svelte +49 -0
  13. package/dist/artifact-blocks/CommentPill.svelte.d.ts +16 -0
  14. package/dist/artifact-blocks/FoldedRounds.svelte +95 -0
  15. package/dist/artifact-blocks/FoldedRounds.svelte.d.ts +9 -0
  16. package/dist/artifact-blocks/Glyph.svelte +54 -0
  17. package/dist/artifact-blocks/Glyph.svelte.d.ts +10 -0
  18. package/dist/artifact-blocks/HistoryScrubber.svelte +500 -0
  19. package/dist/artifact-blocks/HistoryScrubber.svelte.d.ts +15 -0
  20. package/dist/artifact-blocks/Markdown.svelte +186 -0
  21. package/dist/artifact-blocks/Markdown.svelte.d.ts +30 -0
  22. package/dist/artifact-blocks/NoteComposer.svelte +31 -0
  23. package/dist/artifact-blocks/NoteComposer.svelte.d.ts +8 -0
  24. package/dist/artifact-blocks/PlainComposer.svelte +200 -0
  25. package/dist/artifact-blocks/PlainComposer.svelte.d.ts +15 -0
  26. package/dist/artifact-blocks/SectionTools.svelte +102 -0
  27. package/dist/artifact-blocks/SectionTools.svelte.d.ts +19 -0
  28. package/dist/artifact-blocks/adapter.d.ts +13 -0
  29. package/dist/artifact-blocks/adapter.js +31 -0
  30. package/dist/artifact-blocks/autoSend.d.ts +49 -0
  31. package/dist/artifact-blocks/autoSend.js +103 -0
  32. package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte +557 -0
  33. package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte.d.ts +48 -0
  34. package/dist/artifact-blocks/blocks/actions/block.d.ts +8 -0
  35. package/dist/artifact-blocks/blocks/actions/block.js +74 -0
  36. package/dist/artifact-blocks/blocks/actions/requests.d.ts +62 -0
  37. package/dist/artifact-blocks/blocks/actions/requests.js +26 -0
  38. package/dist/artifact-blocks/blocks/actions/schema.d.ts +168 -0
  39. package/dist/artifact-blocks/blocks/actions/schema.js +253 -0
  40. package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte +779 -0
  41. package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte.d.ts +40 -0
  42. package/dist/artifact-blocks/blocks/callout/block.d.ts +7 -0
  43. package/dist/artifact-blocks/blocks/callout/block.js +39 -0
  44. package/dist/artifact-blocks/blocks/callout/schema.d.ts +72 -0
  45. package/dist/artifact-blocks/blocks/callout/schema.js +115 -0
  46. package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte +875 -0
  47. package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte.d.ts +59 -0
  48. package/dist/artifact-blocks/blocks/chart/block.d.ts +9 -0
  49. package/dist/artifact-blocks/blocks/chart/block.js +70 -0
  50. package/dist/artifact-blocks/blocks/chart/schema.d.ts +171 -0
  51. package/dist/artifact-blocks/blocks/chart/schema.js +498 -0
  52. package/dist/artifact-blocks/blocks/chart/thumb.d.ts +2 -0
  53. package/dist/artifact-blocks/blocks/chart/thumb.js +201 -0
  54. package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte +1048 -0
  55. package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte.d.ts +16 -0
  56. package/dist/artifact-blocks/blocks/checklist/Icon.svelte +105 -0
  57. package/dist/artifact-blocks/blocks/checklist/Icon.svelte.d.ts +17 -0
  58. package/dist/artifact-blocks/blocks/checklist/block.d.ts +10 -0
  59. package/dist/artifact-blocks/blocks/checklist/block.js +63 -0
  60. package/dist/artifact-blocks/blocks/checklist/itemReview.d.ts +16 -0
  61. package/dist/artifact-blocks/blocks/checklist/itemReview.js +63 -0
  62. package/dist/artifact-blocks/blocks/checklist/labels.d.ts +62 -0
  63. package/dist/artifact-blocks/blocks/checklist/labels.js +96 -0
  64. package/dist/artifact-blocks/blocks/checklist/retest.d.ts +43 -0
  65. package/dist/artifact-blocks/blocks/checklist/retest.js +68 -0
  66. package/dist/artifact-blocks/blocks/checklist/schema.d.ts +84 -0
  67. package/dist/artifact-blocks/blocks/checklist/schema.js +91 -0
  68. package/dist/artifact-blocks/blocks/code/CodeBlock.svelte +128 -0
  69. package/dist/artifact-blocks/blocks/code/CodeBlock.svelte.d.ts +12 -0
  70. package/dist/artifact-blocks/blocks/code/block.d.ts +8 -0
  71. package/dist/artifact-blocks/blocks/code/block.js +31 -0
  72. package/dist/artifact-blocks/blocks/code/highlight.d.ts +34 -0
  73. package/dist/artifact-blocks/blocks/code/highlight.js +280 -0
  74. package/dist/artifact-blocks/blocks/code/schema.d.ts +32 -0
  75. package/dist/artifact-blocks/blocks/code/schema.js +39 -0
  76. package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte +290 -0
  77. package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte.d.ts +29 -0
  78. package/dist/artifact-blocks/blocks/collage/block.d.ts +9 -0
  79. package/dist/artifact-blocks/blocks/collage/block.js +29 -0
  80. package/dist/artifact-blocks/blocks/collage/schema.d.ts +52 -0
  81. package/dist/artifact-blocks/blocks/collage/schema.js +86 -0
  82. package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte +484 -0
  83. package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte.d.ts +25 -0
  84. package/dist/artifact-blocks/blocks/compare/block.d.ts +9 -0
  85. package/dist/artifact-blocks/blocks/compare/block.js +32 -0
  86. package/dist/artifact-blocks/blocks/compare/schema.d.ts +74 -0
  87. package/dist/artifact-blocks/blocks/compare/schema.js +148 -0
  88. package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte +392 -0
  89. package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +30 -0
  90. package/dist/artifact-blocks/blocks/diagram/block.d.ts +9 -0
  91. package/dist/artifact-blocks/blocks/diagram/block.js +50 -0
  92. package/dist/artifact-blocks/blocks/diagram/constants.d.ts +48 -0
  93. package/dist/artifact-blocks/blocks/diagram/constants.js +53 -0
  94. package/dist/artifact-blocks/blocks/diagram/schema.d.ts +161 -0
  95. package/dist/artifact-blocks/blocks/diagram/schema.js +621 -0
  96. package/dist/artifact-blocks/blocks/diagram/thumb.d.ts +2 -0
  97. package/dist/artifact-blocks/blocks/diagram/thumb.js +157 -0
  98. package/dist/artifact-blocks/blocks/diagram/types.d.ts +74 -0
  99. package/dist/artifact-blocks/blocks/diagram/types.js +1 -0
  100. package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte +81 -0
  101. package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte.d.ts +10 -0
  102. package/dist/artifact-blocks/blocks/diff/block.d.ts +7 -0
  103. package/dist/artifact-blocks/blocks/diff/block.js +36 -0
  104. package/dist/artifact-blocks/blocks/diff/schema.d.ts +18 -0
  105. package/dist/artifact-blocks/blocks/diff/schema.js +35 -0
  106. package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte +1055 -0
  107. package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte.d.ts +5 -0
  108. package/dist/artifact-blocks/blocks/diff-review/block.d.ts +15 -0
  109. package/dist/artifact-blocks/blocks/diff-review/block.js +64 -0
  110. package/dist/artifact-blocks/blocks/diff-review/decisions.d.ts +57 -0
  111. package/dist/artifact-blocks/blocks/diff-review/decisions.js +80 -0
  112. package/dist/artifact-blocks/blocks/diff-review/schema.d.ts +13 -0
  113. package/dist/artifact-blocks/blocks/diff-review/schema.js +84 -0
  114. package/dist/artifact-blocks/blocks/diff-review/types.d.ts +35 -0
  115. package/dist/artifact-blocks/blocks/diff-review/types.js +1 -0
  116. package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte +145 -0
  117. package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte.d.ts +5 -0
  118. package/dist/artifact-blocks/blocks/explainer/block.d.ts +8 -0
  119. package/dist/artifact-blocks/blocks/explainer/block.js +21 -0
  120. package/dist/artifact-blocks/blocks/explainer/schema.d.ts +26 -0
  121. package/dist/artifact-blocks/blocks/explainer/schema.js +69 -0
  122. package/dist/artifact-blocks/blocks/group-content.d.ts +10 -0
  123. package/dist/artifact-blocks/blocks/group-content.js +16 -0
  124. package/dist/artifact-blocks/blocks/group-data.d.ts +10 -0
  125. package/dist/artifact-blocks/blocks/group-data.js +7 -0
  126. package/dist/artifact-blocks/blocks/group-review.d.ts +10 -0
  127. package/dist/artifact-blocks/blocks/group-review.js +12 -0
  128. package/dist/artifact-blocks/blocks/group-visual.d.ts +17 -0
  129. package/dist/artifact-blocks/blocks/group-visual.js +14 -0
  130. package/dist/artifact-blocks/blocks/map/MapBlock.svelte +413 -0
  131. package/dist/artifact-blocks/blocks/map/MapBlock.svelte.d.ts +53 -0
  132. package/dist/artifact-blocks/blocks/map/block.d.ts +9 -0
  133. package/dist/artifact-blocks/blocks/map/block.js +62 -0
  134. package/dist/artifact-blocks/blocks/map/schema.d.ts +123 -0
  135. package/dist/artifact-blocks/blocks/map/schema.js +113 -0
  136. package/dist/artifact-blocks/blocks/map/thumb.d.ts +2 -0
  137. package/dist/artifact-blocks/blocks/map/thumb.js +69 -0
  138. package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte +33 -0
  139. package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte.d.ts +15 -0
  140. package/dist/artifact-blocks/blocks/markdown/block.d.ts +7 -0
  141. package/dist/artifact-blocks/blocks/markdown/block.js +28 -0
  142. package/dist/artifact-blocks/blocks/markdown/render.d.ts +17 -0
  143. package/dist/artifact-blocks/blocks/markdown/render.js +115 -0
  144. package/dist/artifact-blocks/blocks/markdown/schema.d.ts +10 -0
  145. package/dist/artifact-blocks/blocks/markdown/schema.js +23 -0
  146. package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte +2112 -0
  147. package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte.d.ts +5 -0
  148. package/dist/artifact-blocks/blocks/media-picker/block.d.ts +24 -0
  149. package/dist/artifact-blocks/blocks/media-picker/block.js +137 -0
  150. package/dist/artifact-blocks/blocks/media-picker/choices.d.ts +28 -0
  151. package/dist/artifact-blocks/blocks/media-picker/choices.js +35 -0
  152. package/dist/artifact-blocks/blocks/media-picker/decisions.d.ts +51 -0
  153. package/dist/artifact-blocks/blocks/media-picker/decisions.js +75 -0
  154. package/dist/artifact-blocks/blocks/media-picker/schema.d.ts +7 -0
  155. package/dist/artifact-blocks/blocks/media-picker/schema.js +199 -0
  156. package/dist/artifact-blocks/blocks/media-picker/types.d.ts +92 -0
  157. package/dist/artifact-blocks/blocks/media-picker/types.js +1 -0
  158. package/dist/artifact-blocks/blocks/media-picker/versions.d.ts +51 -0
  159. package/dist/artifact-blocks/blocks/media-picker/versions.js +118 -0
  160. package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte +213 -0
  161. package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte.d.ts +5 -0
  162. package/dist/artifact-blocks/blocks/mockup/block.d.ts +26 -0
  163. package/dist/artifact-blocks/blocks/mockup/block.js +52 -0
  164. package/dist/artifact-blocks/blocks/mockup/doc.d.ts +24 -0
  165. package/dist/artifact-blocks/blocks/mockup/doc.js +44 -0
  166. package/dist/artifact-blocks/blocks/mockup/sandbox.d.ts +21 -0
  167. package/dist/artifact-blocks/blocks/mockup/sandbox.js +27 -0
  168. package/dist/artifact-blocks/blocks/mockup/schema.d.ts +7 -0
  169. package/dist/artifact-blocks/blocks/mockup/schema.js +90 -0
  170. package/dist/artifact-blocks/blocks/mockup/types.d.ts +36 -0
  171. package/dist/artifact-blocks/blocks/mockup/types.js +17 -0
  172. package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte +359 -0
  173. package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte.d.ts +32 -0
  174. package/dist/artifact-blocks/blocks/program/block.d.ts +35 -0
  175. package/dist/artifact-blocks/blocks/program/block.js +68 -0
  176. package/dist/artifact-blocks/blocks/program/schema.d.ts +132 -0
  177. package/dist/artifact-blocks/blocks/program/schema.js +268 -0
  178. package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte +315 -0
  179. package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte.d.ts +24 -0
  180. package/dist/artifact-blocks/blocks/stats/block.d.ts +7 -0
  181. package/dist/artifact-blocks/blocks/stats/block.js +49 -0
  182. package/dist/artifact-blocks/blocks/stats/schema.d.ts +82 -0
  183. package/dist/artifact-blocks/blocks/stats/schema.js +43 -0
  184. package/dist/artifact-blocks/blocks/stats/thumb.d.ts +2 -0
  185. package/dist/artifact-blocks/blocks/stats/thumb.js +102 -0
  186. package/dist/artifact-blocks/blocks/table/TableBlock.svelte +490 -0
  187. package/dist/artifact-blocks/blocks/table/TableBlock.svelte.d.ts +28 -0
  188. package/dist/artifact-blocks/blocks/table/block.d.ts +9 -0
  189. package/dist/artifact-blocks/blocks/table/block.js +100 -0
  190. package/dist/artifact-blocks/blocks/table/schema.d.ts +143 -0
  191. package/dist/artifact-blocks/blocks/table/schema.js +278 -0
  192. package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte +347 -0
  193. package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte.d.ts +42 -0
  194. package/dist/artifact-blocks/blocks/timeline/block.d.ts +9 -0
  195. package/dist/artifact-blocks/blocks/timeline/block.js +96 -0
  196. package/dist/artifact-blocks/blocks/timeline/constants.d.ts +26 -0
  197. package/dist/artifact-blocks/blocks/timeline/constants.js +27 -0
  198. package/dist/artifact-blocks/blocks/timeline/schema.d.ts +102 -0
  199. package/dist/artifact-blocks/blocks/timeline/schema.js +405 -0
  200. package/dist/artifact-blocks/blocks/timeline/thumb.d.ts +2 -0
  201. package/dist/artifact-blocks/blocks/timeline/thumb.js +117 -0
  202. package/dist/artifact-blocks/blocks/timeline/types.d.ts +66 -0
  203. package/dist/artifact-blocks/blocks/timeline/types.js +1 -0
  204. package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte +169 -0
  205. package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte.d.ts +26 -0
  206. package/dist/artifact-blocks/blocks/transcript/block.d.ts +9 -0
  207. package/dist/artifact-blocks/blocks/transcript/block.js +86 -0
  208. package/dist/artifact-blocks/blocks/transcript/schema.d.ts +114 -0
  209. package/dist/artifact-blocks/blocks/transcript/schema.js +126 -0
  210. package/dist/artifact-blocks/builtins.d.ts +40 -0
  211. package/dist/artifact-blocks/builtins.js +22 -0
  212. package/dist/artifact-blocks/contractBlocks.d.ts +92 -0
  213. package/dist/artifact-blocks/contractBlocks.js +279 -0
  214. package/dist/artifact-blocks/history.d.ts +140 -0
  215. package/dist/artifact-blocks/history.js +363 -0
  216. package/dist/artifact-blocks/index.d.ts +40 -0
  217. package/dist/artifact-blocks/index.js +46 -0
  218. package/dist/artifact-blocks/listen/context.d.ts +30 -0
  219. package/dist/artifact-blocks/listen/context.js +30 -0
  220. package/dist/artifact-blocks/liveData.d.ts +78 -0
  221. package/dist/artifact-blocks/liveData.js +137 -0
  222. package/dist/artifact-blocks/liveDataPoller.d.ts +41 -0
  223. package/dist/artifact-blocks/liveDataPoller.js +140 -0
  224. package/dist/artifact-blocks/openNav.d.ts +46 -0
  225. package/dist/artifact-blocks/openNav.js +106 -0
  226. package/dist/artifact-blocks/pageReviewKeys.d.ts +17 -0
  227. package/dist/artifact-blocks/pageReviewKeys.js +115 -0
  228. package/dist/artifact-blocks/params.d.ts +98 -0
  229. package/dist/artifact-blocks/params.js +159 -0
  230. package/dist/artifact-blocks/registry.d.ts +27 -0
  231. package/dist/artifact-blocks/registry.js +83 -0
  232. package/dist/artifact-blocks/review/BlockIcon.svelte +99 -0
  233. package/dist/artifact-blocks/review/BlockIcon.svelte.d.ts +17 -0
  234. package/dist/artifact-blocks/review/BulkRejectForm.svelte +94 -0
  235. package/dist/artifact-blocks/review/BulkRejectForm.svelte.d.ts +11 -0
  236. package/dist/artifact-blocks/review/BulkUndoBar.svelte +40 -0
  237. package/dist/artifact-blocks/review/BulkUndoBar.svelte.d.ts +9 -0
  238. package/dist/artifact-blocks/review/ItemNote.svelte +193 -0
  239. package/dist/artifact-blocks/review/ItemNote.svelte.d.ts +33 -0
  240. package/dist/artifact-blocks/review/NoteToggle.svelte +52 -0
  241. package/dist/artifact-blocks/review/NoteToggle.svelte.d.ts +12 -0
  242. package/dist/artifact-blocks/review/TeamChip.svelte +71 -0
  243. package/dist/artifact-blocks/review/TeamChip.svelte.d.ts +8 -0
  244. package/dist/artifact-blocks/review/attachments.d.ts +10 -0
  245. package/dist/artifact-blocks/review/attachments.js +37 -0
  246. package/dist/artifact-blocks/review/bulk.d.ts +52 -0
  247. package/dist/artifact-blocks/review/bulk.js +61 -0
  248. package/dist/artifact-blocks/review/itemReview.d.ts +33 -0
  249. package/dist/artifact-blocks/review/itemReview.js +112 -0
  250. package/dist/artifact-blocks/review/roundLines.d.ts +14 -0
  251. package/dist/artifact-blocks/review/roundLines.js +20 -0
  252. package/dist/artifact-blocks/review/rounds.d.ts +86 -0
  253. package/dist/artifact-blocks/review/rounds.js +302 -0
  254. package/dist/artifact-blocks/review/saver.d.ts +23 -0
  255. package/dist/artifact-blocks/review/saver.js +48 -0
  256. package/dist/artifact-blocks/review/team.d.ts +31 -0
  257. package/dist/artifact-blocks/review/team.js +42 -0
  258. package/dist/artifact-blocks/review/visualOrder.d.ts +29 -0
  259. package/dist/artifact-blocks/review/visualOrder.js +66 -0
  260. package/dist/artifact-blocks/review.d.ts +180 -0
  261. package/dist/artifact-blocks/review.js +716 -0
  262. package/dist/artifact-blocks/thumb-kit.d.ts +80 -0
  263. package/dist/artifact-blocks/thumb-kit.js +149 -0
  264. package/dist/artifact-blocks/types.d.ts +513 -0
  265. package/dist/artifact-blocks/types.js +1 -0
  266. package/dist/components/Button.svelte +8 -0
  267. package/dist/components/TextInput.svelte +39 -6
  268. package/dist/components/TextInput.svelte.d.ts +15 -7
  269. package/dist/components/basic/Checkbox.svelte +52 -14
  270. package/dist/components/basic/Checkbox.svelte.d.ts +11 -1
  271. package/package.json +11 -2
@@ -0,0 +1,875 @@
1
+ <script lang="ts">
2
+ /**
3
+ * ChartBlock — SVG line / bar / area / scatter renderer (block type "chart").
4
+ *
5
+ * Hand-rolled SVG, no chart dependency: scales, ticks and stacking live in
6
+ * ./schema.ts (pure, unit-tested); this file is the render layer.
7
+ *
8
+ * Dataviz discipline (the ruby-kim-soak rules):
9
+ * - recessive grid (base-content/10, horizontal only),
10
+ * - honest axes (value axis always includes 0 for bar/area — an explicit
11
+ * yMin/yMax can extend the domain but never truncate it),
12
+ * - identity never by color alone (series name in the legend chips, in the
13
+ * direct end-labels, in the tooltip, and in the data-table fallback).
14
+ *
15
+ * Colors are tokens only: series strokes come from the `--chart-series-1..6`
16
+ * contract (src/lib/theme-bridge/layerchart.css, mapped to DaisyUI palette
17
+ * tokens) — this wrapper carries the `lc-root-container` class so those vars
18
+ * resolve, with `var(--color-primary)` as the fallback.
19
+ */
20
+ import type { BlockProps } from "../../types.js"
21
+ import {
22
+ chartSchema,
23
+ normalizeChart,
24
+ linearTicks,
25
+ valueDomain,
26
+ timeTicks,
27
+ timeTickFormat,
28
+ formatYValue,
29
+ formatYTick,
30
+ nearestIndex,
31
+ stackByX,
32
+ layoutEndLabels,
33
+ dataTableRows,
34
+ formatTableValue,
35
+ valueMap,
36
+ parseTimeX,
37
+ END_LABEL_SERIES_LIMIT,
38
+ type ChartParams,
39
+ } from "./schema.js"
40
+ import SectionTools from "../../SectionTools.svelte"
41
+
42
+ let { params: rawParams, ctx }: BlockProps<ChartParams> = $props()
43
+
44
+ /** Params are parsed here, not assumed pre-parsed: the host validates at
45
+ * publish but stores what was sent, so defaults (xType, axis, goalLines…)
46
+ * must be applied at render. Corrupt params fall back to an empty render —
47
+ * a bad agent payload must never throw inside the artifact page. */
48
+ const EMPTY_PARAMS: ChartParams = {
49
+ chartType: "line",
50
+ xType: "category",
51
+ series: [],
52
+ stacked: false,
53
+ goalLines: [],
54
+ annotations: [],
55
+ yFormat: "raw",
56
+ currency: "USD",
57
+ showLegend: true,
58
+ endLabels: true,
59
+ height: 280,
60
+ }
61
+ const parsed = $derived(chartSchema.safeParse(rawParams))
62
+ const params = $derived(parsed.success ? parsed.data : EMPTY_PARAMS)
63
+
64
+ // ── Responsive width (SSR/tests: 640 fallback) ──────────────────────────
65
+ let containerEl = $state<HTMLDivElement | undefined>(undefined)
66
+ let measured = $state(0)
67
+ $effect(() => {
68
+ if (!containerEl) return
69
+ const update = () => {
70
+ measured = containerEl?.offsetWidth ?? 0
71
+ }
72
+ update()
73
+ if (typeof ResizeObserver === "undefined") return
74
+ const ro = new ResizeObserver(update)
75
+ ro.observe(containerEl)
76
+ return () => ro.disconnect()
77
+ })
78
+ const W = $derived(measured > 160 ? measured : 640)
79
+ const H = $derived(params.height)
80
+
81
+ // ── Normalized data ──────────────────────────────────────────────────────
82
+ const norm = $derived(normalizeChart(params))
83
+
84
+ const endLabelsOn = $derived(
85
+ params.endLabels &&
86
+ (params.chartType === "line" || params.chartType === "area") &&
87
+ norm.series.length <= END_LABEL_SERIES_LIMIT &&
88
+ norm.xNums.length > 0,
89
+ )
90
+
91
+ const seriesColor = (i: number) =>
92
+ `var(--chart-series-${(i % 6) + 1}, var(--color-primary))`
93
+ const seriesAreaFill = (i: number) =>
94
+ `color-mix(in oklch, ${seriesColor(i)} 22%, transparent)`
95
+ const seriesAreaFillStacked = (i: number) =>
96
+ `color-mix(in oklch, ${seriesColor(i)} 45%, transparent)`
97
+
98
+ // ── Geometry ─────────────────────────────────────────────────────────────
99
+ const padLeft = $derived(params.yFormat === "currency" ? 58 : 46)
100
+ const padRight = $derived((endLabelsOn ? 88 : 12) + (norm.dual ? 48 : 0))
101
+ const padTop = 14
102
+ const padBottom = 28
103
+ const plotW = $derived(Math.max(40, W - padLeft - padRight))
104
+ const plotH = $derived(Math.max(40, H - padTop - padBottom))
105
+ const plotBottom = $derived(padTop + plotH)
106
+
107
+ // X: category = one slot per value (bars and lines share slot geometry);
108
+ // time and number = padded linear scale (bars still slot per distinct x).
109
+ const usesSlots = $derived(
110
+ params.chartType === "bar" || norm.xType === "category",
111
+ )
112
+ const slotW = $derived(plotW / Math.max(1, norm.xNums.length))
113
+ const catPos = $derived(new Map(norm.xNums.map((x, i) => [x, i] as const)))
114
+ const xLinear = $derived.by(() => {
115
+ const xs = norm.xNums
116
+ if (usesSlots || xs.length === 0) return null
117
+ const min = xs[0]
118
+ const max = xs[xs.length - 1]
119
+ if (min === max) {
120
+ const pad = norm.xType === "time" ? 86.4e6 : 1
121
+ return { d0: min - pad, d1: max + pad }
122
+ }
123
+ const pad = (max - min) * 0.02
124
+ return { d0: min - pad, d1: max + pad }
125
+ })
126
+ const xPx = (xNum: number): number => {
127
+ if (usesSlots) {
128
+ const i = catPos.get(xNum) ?? 0
129
+ return padLeft + (i + 0.5) * slotW
130
+ }
131
+ const lin = xLinear
132
+ if (!lin) return padLeft + plotW / 2
133
+ return padLeft + ((xNum - lin.d0) / (lin.d1 - lin.d0)) * plotW
134
+ }
135
+ const slotLeft = (i: number) => padLeft + i * slotW
136
+ const slotCenterOf = (i: number) => padLeft + (i + 0.5) * slotW
137
+
138
+ // ── Stacking (per value-axis group) ─────────────────────────────────────
139
+ type Seg = { xNum: number; y0: number; y1: number }
140
+ const stackedSegs = $derived.by(() => {
141
+ if (!params.stacked) return null
142
+ const segs: (Seg[] | undefined)[] = new Array(norm.series.length)
143
+ for (const axis of ["left", "right"] as const) {
144
+ const idxs = norm.series
145
+ .map((s, i) => (s.axis === axis ? i : -1))
146
+ .filter((i) => i >= 0)
147
+ const stacked = stackByX(idxs.map((i) => norm.series[i].points))
148
+ idxs.forEach((si, k) => (segs[si] = stacked[k]))
149
+ }
150
+ return segs
151
+ })
152
+ /** Per-series x → segment map (bar marks hit this per slot). */
153
+ const stackedSegMaps = $derived.by(() => {
154
+ if (!params.stacked || !stackedSegs) return null
155
+ return stackedSegs.map(
156
+ (segs) => new Map((segs ?? []).map((s) => [s.xNum, s] as const)),
157
+ )
158
+ })
159
+ /** Per-series x → y map (the render loop hits this per slot — a `.find()`
160
+ * per cell would be quadratic on 5000-point charts). */
161
+ const pointMaps = $derived(norm.series.map((s) => valueMap(s.points)))
162
+ function valueAt(si: number, xNum: number): number | null {
163
+ return pointMaps[si]?.get(xNum) ?? null
164
+ }
165
+
166
+ // ── Value scales ─────────────────────────────────────────────────────────
167
+ function axisVals(axis: "left" | "right"): number[] {
168
+ const vals: number[] = []
169
+ if (params.stacked && stackedSegs) {
170
+ norm.series.forEach((s, i) => {
171
+ if (s.axis !== axis) return
172
+ for (const seg of stackedSegs[i] ?? []) vals.push(seg.y0, seg.y1)
173
+ })
174
+ } else {
175
+ for (const s of norm.series) {
176
+ if (s.axis !== axis) continue
177
+ for (const p of s.points) vals.push(p.y)
178
+ }
179
+ }
180
+ for (const g of params.goalLines) if (g.axis === axis) vals.push(g.value)
181
+ if (axis === "left")
182
+ for (const a of params.annotations) if (a.y !== undefined) vals.push(a.y)
183
+ return vals
184
+ }
185
+ const leftScale = $derived(
186
+ linearTicks(
187
+ ...valueDomain(axisVals("left"), params.chartType, {
188
+ yMin: params.yMin,
189
+ yMax: params.yMax,
190
+ }),
191
+ 5,
192
+ ),
193
+ )
194
+ const rightScale = $derived(
195
+ norm.dual
196
+ ? linearTicks(...valueDomain(axisVals("right"), params.chartType, {}), 4)
197
+ : null,
198
+ )
199
+ const yPxLeft = (v: number) =>
200
+ padTop +
201
+ plotH -
202
+ ((v - leftScale.domain[0]) /
203
+ (leftScale.domain[1] - leftScale.domain[0] || 1)) *
204
+ plotH
205
+ const yPxRight = (v: number) => {
206
+ const s = rightScale
207
+ if (!s) return yPxLeft(v)
208
+ return (
209
+ padTop +
210
+ plotH -
211
+ ((v - s.domain[0]) / (s.domain[1] - s.domain[0] || 1)) * plotH
212
+ )
213
+ }
214
+ const yPxFor = (axis: "left" | "right") =>
215
+ axis === "right" ? yPxRight : yPxLeft
216
+ /** Bars rise from zero (or below zero when negatives exist). */
217
+ function zeroYFor(axis: "left" | "right"): number {
218
+ if (axis === "right") {
219
+ const s = rightScale
220
+ if (!s) return yPxLeft(0)
221
+ return yPxRight(Math.max(s.domain[0], Math.min(0, s.domain[1])))
222
+ }
223
+ return yPxLeft(
224
+ Math.max(leftScale.domain[0], Math.min(0, leftScale.domain[1])),
225
+ )
226
+ }
227
+ const baselineY = $derived(zeroYFor("left"))
228
+
229
+ // ── Paths ────────────────────────────────────────────────────────────────
230
+ const r1 = (n: number) => Math.round(n * 10) / 10
231
+ function linePathOf(si: number): string {
232
+ const s = norm.series[si]
233
+ const yPx = yPxFor(s.axis)
234
+ const segs = params.stacked ? stackedSegs?.[si] : undefined
235
+ if (segs)
236
+ return segs
237
+ .map(
238
+ (seg, k) =>
239
+ `${k ? "L" : "M"} ${r1(xPx(seg.xNum))} ${r1(yPx(seg.y1))}`,
240
+ )
241
+ .join(" ")
242
+ return s.points
243
+ .map((p, k) => `${k ? "L" : "M"} ${r1(xPx(p.xNum))} ${r1(yPx(p.y))}`)
244
+ .join(" ")
245
+ }
246
+ function areaPathOf(si: number): string {
247
+ const s = norm.series[si]
248
+ const yPx = yPxFor(s.axis)
249
+ const segs = params.stacked ? stackedSegs?.[si] : undefined
250
+ if (segs) {
251
+ if (!segs.length) return ""
252
+ const down = [...segs]
253
+ .reverse()
254
+ .map((seg) => `L ${r1(xPx(seg.xNum))} ${r1(yPx(seg.y0))}`)
255
+ .join(" ")
256
+ return (
257
+ segs
258
+ .map(
259
+ (seg, k) =>
260
+ `${k ? "L" : "M"} ${r1(xPx(seg.xNum))} ${r1(yPx(seg.y1))}`,
261
+ )
262
+ .join(" ") +
263
+ " " +
264
+ down +
265
+ " Z"
266
+ )
267
+ }
268
+ const pts = s.points
269
+ if (!pts.length) return ""
270
+ const path = pts
271
+ .map((p, k) => `${k ? "L" : "M"} ${r1(xPx(p.xNum))} ${r1(yPx(p.y))}`)
272
+ .join(" ")
273
+ const first = pts[0]
274
+ const last = pts[pts.length - 1]
275
+ return `${path} L ${r1(xPx(last.xNum))} ${r1(baselineY)} L ${r1(xPx(first.xNum))} ${r1(baselineY)} Z`
276
+ }
277
+
278
+ // ── Bars ─────────────────────────────────────────────────────────────────
279
+ const barW = $derived(
280
+ Math.max(1, Math.min((slotW * 0.78) / Math.max(1, norm.series.length), 36)),
281
+ )
282
+ const stackedBarW = $derived(Math.max(1, Math.min(slotW * 0.78, 36)))
283
+
284
+ // ── Annotations ──────────────────────────────────────────────────────────
285
+ function resolveAnnX(raw: string | number): number | null {
286
+ if (norm.xType === "category") {
287
+ const i = norm.categories.indexOf(String(raw))
288
+ return i === -1 ? null : (norm.xNums[i] ?? null)
289
+ }
290
+ const n =
291
+ norm.xType === "time"
292
+ ? parseTimeX(raw)
293
+ : typeof raw === "number"
294
+ ? raw
295
+ : Number.parseFloat(raw)
296
+ return Number.isFinite(n) ? n : null
297
+ }
298
+ const renderedAnnotations = $derived.by(() => {
299
+ const out: {
300
+ kind: "point" | "range"
301
+ label: string
302
+ y?: number
303
+ px1: number
304
+ px2?: number
305
+ }[] = []
306
+ for (const a of params.annotations) {
307
+ const dx1 = resolveAnnX(a.x)
308
+ if (dx1 === null) continue
309
+ const px1 = xPx(dx1)
310
+ if (a.kind === "range") {
311
+ if (a.x2 === undefined) continue
312
+ const dx2 = resolveAnnX(a.x2)
313
+ if (dx2 === null) continue
314
+ const px2 = xPx(dx2)
315
+ if (
316
+ Math.max(px1, px2) < padLeft ||
317
+ Math.min(px1, px2) > padLeft + plotW
318
+ )
319
+ continue
320
+ out.push({ kind: "range", label: a.label, px1, px2 })
321
+ } else {
322
+ if (px1 < padLeft - 1 || px1 > padLeft + plotW + 1) continue
323
+ out.push({ kind: "point", label: a.label, y: a.y, px1 })
324
+ }
325
+ }
326
+ return out
327
+ })
328
+
329
+ // ── Ticks / labels ───────────────────────────────────────────────────────
330
+ const yTickFmt = $derived((v: number) =>
331
+ formatYTick(v, params.yFormat, params.currency),
332
+ )
333
+ const xTicks = $derived.by(() => {
334
+ if (usesSlots) return []
335
+ const lin = xLinear
336
+ if (!lin) return []
337
+ if (norm.xType === "time")
338
+ return timeTicks(lin.d0, lin.d1, Math.max(3, Math.floor(plotW / 90)))
339
+ return linearTicks(lin.d0, lin.d1, Math.max(3, Math.floor(plotW / 80)))
340
+ .ticks
341
+ })
342
+ const xTickFmt = $derived.by(() => {
343
+ if (norm.xType === "time") {
344
+ const lin = xLinear
345
+ const f = timeTickFormat(lin?.d0 ?? 0, lin?.d1 ?? 0)
346
+ return (v: number) => f(v)
347
+ }
348
+ if (norm.xType === "number")
349
+ return (v: number) =>
350
+ new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(v)
351
+ return (v: number) => norm.categories[v] ?? String(v)
352
+ })
353
+ const xLabelBudget = $derived(Math.max(2, Math.floor(plotW / 58)))
354
+ const categoryLabelStep = $derived(
355
+ Math.max(1, Math.ceil(norm.xNums.length / xLabelBudget)),
356
+ )
357
+
358
+ function truncate(s: string, n: number): string {
359
+ return s.length > n ? `${s.slice(0, n - 1)}…` : s
360
+ }
361
+
362
+ // End labels: series name at the last visible point, pushed apart so
363
+ // neighbors stay readable.
364
+ const endLabels = $derived.by(() => {
365
+ if (!endLabelsOn) return []
366
+ const items = norm.series.map((s, i) => {
367
+ const yPx = yPxFor(s.axis)
368
+ const last = params.stacked
369
+ ? (stackedSegs?.[i] ?? []).at(-1)
370
+ : s.points.at(-1)
371
+ const lastX = params.stacked
372
+ ? (stackedSegs?.[i] ?? []).at(-1)?.xNum
373
+ : s.points.at(-1)?.xNum
374
+ return {
375
+ label: s.label,
376
+ y: last ? yPx(params.stacked ? last.y1 : last.y) : plotBottom,
377
+ x: lastX !== undefined ? xPx(lastX) : padLeft + plotW,
378
+ i,
379
+ }
380
+ })
381
+ const ys = layoutEndLabels(items, padTop + 4, plotBottom - 2)
382
+ return items.map((it, k) => ({ ...it, y: ys[k] }))
383
+ })
384
+
385
+ // ── Crosshair + tooltip ──────────────────────────────────────────────────
386
+ let svgEl = $state<SVGSVGElement | undefined>(undefined)
387
+ let hoverIdx = $state<number | null>(null)
388
+ let hoverPx = $state(0)
389
+
390
+ function invertX(px: number): number {
391
+ if (usesSlots) {
392
+ const i = Math.floor((px - padLeft) / slotW)
393
+ const idx = Math.min(Math.max(i, 0), norm.xNums.length - 1)
394
+ return norm.xNums[idx] ?? 0
395
+ }
396
+ const lin = xLinear
397
+ if (!lin) return norm.xNums[0] ?? 0
398
+ return lin.d0 + ((px - padLeft) / plotW) * (lin.d1 - lin.d0)
399
+ }
400
+ function onPointerMove(e: PointerEvent) {
401
+ const rect = svgEl?.getBoundingClientRect()
402
+ if (!rect || rect.width === 0 || !norm.xNums.length) {
403
+ hoverIdx = null
404
+ return
405
+ }
406
+ const px = e.clientX - rect.left
407
+ if (px < padLeft - 6 || px > padLeft + plotW + 6) {
408
+ hoverIdx = null
409
+ return
410
+ }
411
+ hoverPx = px
412
+ hoverIdx = nearestIndex(norm.xNums, invertX(px))
413
+ }
414
+ function onPointerLeave() {
415
+ hoverIdx = null
416
+ }
417
+ const hoverSnapX = $derived(
418
+ hoverIdx === null
419
+ ? 0
420
+ : usesSlots
421
+ ? slotCenterOf(hoverIdx)
422
+ : xLinear
423
+ ? padLeft +
424
+ ((norm.xNums[hoverIdx] - xLinear.d0) / (xLinear.d1 - xLinear.d0)) *
425
+ plotW
426
+ : padLeft,
427
+ )
428
+ const hoverXLabel = $derived.by(() => {
429
+ if (hoverIdx === null) return ""
430
+ const xNum = norm.xNums[hoverIdx]
431
+ if (norm.xType === "category") return norm.categories[xNum] ?? ""
432
+ if (norm.xType === "time") {
433
+ const step =
434
+ norm.xNums.length > 1 ? norm.xNums[1] - norm.xNums[0] : 86.4e6
435
+ return new Intl.DateTimeFormat("en-US", {
436
+ dateStyle: "medium",
437
+ ...(step < 86.4e6 ? { timeStyle: "short" as const } : {}),
438
+ }).format(xNum)
439
+ }
440
+ return new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(
441
+ xNum,
442
+ )
443
+ })
444
+ const hoverData = $derived.by(() => {
445
+ if (hoverIdx === null) return null
446
+ const xNum = norm.xNums[hoverIdx]
447
+ const rows = norm.series.map((s, si) => ({
448
+ si,
449
+ v: valueAt(si, xNum),
450
+ yPx: yPxFor(s.axis)(valueAt(si, xNum) ?? 0),
451
+ }))
452
+ return { xNum, rows: rows.filter((r) => r.v !== null) }
453
+ })
454
+
455
+ // ── A11y data fallback ───────────────────────────────────────────────────
456
+ let dataOpen = $state(false)
457
+ const tableId = $derived(`block-data-${ctx.blockId || "chart"}`)
458
+ const tableRows = $derived(dataTableRows(norm))
459
+ </script>
460
+
461
+ <figure class="lc-root-container w-full" data-block-type="chart">
462
+ {#if params.title || params.subtitle}
463
+ <div class="mb-1">
464
+ {#if params.title}
465
+ <div class="flex items-center gap-2">
466
+ <h4 class="min-w-0 text-sm font-semibold text-base-content">
467
+ {params.title}
468
+ </h4>
469
+ <SectionTools {ctx} inline />
470
+ </div>
471
+ {/if}
472
+ {#if params.subtitle}
473
+ <p class="text-xs text-base-content/60">{params.subtitle}</p>
474
+ {/if}
475
+ </div>
476
+ {/if}
477
+
478
+ {#if norm.series.length === 0 || norm.xNums.length === 0}
479
+ <p class="text-sm text-base-content/45">No data to chart.</p>
480
+ {:else}
481
+ <div class="relative w-full" bind:this={containerEl}>
482
+ <svg
483
+ bind:this={svgEl}
484
+ width={W}
485
+ height={H}
486
+ viewBox="0 0 {W} {H}"
487
+ class="block max-w-full"
488
+ role="img"
489
+ aria-label="{params.chartType} chart{params.title
490
+ ? `: ${params.title}`
491
+ : ''}. Series: {norm.series.map((s) => s.label).join(', ')}."
492
+ onpointermove={onPointerMove}
493
+ onpointerleave={onPointerLeave}
494
+ >
495
+ <!-- recessive horizontal grid (left scale only) -->
496
+ {#each leftScale.ticks as t (t)}
497
+ <line
498
+ x1={padLeft}
499
+ x2={padLeft + plotW}
500
+ y1={yPxLeft(t)}
501
+ y2={yPxLeft(t)}
502
+ class="stroke-base-content/10"
503
+ stroke-width="1"
504
+ shape-rendering="crispEdges"
505
+ />
506
+ <text
507
+ x={padLeft - 6}
508
+ y={yPxLeft(t) + 3}
509
+ text-anchor="end"
510
+ class="fill-base-content/45 text-2xs tabular-nums"
511
+ >
512
+ {yTickFmt(t)}
513
+ </text>
514
+ {/each}
515
+ {#if rightScale}
516
+ {#each rightScale.ticks as t (t)}
517
+ <text
518
+ x={padLeft + plotW + 6}
519
+ y={yPxRight(t) + 3}
520
+ text-anchor="start"
521
+ class="fill-base-content/45 text-2xs tabular-nums"
522
+ >
523
+ {yTickFmt(t)}
524
+ </text>
525
+ {/each}
526
+ {/if}
527
+
528
+ <!-- honest baseline (0 or domain floor) -->
529
+ <line
530
+ x1={padLeft}
531
+ x2={padLeft + plotW}
532
+ y1={baselineY}
533
+ y2={baselineY}
534
+ class="stroke-base-content/25"
535
+ stroke-width="1"
536
+ shape-rendering="crispEdges"
537
+ />
538
+
539
+ <!-- y axis titles -->
540
+ {#if params.yLabel}
541
+ <text
542
+ x={12}
543
+ y={padTop + plotH / 2}
544
+ text-anchor="middle"
545
+ transform="rotate(-90, 12, {padTop + plotH / 2})"
546
+ class="fill-base-content/60 text-2xs font-medium"
547
+ >
548
+ {params.yLabel}
549
+ </text>
550
+ {/if}
551
+ {#if norm.dual && params.y2Label}
552
+ <text
553
+ x={W - 12}
554
+ y={padTop + plotH / 2}
555
+ text-anchor="middle"
556
+ transform="rotate(90, {W - 12}, {padTop + plotH / 2})"
557
+ class="fill-base-content/60 text-2xs font-medium"
558
+ >
559
+ {params.y2Label}
560
+ </text>
561
+ {/if}
562
+
563
+ <!-- annotations (below marks) -->
564
+ {#each renderedAnnotations as a, ai (ai)}
565
+ {#if a.kind === "range"}
566
+ <rect
567
+ x={Math.min(a.px1, a.px2 ?? a.px1)}
568
+ y={padTop}
569
+ width={Math.max(1, Math.abs((a.px2 ?? a.px1) - a.px1))}
570
+ height={plotH}
571
+ class="fill-base-content/5"
572
+ />
573
+ <text
574
+ x={Math.min(a.px1, a.px2 ?? a.px1) + 4}
575
+ y={padTop + 10}
576
+ class="fill-base-content/60 text-2xs"
577
+ >
578
+ {truncate(a.label, 36)}
579
+ </text>
580
+ {:else}
581
+ <line
582
+ x1={a.px1}
583
+ x2={a.px1}
584
+ y1={padTop}
585
+ y2={padTop + plotH}
586
+ class="stroke-base-content/30"
587
+ stroke-dasharray="4 3"
588
+ />
589
+ {#if a.y !== undefined}
590
+ <circle
591
+ cx={a.px1}
592
+ cy={yPxLeft(a.y)}
593
+ r="3.5"
594
+ class="fill-base-content/40"
595
+ />
596
+ {/if}
597
+ <text
598
+ x={a.px1 + 4}
599
+ y={padTop + 10}
600
+ class="fill-base-content/60 text-2xs"
601
+ >
602
+ {truncate(a.label, 36)}
603
+ </text>
604
+ {/if}
605
+ {/each}
606
+
607
+ <!-- goal / threshold lines -->
608
+ {#each params.goalLines as g, gi (gi)}
609
+ {@const gy = yPxFor(g.axis)(g.value)}
610
+ {#if gy >= padTop && gy <= plotBottom}
611
+ <line
612
+ x1={padLeft}
613
+ x2={padLeft + plotW}
614
+ y1={gy}
615
+ y2={gy}
616
+ class="stroke-base-content/30"
617
+ stroke-dasharray="5 4"
618
+ />
619
+ {#if g.label}
620
+ <text
621
+ x={padLeft + 4}
622
+ y={gy - 4}
623
+ class="fill-base-content/60 text-2xs"
624
+ >
625
+ {g.label}
626
+ </text>
627
+ {/if}
628
+ {/if}
629
+ {/each}
630
+
631
+ <!-- marks -->
632
+ {#if params.chartType === "bar"}
633
+ {#each norm.xNums as xNum, xi (xNum)}
634
+ {#each norm.series as s, si (si)}
635
+ {@const seg = params.stacked
636
+ ? stackedSegMaps?.[si]?.get(xNum)
637
+ : undefined}
638
+ {#if params.stacked ? seg !== undefined : valueAt(si, xNum) !== null}
639
+ {@const yPx = yPxFor(s.axis)}
640
+ {@const topV = params.stacked
641
+ ? (seg?.y1 ?? 0)
642
+ : (valueAt(si, xNum) ?? 0)}
643
+ {@const botV = params.stacked ? (seg?.y0 ?? 0) : 0}
644
+ {@const x = params.stacked
645
+ ? slotLeft(xi) + (slotW - stackedBarW) / 2
646
+ : slotLeft(xi) +
647
+ (slotW - barW * norm.series.length) / 2 +
648
+ si * barW}
649
+ <rect
650
+ {x}
651
+ y={Math.min(yPx(topV), yPx(botV))}
652
+ width={params.stacked ? stackedBarW : barW}
653
+ height={Math.max(1, Math.abs(yPx(topV) - yPx(botV)))}
654
+ fill={seriesColor(si)}
655
+ />
656
+ {/if}
657
+ {/each}
658
+ {/each}
659
+ {:else if params.chartType === "area"}
660
+ {#each norm.series as _, si (si)}
661
+ <path
662
+ d={areaPathOf(si)}
663
+ fill={params.stacked
664
+ ? seriesAreaFillStacked(si)
665
+ : seriesAreaFill(si)}
666
+ stroke={seriesColor(si)}
667
+ stroke-width="1.5"
668
+ />
669
+ {/each}
670
+ {:else if params.chartType === "line"}
671
+ {#each norm.series as _, si (si)}
672
+ <path
673
+ d={linePathOf(si)}
674
+ fill="none"
675
+ stroke={seriesColor(si)}
676
+ stroke-width="2"
677
+ stroke-linejoin="round"
678
+ stroke-linecap="round"
679
+ />
680
+ {/each}
681
+ {:else}
682
+ {#each norm.series as s, si (si)}
683
+ {#each s.points as p, pi (pi)}
684
+ <circle
685
+ cx={r1(xPx(p.xNum))}
686
+ cy={r1(yPxFor(s.axis)(p.y))}
687
+ r="3.5"
688
+ fill={seriesColor(si)}
689
+ />
690
+ {/each}
691
+ {/each}
692
+ {/if}
693
+
694
+ <!-- direct end-labels -->
695
+ {#each endLabels as el (el.i)}
696
+ <text
697
+ x={el.x + 7}
698
+ y={el.y + 3}
699
+ class="text-2xs font-medium"
700
+ fill={seriesColor(el.i)}
701
+ >
702
+ {truncate(el.label, 14)}
703
+ </text>
704
+ {/each}
705
+
706
+ <!-- x tick labels -->
707
+ {#if usesSlots}
708
+ {#each norm.xNums as xNum, xi (xNum)}
709
+ {#if xi % categoryLabelStep === 0}
710
+ <text
711
+ x={slotCenterOf(xi)}
712
+ y={plotBottom + 16}
713
+ text-anchor="middle"
714
+ class="fill-base-content/45 text-2xs"
715
+ >
716
+ {truncate(norm.categories[xNum] ?? String(xNum), 12)}
717
+ </text>
718
+ {/if}
719
+ {/each}
720
+ {:else}
721
+ {#each xTicks as t (t)}
722
+ <text
723
+ x={xPx(t)}
724
+ y={plotBottom + 16}
725
+ text-anchor="middle"
726
+ class="fill-base-content/45 text-2xs"
727
+ >
728
+ {truncate(xTickFmt(t), 20)}
729
+ </text>
730
+ {/each}
731
+ {/if}
732
+
733
+ <!-- crosshair -->
734
+ {#if hoverData}
735
+ {#if params.chartType === "bar"}
736
+ <rect
737
+ x={slotLeft(hoverIdx ?? 0)}
738
+ y={padTop}
739
+ width={slotW}
740
+ height={plotH}
741
+ class="fill-base-content/5"
742
+ />
743
+ {/if}
744
+ <line
745
+ x1={hoverSnapX}
746
+ x2={hoverSnapX}
747
+ y1={padTop}
748
+ y2={padTop + plotH}
749
+ class="stroke-base-content/25"
750
+ stroke-width="1"
751
+ />
752
+ {#each hoverData.rows as d (d.si)}
753
+ <circle
754
+ cx={r1(hoverSnapX)}
755
+ cy={r1(d.yPx)}
756
+ r="3.5"
757
+ fill={seriesColor(d.si)}
758
+ stroke="var(--color-base-100)"
759
+ stroke-width="1.5"
760
+ />
761
+ {/each}
762
+ {/if}
763
+ </svg>
764
+
765
+ <!-- tooltip -->
766
+ {#if hoverData}
767
+ <div
768
+ class="pointer-events-none absolute top-1 z-10 min-w-32 rounded-box border border-base-content/10 bg-base-100/95 px-2.5 py-1.5 text-xs"
769
+ style:left={hoverPx > padLeft + plotW * 0.62
770
+ ? "auto"
771
+ : `${hoverPx + 12}px`}
772
+ style:right={hoverPx > padLeft + plotW * 0.62
773
+ ? `${W - hoverPx + 10}px`
774
+ : "auto"}
775
+ >
776
+ <div class="mb-0.5 font-medium text-base-content/60">
777
+ {hoverXLabel}
778
+ </div>
779
+ {#each hoverData.rows as d (d.si)}
780
+ <div class="flex items-center gap-1.5 whitespace-nowrap">
781
+ <span
782
+ class="h-2 w-2 shrink-0 rounded-[2px]"
783
+ style:background={seriesColor(d.si)}
784
+ aria-hidden="true"
785
+ ></span>
786
+ <span class="text-base-content/70">{norm.series[d.si].label}</span
787
+ >
788
+ <span
789
+ class="ml-auto pl-2 font-medium tabular-nums text-base-content"
790
+ >
791
+ {formatYValue(d.v ?? 0, params.yFormat, params.currency)}
792
+ </span>
793
+ </div>
794
+ {/each}
795
+ </div>
796
+ {/if}
797
+ </div>
798
+ {/if}
799
+
800
+ <!-- legend: swatch + TEXT label (identity never by color alone) -->
801
+ {#if params.showLegend && norm.series.length > 1}
802
+ <div class="mt-1.5 flex flex-wrap items-center gap-x-4 gap-y-1">
803
+ {#each norm.series as s, si (si)}
804
+ <span
805
+ class="inline-flex items-center gap-1.5 text-xs text-base-content/70"
806
+ >
807
+ <span
808
+ class="h-2.5 w-2.5 shrink-0 rounded-[3px]"
809
+ style:background={seriesColor(si)}
810
+ aria-hidden="true"
811
+ ></span>
812
+ {s.label}
813
+ {#if norm.dual && s.axis === "right"}
814
+ <span class="badge badge-ghost badge-xs font-normal"
815
+ >right axis</span
816
+ >
817
+ {/if}
818
+ </span>
819
+ {/each}
820
+ </div>
821
+ {/if}
822
+
823
+ <!-- accessible data fallback: sr-only table always; expandable for everyone -->
824
+ <div class="mt-1.5 flex items-center justify-between gap-2">
825
+ {#if params.xLabel}
826
+ <span class="text-2xs text-base-content/45">{params.xLabel}</span>
827
+ {:else}
828
+ <span></span>
829
+ {/if}
830
+ <button
831
+ type="button"
832
+ class="btn btn-ghost btn-xs text-base-content/70"
833
+ aria-expanded={dataOpen}
834
+ aria-controls={tableId}
835
+ onclick={() => (dataOpen = !dataOpen)}
836
+ >
837
+ {dataOpen ? "Hide data" : "View data"}
838
+ </button>
839
+ </div>
840
+ <div
841
+ class={dataOpen
842
+ ? "mt-1 max-h-64 overflow-auto rounded-box border border-base-content/10"
843
+ : "sr-only"}
844
+ >
845
+ <!-- sr-only sits on the wrapper: a <table> ignores width:1px and grows to
846
+ its content, so on the table itself it widened the page at 390px. -->
847
+ <table id={tableId} class={dataOpen ? "table table-sm" : undefined}>
848
+ <caption class="sr-only">
849
+ Chart data{params.title ? `: ${params.title}` : ""}
850
+ </caption>
851
+ <thead>
852
+ <tr>
853
+ <th scope="col" class="text-left">x</th>
854
+ {#each norm.series as s, si (si)}
855
+ <th scope="col" class="text-right">{s.label}</th>
856
+ {/each}
857
+ </tr>
858
+ </thead>
859
+ <tbody>
860
+ {#each tableRows as row (row.x)}
861
+ <tr>
862
+ <th scope="row" class="text-left font-normal">{row.xLabel}</th>
863
+ {#each row.values as v, vi (vi)}
864
+ <td class="text-right tabular-nums">
865
+ {v === null
866
+ ? "n/a"
867
+ : formatTableValue(v, params.yFormat, params.currency)}
868
+ </td>
869
+ {/each}
870
+ </tr>
871
+ {/each}
872
+ </tbody>
873
+ </table>
874
+ </div>
875
+ </figure>