@svgrid/grid 1.0.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 (213) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +39 -0
  3. package/dist/FlexRender.svelte +96 -0
  4. package/dist/FlexRender.svelte.d.ts +49 -0
  5. package/dist/SvGrid.svelte +8742 -0
  6. package/dist/SvGrid.svelte.d.ts +381 -0
  7. package/dist/SvGridChart.svelte +653 -0
  8. package/dist/SvGridChart.svelte.d.ts +25 -0
  9. package/dist/SvGridChart.test.d.ts +1 -0
  10. package/dist/SvGridChart.test.js +62 -0
  11. package/dist/SvGridDropdown.svelte +675 -0
  12. package/dist/SvGridDropdown.svelte.d.ts +40 -0
  13. package/dist/a11y.contract.test.d.ts +1 -0
  14. package/dist/a11y.contract.test.js +39 -0
  15. package/dist/a11y.d.ts +38 -0
  16. package/dist/a11y.js +37 -0
  17. package/dist/a11y.test.d.ts +1 -0
  18. package/dist/a11y.test.js +44 -0
  19. package/dist/cell-formatting.d.ts +17 -0
  20. package/dist/cell-formatting.js +141 -0
  21. package/dist/cell-formatting.test.d.ts +1 -0
  22. package/dist/cell-formatting.test.js +234 -0
  23. package/dist/chart-export.d.ts +14 -0
  24. package/dist/chart-export.js +112 -0
  25. package/dist/chart.d.ts +226 -0
  26. package/dist/chart.js +732 -0
  27. package/dist/chart.test.d.ts +1 -0
  28. package/dist/chart.test.js +289 -0
  29. package/dist/collaboration.d.ts +74 -0
  30. package/dist/collaboration.js +98 -0
  31. package/dist/collaboration.test.d.ts +1 -0
  32. package/dist/collaboration.test.js +65 -0
  33. package/dist/conditional-formatting.d.ts +91 -0
  34. package/dist/conditional-formatting.js +170 -0
  35. package/dist/conditional-formatting.test.d.ts +1 -0
  36. package/dist/conditional-formatting.test.js +87 -0
  37. package/dist/core.coverage.test.d.ts +1 -0
  38. package/dist/core.coverage.test.js +186 -0
  39. package/dist/core.d.ts +370 -0
  40. package/dist/core.js +568 -0
  41. package/dist/core.performance.test.d.ts +1 -0
  42. package/dist/core.performance.test.js +29 -0
  43. package/dist/createGrid.svelte.d.ts +6 -0
  44. package/dist/createGrid.svelte.js +17 -0
  45. package/dist/createGrid.test.d.ts +1 -0
  46. package/dist/createGrid.test.js +9 -0
  47. package/dist/createGridState.svelte.d.ts +3 -0
  48. package/dist/createGridState.svelte.js +13 -0
  49. package/dist/editors/cell-editors.d.ts +23 -0
  50. package/dist/editors/cell-editors.js +97 -0
  51. package/dist/editors/cell-editors.test.d.ts +1 -0
  52. package/dist/editors/cell-editors.test.js +75 -0
  53. package/dist/fill-patterns.d.ts +30 -0
  54. package/dist/fill-patterns.js +207 -0
  55. package/dist/fill-patterns.test.d.ts +1 -0
  56. package/dist/fill-patterns.test.js +81 -0
  57. package/dist/filtering/excel-filters.d.ts +26 -0
  58. package/dist/filtering/excel-filters.js +64 -0
  59. package/dist/filtering/excel-filters.test.d.ts +1 -0
  60. package/dist/filtering/excel-filters.test.js +116 -0
  61. package/dist/filtering/locale-filter.test.d.ts +13 -0
  62. package/dist/filtering/locale-filter.test.js +189 -0
  63. package/dist/flex-render.d.ts +1 -0
  64. package/dist/flex-render.js +2 -0
  65. package/dist/flex-render.test.d.ts +1 -0
  66. package/dist/flex-render.test.js +142 -0
  67. package/dist/group-aggregate.test.d.ts +1 -0
  68. package/dist/group-aggregate.test.js +32 -0
  69. package/dist/index.d.ts +29 -0
  70. package/dist/index.js +27 -0
  71. package/dist/keyboard.d.ts +8 -0
  72. package/dist/keyboard.js +91 -0
  73. package/dist/keyboard.test.d.ts +1 -0
  74. package/dist/keyboard.test.js +27 -0
  75. package/dist/merge-objects.d.ts +9 -0
  76. package/dist/merge-objects.js +28 -0
  77. package/dist/named-views.d.ts +42 -0
  78. package/dist/named-views.js +80 -0
  79. package/dist/named-views.test.d.ts +1 -0
  80. package/dist/named-views.test.js +57 -0
  81. package/dist/new-features.test.d.ts +1 -0
  82. package/dist/new-features.test.js +183 -0
  83. package/dist/render-component.d.ts +13 -0
  84. package/dist/render-component.js +14 -0
  85. package/dist/render-component.test.d.ts +1 -0
  86. package/dist/render-component.test.js +41 -0
  87. package/dist/server-data-source.d.ts +76 -0
  88. package/dist/server-data-source.js +83 -0
  89. package/dist/server-data-source.test.d.ts +1 -0
  90. package/dist/server-data-source.test.js +81 -0
  91. package/dist/sparkline.d.ts +56 -0
  92. package/dist/sparkline.js +97 -0
  93. package/dist/sparkline.test.d.ts +1 -0
  94. package/dist/sparkline.test.js +52 -0
  95. package/dist/static-functions.d.ts +1 -0
  96. package/dist/static-functions.js +1 -0
  97. package/dist/subscribe.d.ts +9 -0
  98. package/dist/subscribe.js +31 -0
  99. package/dist/subscribe.test.d.ts +1 -0
  100. package/dist/subscribe.test.js +93 -0
  101. package/dist/sv-grid-scrollbar.d.ts +1 -0
  102. package/dist/sv-grid-scrollbar.js +310 -0
  103. package/dist/svgrid-wrapper.types.d.ts +341 -0
  104. package/dist/svgrid-wrapper.types.js +1 -0
  105. package/dist/svgrid.api-extensions.test.d.ts +14 -0
  106. package/dist/svgrid.api-extensions.test.js +334 -0
  107. package/dist/svgrid.api.test.d.ts +15 -0
  108. package/dist/svgrid.api.test.js +169 -0
  109. package/dist/svgrid.behavior.test.d.ts +15 -0
  110. package/dist/svgrid.behavior.test.js +605 -0
  111. package/dist/svgrid.column-reorder.test.d.ts +15 -0
  112. package/dist/svgrid.column-reorder.test.js +224 -0
  113. package/dist/svgrid.features.test.d.ts +1 -0
  114. package/dist/svgrid.features.test.js +129 -0
  115. package/dist/svgrid.interaction.test.d.ts +9 -0
  116. package/dist/svgrid.interaction.test.js +318 -0
  117. package/dist/svgrid.locale-filtering.test.d.ts +15 -0
  118. package/dist/svgrid.locale-filtering.test.js +261 -0
  119. package/dist/svgrid.new-features.wrapper.test.d.ts +1 -0
  120. package/dist/svgrid.new-features.wrapper.test.js +136 -0
  121. package/dist/svgrid.row-pinning.test.d.ts +14 -0
  122. package/dist/svgrid.row-pinning.test.js +250 -0
  123. package/dist/svgrid.set-filter.test.d.ts +17 -0
  124. package/dist/svgrid.set-filter.test.js +268 -0
  125. package/dist/svgrid.wrapper.test.d.ts +1 -0
  126. package/dist/svgrid.wrapper.test.js +33 -0
  127. package/dist/test-setup.d.ts +1 -0
  128. package/dist/test-setup.js +29 -0
  129. package/dist/transaction.test.d.ts +1 -0
  130. package/dist/transaction.test.js +89 -0
  131. package/dist/virtualization/column-virtualizer.d.ts +20 -0
  132. package/dist/virtualization/column-virtualizer.js +19 -0
  133. package/dist/virtualization/column-virtualizer.test.d.ts +1 -0
  134. package/dist/virtualization/column-virtualizer.test.js +25 -0
  135. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +11 -0
  136. package/dist/virtualization/svelte-virtualizer.svelte.js +20 -0
  137. package/dist/virtualization/types.d.ts +28 -0
  138. package/dist/virtualization/types.js +1 -0
  139. package/dist/virtualization/virtualizer.d.ts +13 -0
  140. package/dist/virtualization/virtualizer.js +232 -0
  141. package/dist/virtualization/virtualizer.test.d.ts +1 -0
  142. package/dist/virtualization/virtualizer.test.js +41 -0
  143. package/package.json +69 -0
  144. package/src/FlexRender.svelte +96 -0
  145. package/src/SvGrid.svelte +8742 -0
  146. package/src/SvGridChart.svelte +653 -0
  147. package/src/SvGridChart.test.ts +69 -0
  148. package/src/SvGridDropdown.svelte +675 -0
  149. package/src/a11y.contract.test.ts +49 -0
  150. package/src/a11y.test.ts +59 -0
  151. package/src/a11y.ts +59 -0
  152. package/src/cell-formatting.test.ts +273 -0
  153. package/src/cell-formatting.ts +169 -0
  154. package/src/chart-export.ts +144 -0
  155. package/src/chart.test.ts +311 -0
  156. package/src/chart.ts +934 -0
  157. package/src/collaboration.test.ts +74 -0
  158. package/src/collaboration.ts +161 -0
  159. package/src/conditional-formatting.test.ts +107 -0
  160. package/src/conditional-formatting.ts +239 -0
  161. package/src/core.coverage.test.ts +230 -0
  162. package/src/core.performance.test.ts +30 -0
  163. package/src/core.ts +991 -0
  164. package/src/createGrid.svelte.ts +42 -0
  165. package/src/createGrid.test.ts +10 -0
  166. package/src/createGridState.svelte.ts +17 -0
  167. package/src/editors/cell-editors.test.ts +80 -0
  168. package/src/editors/cell-editors.ts +127 -0
  169. package/src/fill-patterns.test.ts +93 -0
  170. package/src/fill-patterns.ts +229 -0
  171. package/src/filtering/excel-filters.test.ts +131 -0
  172. package/src/filtering/excel-filters.ts +101 -0
  173. package/src/filtering/locale-filter.test.ts +220 -0
  174. package/src/flex-render.test.ts +143 -0
  175. package/src/flex-render.ts +3 -0
  176. package/src/group-aggregate.test.ts +48 -0
  177. package/src/index.ts +159 -0
  178. package/src/keyboard.test.ts +59 -0
  179. package/src/keyboard.ts +97 -0
  180. package/src/merge-objects.ts +48 -0
  181. package/src/named-views.test.ts +66 -0
  182. package/src/named-views.ts +120 -0
  183. package/src/new-features.test.ts +217 -0
  184. package/src/render-component.test.ts +51 -0
  185. package/src/render-component.ts +28 -0
  186. package/src/server-data-source.test.ts +88 -0
  187. package/src/server-data-source.ts +163 -0
  188. package/src/sparkline.test.ts +59 -0
  189. package/src/sparkline.ts +164 -0
  190. package/src/static-functions.ts +11 -0
  191. package/src/subscribe.test.ts +103 -0
  192. package/src/subscribe.ts +38 -0
  193. package/src/sv-grid-scrollbar.ts +347 -0
  194. package/src/svgrid-wrapper.types.ts +382 -0
  195. package/src/svgrid.api-extensions.test.ts +362 -0
  196. package/src/svgrid.api.test.ts +192 -0
  197. package/src/svgrid.behavior.test.ts +657 -0
  198. package/src/svgrid.column-reorder.test.ts +234 -0
  199. package/src/svgrid.features.test.ts +157 -0
  200. package/src/svgrid.interaction.test.ts +355 -0
  201. package/src/svgrid.locale-filtering.test.ts +259 -0
  202. package/src/svgrid.new-features.wrapper.test.ts +164 -0
  203. package/src/svgrid.row-pinning.test.ts +266 -0
  204. package/src/svgrid.set-filter.test.ts +270 -0
  205. package/src/svgrid.wrapper.test.ts +35 -0
  206. package/src/test-setup.ts +37 -0
  207. package/src/transaction.test.ts +100 -0
  208. package/src/virtualization/column-virtualizer.test.ts +27 -0
  209. package/src/virtualization/column-virtualizer.ts +30 -0
  210. package/src/virtualization/svelte-virtualizer.svelte.ts +24 -0
  211. package/src/virtualization/types.ts +30 -0
  212. package/src/virtualization/virtualizer.test.ts +47 -0
  213. package/src/virtualization/virtualizer.ts +270 -0
@@ -0,0 +1,653 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SvGridChart - renders a `ChartSpec` as inline SVG. Supports grouped +
4
+ * stacked bars, line, area, pie / donut, combo charts (per-series type),
5
+ * a secondary Y axis, signed Y domains, and axis titles. Interactive: a
6
+ * unified crosshair tooltip (hover a category -> all series at once),
7
+ * focus tooltips, a clickable legend that toggles series, optional data
8
+ * labels, and an `onSelect` drill hook. No external charting dependency.
9
+ */
10
+ import { buildChart, DEFAULT_PALETTE, type ChartSpec, type ChartSelection } from './chart'
11
+
12
+ type Props = {
13
+ spec: ChartSpec
14
+ /** Show the (clickable) legend. Default true. */
15
+ legend?: boolean
16
+ /** Enable tooltips + crosshair + legend toggling. Default true. */
17
+ interactive?: boolean
18
+ /** Draw the value on each bar / point / slice. Default false. */
19
+ dataLabels?: boolean
20
+ /** Format a value for tooltips, data labels, AND Y-axis ticks. */
21
+ formatValue?: (value: number) => string
22
+ /** Fired when a category / slice is clicked (drill into the grid). */
23
+ onSelect?: (selection: ChartSelection) => void
24
+ }
25
+ let {
26
+ spec,
27
+ legend = true,
28
+ interactive = true,
29
+ dataLabels = false,
30
+ formatValue,
31
+ onSelect,
32
+ }: Props = $props()
33
+
34
+ const fmt = (v: number) =>
35
+ formatValue
36
+ ? formatValue(v)
37
+ : Number.isFinite(v)
38
+ ? v.toLocaleString(undefined, { maximumFractionDigits: 2 })
39
+ : String(v)
40
+
41
+ let hidden = $state(new Set<string>())
42
+ // Isolate: double-clicking a legend chip shows ONLY that series/slice.
43
+ let isolated = $state<string | null>(null)
44
+ // Hover-dim: hovering a legend chip dims the other series (visual only).
45
+ let dimmed = $state<string | null>(null)
46
+ const palette = $derived(spec.palette ?? DEFAULT_PALETTE)
47
+ const uid = `svgc-${Math.random().toString(36).slice(2, 8)}`
48
+
49
+ const coloredSeries = $derived(
50
+ spec.series.map((s, i) => ({ ...s, color: s.color ?? palette[i % palette.length]! })),
51
+ )
52
+
53
+ // Effective hidden set = manual toggles, unless a series is isolated (then
54
+ // everything else is hidden).
55
+ const effectiveHidden = $derived.by(() => {
56
+ if (isolated == null) return hidden
57
+ const labels = spec.type === 'pie' ? spec.categories : spec.series.map((s) => s.label)
58
+ return new Set(labels.filter((l) => l !== isolated))
59
+ })
60
+
61
+ const visibleSpec = $derived.by<ChartSpec>(() => {
62
+ if (spec.type === 'pie') {
63
+ const s = coloredSeries[0]
64
+ if (!s) return spec
65
+ const values = s.values.map((v, i) => (effectiveHidden.has(spec.categories[i] ?? String(i)) ? 0 : v))
66
+ return { ...spec, series: [{ ...s, values }] }
67
+ }
68
+ return { ...spec, series: coloredSeries.filter((s) => !effectiveHidden.has(s.label)) }
69
+ })
70
+
71
+ const geo = $derived(buildChart(visibleSpec))
72
+ const isCartesian = $derived(spec.type !== 'pie')
73
+ const isScatter = $derived(spec.type === 'scatter')
74
+ const isHorizontal = $derived(geo.orientation === 'horizontal')
75
+ // Per-category band size along the main (category) axis: width when vertical,
76
+ // height when horizontal.
77
+ const slot = $derived(
78
+ (isHorizontal ? geo.plot.h : geo.plot.w) / Math.max(1, spec.categories.length),
79
+ )
80
+
81
+ // Opacity for a series when another legend chip is being hovered.
82
+ const dimOf = (seriesLabel: string) => (dimmed && dimmed !== seriesLabel ? 0.18 : 1)
83
+
84
+ const legendItems = $derived(
85
+ spec.type === 'pie'
86
+ ? spec.categories.map((label, i) => ({ label, color: palette[i % palette.length]!, off: hidden.has(label) }))
87
+ : coloredSeries.map((s) => ({ label: s.label, color: s.color, off: hidden.has(s.label) })),
88
+ )
89
+ // Legend overflow: collapse to the first N chips with a "+N more" toggle so a
90
+ // wide pivot (many series) doesn't flood the chart with legend rows.
91
+ const LEGEND_MAX = 10
92
+ let legendExpanded = $state(false)
93
+ const legendOverflow = $derived(legendItems.length > LEGEND_MAX)
94
+ const shownLegend = $derived(
95
+ legendOverflow && !legendExpanded ? legendItems.slice(0, LEGEND_MAX) : legendItems,
96
+ )
97
+
98
+ const isEmpty = $derived.by(() => {
99
+ if (spec.type === 'pie') return geo.slices.every((s) => s.value <= 0)
100
+ if (spec.type === 'scatter') return geo.scatterPoints.length === 0
101
+ return geo.bars.length === 0 && geo.lines.every((l) => l.points.every((p) => !p.defined))
102
+ })
103
+
104
+ function toggle(label: string) {
105
+ if (!interactive) return
106
+ if (isolated != null) { isolated = null; return }
107
+ const next = new Set(hidden)
108
+ if (next.has(label)) next.delete(label)
109
+ else next.add(label)
110
+ hidden = next
111
+ }
112
+ // Double-click a legend chip to isolate it (show only that one); double-click
113
+ // again (or the same chip) to clear the isolation.
114
+ function isolate(label: string) {
115
+ if (!interactive) return
116
+ isolated = isolated === label ? null : label
117
+ }
118
+
119
+ // ---- Screen-reader data table -----------------------------------------
120
+ // A visually-hidden table that conveys the same data to assistive tech.
121
+ const srTable = $derived.by(() => {
122
+ if (spec.type === 'pie') {
123
+ const s = coloredSeries[0]
124
+ return {
125
+ cols: ['Category', 'Value'],
126
+ rows: spec.categories.map((c, i) => [c, fmt(s?.values[i] ?? 0)]),
127
+ }
128
+ }
129
+ if (isScatter) {
130
+ const rows: string[][] = []
131
+ for (const s of coloredSeries) for (const p of s.points ?? []) rows.push([s.label, fmt(p.x), fmt(p.y), p.r != null ? fmt(p.r) : ''])
132
+ return { cols: ['Series', 'X', 'Y', 'Size'], rows }
133
+ }
134
+ return {
135
+ cols: ['Category', ...coloredSeries.map((s) => s.label)],
136
+ rows: spec.categories.map((c, i) => [c, ...coloredSeries.map((s) => fmt(s.values[i] ?? 0))]),
137
+ }
138
+ })
139
+ function truncate(s: string, n = 12): string {
140
+ return s.length > n ? s.slice(0, n - 1) + '…' : s
141
+ }
142
+ const yTickLabel = (value: number, fallback: string) => (formatValue ? formatValue(value) : fallback)
143
+
144
+ // ---- Tooltip + crosshair ----------------------------------------------
145
+ let chartEl: HTMLElement | null = $state(null)
146
+ type TipRow = { label?: string; color?: string; value: string }
147
+ let tip = $state<{ left: number; top: number; below: boolean; title: string; rows: TipRow[] } | null>(null)
148
+ let activeCat = $state<number | null>(null)
149
+
150
+ function showTip(clientX: number, clientY: number, title: string, rows: TipRow[]) {
151
+ if (!interactive || !chartEl || rows.length === 0) return
152
+ const r = chartEl.getBoundingClientRect()
153
+ const left = Math.max(70, Math.min(r.width - 70, clientX - r.left))
154
+ const top = clientY - r.top
155
+ tip = { left, top, below: top < 44, title, rows }
156
+ }
157
+
158
+ // Unified tooltip: every visible series' value at one category.
159
+ function catRows(i: number): TipRow[] {
160
+ return visibleSpec.series
161
+ .map((s) => ({ label: s.label, color: (s as { color?: string }).color, value: s.values[i] }))
162
+ .filter((r) => Number.isFinite(r.value))
163
+ .map((r) => ({ label: r.label, color: r.color, value: fmt(r.value as number) }))
164
+ }
165
+ function hoverCat(clientX: number, clientY: number, i: number) {
166
+ activeCat = i
167
+ showTip(clientX, clientY, spec.categories[i] ?? '', catRows(i))
168
+ }
169
+ function focusCat(el: Element, i: number) {
170
+ const b = el.getBoundingClientRect()
171
+ hoverCat(b.left + b.width / 2, b.top + 12, i)
172
+ }
173
+ function clearActive() {
174
+ activeCat = null
175
+ tip = null
176
+ }
177
+ function hoverSlice(clientX: number, clientY: number, label: string, value: number, pct: number) {
178
+ showTip(clientX, clientY, label, [{ value: `${fmt(value)} · ${pct.toFixed(1)}%` }])
179
+ }
180
+ function hoverDot(clientX: number, clientY: number, d: (typeof geo.scatterPoints)[number]) {
181
+ const rows: TipRow[] = [
182
+ { label: 'x', value: fmt(d.x) },
183
+ { label: 'y', value: fmt(d.y) },
184
+ ]
185
+ showTip(clientX, clientY, d.label || d.series, rows)
186
+ }
187
+
188
+ function select(category: string, series: string, value: number) {
189
+ onSelect?.({ category, series, value })
190
+ }
191
+ function onCatKey(e: KeyboardEvent, i: number) {
192
+ if (!onSelect) return
193
+ if (e.key === 'Enter' || e.key === ' ') {
194
+ e.preventDefault()
195
+ select(spec.categories[i] ?? '', '', 0)
196
+ }
197
+ }
198
+ function onSliceKey(e: KeyboardEvent, label: string, value: number) {
199
+ if (!onSelect) return
200
+ if (e.key === 'Enter' || e.key === ' ') {
201
+ e.preventDefault()
202
+ select(label, label, value)
203
+ }
204
+ }
205
+ </script>
206
+
207
+ <div class="sv-grid-chart" bind:this={chartEl}>
208
+ <svg
209
+ class="sv-grid-chart-svg"
210
+ class:is-interactive={interactive}
211
+ class:is-clickable={!!onSelect}
212
+ viewBox={`0 0 ${geo.width} ${geo.height}`}
213
+ width="100%"
214
+ role="img"
215
+ aria-label={`${spec.type} chart`}
216
+ aria-describedby={`${uid}-table`}
217
+ >
218
+ {#if isHorizontal}
219
+ <!-- Value axis (vertical gridlines + bottom labels) -->
220
+ {#each geo.valueTicks as t (t.label + t.x)}
221
+ <line class="sv-grid-chart-gridline" x1={t.x} y1={geo.plot.y} x2={t.x} y2={geo.plot.y + geo.plot.h} />
222
+ <text class="sv-grid-chart-axis" x={t.x} y={geo.plot.y + geo.plot.h + 14} text-anchor="middle">{t.label}</text>
223
+ {/each}
224
+ <!-- Category labels down the left -->
225
+ {#each geo.catTicks as t (t.value)}
226
+ <text class="sv-grid-chart-axis" x={geo.plot.x - 6} y={t.y + 3} text-anchor="end">{truncate(t.label, 18)}</text>
227
+ {/each}
228
+ {#if spec.yAxisTitle}
229
+ <text class="sv-grid-chart-axis-title" x={13} y={geo.plot.y + geo.plot.h / 2} text-anchor="middle" transform={`rotate(-90 13 ${geo.plot.y + geo.plot.h / 2})`}>{spec.yAxisTitle}</text>
230
+ {/if}
231
+ {#if spec.xAxisTitle}
232
+ <text class="sv-grid-chart-axis-title" x={geo.plot.x + geo.plot.w / 2} y={geo.height - 3} text-anchor="middle">{spec.xAxisTitle}</text>
233
+ {/if}
234
+ {:else if isCartesian}
235
+ {#each geo.yTicks as t (t.value)}
236
+ <line class="sv-grid-chart-gridline" class:is-zero={t.value === 0} x1={geo.plot.x} y1={t.y} x2={geo.plot.x + geo.plot.w} y2={t.y} />
237
+ <text class="sv-grid-chart-axis" x={geo.plot.x - 6} y={t.y + 3} text-anchor="end">{yTickLabel(t.value, t.label)}</text>
238
+ {/each}
239
+ {#if geo.hasRightAxis}
240
+ {#each geo.y2Ticks as t (t.value)}
241
+ <text class="sv-grid-chart-axis" x={geo.plot.x + geo.plot.w + 6} y={t.y + 3} text-anchor="start">{yTickLabel(t.value, t.label)}</text>
242
+ {/each}
243
+ {/if}
244
+ {#each geo.xTicks as t (t.label + t.x)}
245
+ {#if geo.xLabelRotated}
246
+ <text class="sv-grid-chart-axis" x={t.x} y={geo.plot.y + geo.plot.h + 12} text-anchor="end" transform={`rotate(-40 ${t.x} ${geo.plot.y + geo.plot.h + 12})`}>{truncate(t.label)}</text>
247
+ {:else}
248
+ <text class="sv-grid-chart-axis" x={t.x} y={geo.plot.y + geo.plot.h + 16} text-anchor="middle">{truncate(t.label, 16)}</text>
249
+ {/if}
250
+ {/each}
251
+ {#if spec.yAxisTitle}
252
+ <text class="sv-grid-chart-axis-title" x={13} y={geo.plot.y + geo.plot.h / 2} text-anchor="middle" transform={`rotate(-90 13 ${geo.plot.y + geo.plot.h / 2})`}>{spec.yAxisTitle}</text>
253
+ {/if}
254
+ {#if spec.y2AxisTitle}
255
+ <text class="sv-grid-chart-axis-title" x={geo.width - 5} y={geo.plot.y + geo.plot.h / 2} text-anchor="middle" transform={`rotate(90 ${geo.width - 5} ${geo.plot.y + geo.plot.h / 2})`}>{spec.y2AxisTitle}</text>
256
+ {/if}
257
+ {#if spec.xAxisTitle}
258
+ <text class="sv-grid-chart-axis-title" x={geo.plot.x + geo.plot.w / 2} y={geo.height - 3} text-anchor="middle">{spec.xAxisTitle}</text>
259
+ {/if}
260
+ {/if}
261
+
262
+ {#each geo.lines as line, li (line.label + li)}
263
+ <g style={`opacity:${dimOf(line.label)}`}>
264
+ {#if line.areaPath}
265
+ <path class="sv-grid-chart-area" d={line.areaPath} fill={line.color} fill-opacity="0.15" stroke="none" />
266
+ {/if}
267
+ <path class="sv-grid-chart-linepath" d={line.path} fill="none" stroke={line.color} stroke-width="2" stroke-linejoin="round" stroke-linecap="round" />
268
+ {#each line.points as pt, pi (pi)}
269
+ {#if pt.defined}
270
+ <circle class="sv-grid-chart-dot" class:is-active={activeCat === pi} cx={pt.x} cy={pt.y} r={activeCat === pi ? 4 : 3} fill={line.color} />
271
+ {#if dataLabels}
272
+ <text class="sv-grid-chart-datalabel" x={pt.x} y={pt.y - 7} text-anchor="middle">{fmt(pt.value)}</text>
273
+ {/if}
274
+ {/if}
275
+ {/each}
276
+ </g>
277
+ {/each}
278
+
279
+ {#each geo.bars as bar, bi (bi)}
280
+ <rect class="sv-grid-chart-bar" x={bar.x} y={bar.y} width={bar.w} height={bar.h} rx="1" fill={bar.color} style={`opacity:${dimOf(bar.series)}`} />
281
+ {#if dataLabels && isHorizontal && bar.w > 18}
282
+ <text class="sv-grid-chart-datalabel" class:on-bar={spec.stacked || spec.stacked100} x={spec.stacked || spec.stacked100 ? bar.x + bar.w / 2 : bar.value >= 0 ? bar.x + bar.w + 3 : bar.x - 3} y={bar.y + bar.h / 2 + 3} text-anchor={spec.stacked || spec.stacked100 ? 'middle' : bar.value >= 0 ? 'start' : 'end'}>{fmt(bar.value)}</text>
283
+ {:else if dataLabels && !isHorizontal && bar.h > 13}
284
+ <text class="sv-grid-chart-datalabel" class:on-bar={spec.stacked || spec.stacked100} x={bar.x + bar.w / 2} y={spec.stacked || spec.stacked100 ? bar.y + bar.h / 2 + 3 : bar.value >= 0 ? bar.y - 3 : bar.y + bar.h + 11} text-anchor="middle">{fmt(bar.value)}</text>
285
+ {/if}
286
+ {/each}
287
+
288
+ {#each geo.scatterPoints as dot, di (di)}
289
+ <!-- svelte-ignore a11y_no_static_element_interactions a11y_mouse_events_have_key_events -->
290
+ <circle
291
+ class="sv-grid-chart-scatter"
292
+ cx={dot.cx}
293
+ cy={dot.cy}
294
+ r={dot.r}
295
+ fill={dot.color}
296
+ fill-opacity="0.7"
297
+ stroke={dot.color}
298
+ style={`opacity:${dimOf(dot.series)}`}
299
+ onmousemove={(e) => hoverDot(e.clientX, e.clientY, dot)}
300
+ onmouseleave={clearActive}
301
+ />
302
+ {/each}
303
+
304
+ <!-- Reference / target lines paint on top so they read over the bars/areas. -->
305
+ {#each geo.referenceLines as ref, ri (ri)}
306
+ <line class="sv-grid-chart-refline" x1={geo.plot.x} y1={ref.y} x2={geo.plot.x + geo.plot.w} y2={ref.y} stroke={ref.color} stroke-dasharray={ref.dashed ? '5 4' : undefined} />
307
+ <text class="sv-grid-chart-reflabel" x={geo.plot.x + geo.plot.w - 3} y={ref.y - 3} text-anchor="end" fill={ref.color}>{ref.label}</text>
308
+ {/each}
309
+ {#each geo.referenceLinesV as ref, ri (ri)}
310
+ <line class="sv-grid-chart-refline" x1={ref.x} y1={geo.plot.y} x2={ref.x} y2={geo.plot.y + geo.plot.h} stroke={ref.color} stroke-dasharray={ref.dashed ? '5 4' : undefined} />
311
+ <text class="sv-grid-chart-reflabel" x={ref.x + 3} y={geo.plot.y + 9} text-anchor="start" fill={ref.color}>{ref.label}</text>
312
+ {/each}
313
+
314
+ {#if isCartesian && activeCat !== null}
315
+ {#if isHorizontal}
316
+ {@const cy = geo.plot.y + slot * activeCat + slot / 2}
317
+ <line class="sv-grid-chart-crosshair" x1={geo.plot.x} y1={cy} x2={geo.plot.x + geo.plot.w} y2={cy} />
318
+ {:else}
319
+ {@const cx = geo.plot.x + slot * activeCat + slot / 2}
320
+ <line class="sv-grid-chart-crosshair" x1={cx} y1={geo.plot.y} x2={cx} y2={geo.plot.y + geo.plot.h} />
321
+ {/if}
322
+ {/if}
323
+
324
+ {#each geo.slices as slice, si (si)}
325
+ <!-- svelte-ignore a11y_no_static_element_interactions a11y_mouse_events_have_key_events a11y_no_noninteractive_tabindex -->
326
+ <path
327
+ class="sv-grid-chart-slice"
328
+ d={slice.path}
329
+ fill={slice.color}
330
+ stroke="var(--sg-bg, #fff)"
331
+ stroke-width="1"
332
+ role={onSelect ? 'button' : 'presentation'}
333
+ tabindex={onSelect ? 0 : undefined}
334
+ aria-label={`${slice.label}: ${fmt(slice.value)}, ${slice.percent.toFixed(1)}%`}
335
+ onmousemove={(e) => hoverSlice(e.clientX, e.clientY, slice.label, slice.value, slice.percent)}
336
+ onmouseleave={clearActive}
337
+ onfocus={(e) => { const b = e.currentTarget.getBoundingClientRect(); hoverSlice(b.left + b.width / 2, b.top, slice.label, slice.value, slice.percent) }}
338
+ onblur={clearActive}
339
+ onclick={() => select(slice.label, slice.label, slice.value)}
340
+ onkeydown={(e) => onSliceKey(e, slice.label, slice.value)}
341
+ />
342
+ {#if dataLabels && slice.percent >= 6}
343
+ <text class="sv-grid-chart-datalabel on-bar" x={slice.cx} y={slice.cy} text-anchor="middle" dominant-baseline="middle">{slice.percent.toFixed(0)}%</text>
344
+ {/if}
345
+ {/each}
346
+
347
+ {#if geo.donut}
348
+ <text class="sv-grid-chart-donut-total" x={geo.donut.cx} y={geo.donut.cy - 4} text-anchor="middle">{fmt(geo.donut.total)}</text>
349
+ <text class="sv-grid-chart-donut-label" x={geo.donut.cx} y={geo.donut.cy + 11} text-anchor="middle">Total</text>
350
+ {/if}
351
+
352
+ {#if isCartesian && !isScatter}
353
+ <!-- Per-category hover/focus zones drive the unified crosshair tooltip. -->
354
+ {#each spec.categories as cat, i (cat + i)}
355
+ <!-- svelte-ignore a11y_no_static_element_interactions a11y_mouse_events_have_key_events a11y_no_noninteractive_tabindex -->
356
+ <rect
357
+ class="sv-grid-chart-cat-hit"
358
+ x={isHorizontal ? geo.plot.x : geo.plot.x + slot * i}
359
+ y={isHorizontal ? geo.plot.y + slot * i : geo.plot.y}
360
+ width={isHorizontal ? geo.plot.w : slot}
361
+ height={isHorizontal ? slot : geo.plot.h}
362
+ role={onSelect ? 'button' : 'presentation'}
363
+ tabindex={onSelect ? 0 : undefined}
364
+ aria-label={`${cat}: ${catRows(i).map((r) => `${r.label} ${r.value}`).join(', ')}`}
365
+ onmousemove={(e) => hoverCat(e.clientX, e.clientY, i)}
366
+ onmouseleave={clearActive}
367
+ onfocus={(e) => focusCat(e.currentTarget, i)}
368
+ onblur={clearActive}
369
+ onclick={() => select(cat, '', 0)}
370
+ onkeydown={(e) => onCatKey(e, i)}
371
+ />
372
+ {/each}
373
+ {/if}
374
+ </svg>
375
+
376
+ {#if isEmpty}
377
+ <div class="sv-grid-chart-empty">No data</div>
378
+ {/if}
379
+
380
+ {#if tip}
381
+ <div class="sv-grid-chart-tooltip" class:is-below={tip.below} style={`left:${tip.left}px; top:${tip.top}px;`}>
382
+ <span class="sv-grid-chart-tooltip-title">{tip.title}</span>
383
+ {#each tip.rows as r, ri (ri)}
384
+ <span class="sv-grid-chart-tooltip-row">
385
+ {#if r.color}<span class="sv-grid-chart-tooltip-dot" style={`background:${r.color}`}></span>{/if}
386
+ {#if r.label}<span class="sv-grid-chart-tooltip-row-label">{r.label}</span>{/if}
387
+ <span class="sv-grid-chart-tooltip-row-value">{r.value}</span>
388
+ </span>
389
+ {/each}
390
+ </div>
391
+ {/if}
392
+
393
+ {#if legend && legendItems.length}
394
+ <div class="sv-grid-chart-legend">
395
+ {#each shownLegend as item (item.label)}
396
+ <button
397
+ type="button"
398
+ class="sv-grid-chart-legend-item"
399
+ class:is-off={item.off}
400
+ class:is-isolated={isolated === item.label}
401
+ disabled={!interactive}
402
+ aria-pressed={!item.off}
403
+ onclick={() => toggle(item.label)}
404
+ ondblclick={() => isolate(item.label)}
405
+ onpointerenter={() => interactive && (dimmed = item.label)}
406
+ onpointerleave={() => (dimmed = null)}
407
+ title={interactive ? `${item.off ? 'Show' : 'Hide'} ${item.label} · double-click to isolate` : item.label}
408
+ >
409
+ <span class="sv-grid-chart-swatch" style={`background:${item.off ? 'transparent' : item.color}; border-color:${item.color}`}></span>
410
+ {item.label}
411
+ </button>
412
+ {/each}
413
+ {#if legendOverflow}
414
+ <button type="button" class="sv-grid-chart-legend-more" onclick={() => (legendExpanded = !legendExpanded)}>
415
+ {legendExpanded ? 'Show less' : `+${legendItems.length - LEGEND_MAX} more`}
416
+ </button>
417
+ {/if}
418
+ </div>
419
+ {/if}
420
+
421
+ <!-- Visually-hidden data table: the same data for assistive technology. -->
422
+ <table id={`${uid}-table`} class="sv-grid-chart-sr-only">
423
+ <caption>{spec.type} chart data</caption>
424
+ <thead>
425
+ <tr>{#each srTable.cols as c (c)}<th>{c}</th>{/each}</tr>
426
+ </thead>
427
+ <tbody>
428
+ {#each srTable.rows as row, ri (ri)}
429
+ <tr>{#each row as cell, ci (ci)}<td>{cell}</td>{/each}</tr>
430
+ {/each}
431
+ </tbody>
432
+ </table>
433
+ </div>
434
+
435
+ <style>
436
+ .sv-grid-chart {
437
+ position: relative;
438
+ display: flex;
439
+ flex-direction: column;
440
+ gap: 8px;
441
+ width: 100%;
442
+ }
443
+ .sv-grid-chart-svg {
444
+ display: block;
445
+ width: 100%;
446
+ height: auto;
447
+ }
448
+ .sv-grid-chart-gridline {
449
+ stroke: var(--sg-border, #e2e8f0);
450
+ stroke-width: 1;
451
+ opacity: 0.6;
452
+ }
453
+ .sv-grid-chart-gridline.is-zero {
454
+ stroke: var(--sg-muted, #94a3b8);
455
+ opacity: 0.9;
456
+ }
457
+ .sv-grid-chart-axis {
458
+ fill: var(--sg-muted, #64748b);
459
+ font-size: 10px;
460
+ font-family: inherit;
461
+ }
462
+ .sv-grid-chart-axis-title {
463
+ fill: var(--sg-muted, #64748b);
464
+ font-size: 11px;
465
+ font-weight: 600;
466
+ font-family: inherit;
467
+ }
468
+ .sv-grid-chart-crosshair {
469
+ stroke: var(--sg-accent, #2563eb);
470
+ stroke-width: 1;
471
+ stroke-dasharray: 3 3;
472
+ opacity: 0.65;
473
+ pointer-events: none;
474
+ }
475
+ .sv-grid-chart-refline {
476
+ stroke-width: 1.5;
477
+ opacity: 0.9;
478
+ pointer-events: none;
479
+ }
480
+ .sv-grid-chart-reflabel {
481
+ font-size: 9.5px;
482
+ font-weight: 700;
483
+ font-family: inherit;
484
+ pointer-events: none;
485
+ }
486
+ .sv-grid-chart-scatter {
487
+ transition: opacity 0.2s ease;
488
+ stroke-width: 1;
489
+ }
490
+ .is-clickable .sv-grid-chart-scatter,
491
+ .sv-grid-chart-scatter:hover {
492
+ cursor: pointer;
493
+ }
494
+ .sv-grid-chart-cat-hit {
495
+ fill: transparent;
496
+ }
497
+ .sv-grid-chart-cat-hit:focus-visible,
498
+ .sv-grid-chart-slice:focus-visible {
499
+ outline: 2px solid var(--sg-accent, #2563eb);
500
+ outline-offset: 1px;
501
+ }
502
+ .sv-grid-chart-datalabel {
503
+ fill: var(--sg-fg, #0f172a);
504
+ font-size: 9.5px;
505
+ font-weight: 600;
506
+ font-variant-numeric: tabular-nums;
507
+ pointer-events: none;
508
+ }
509
+ .sv-grid-chart-datalabel.on-bar {
510
+ fill: #fff;
511
+ }
512
+ .sv-grid-chart-donut-total {
513
+ fill: var(--sg-fg, #0f172a);
514
+ font-size: 16px;
515
+ font-weight: 800;
516
+ font-variant-numeric: tabular-nums;
517
+ }
518
+ .sv-grid-chart-donut-label {
519
+ fill: var(--sg-muted, #64748b);
520
+ font-size: 10px;
521
+ }
522
+ .sv-grid-chart-bar {
523
+ transition: x 0.3s ease, y 0.3s ease, width 0.3s ease, height 0.3s ease;
524
+ }
525
+ .sv-grid-chart-dot {
526
+ transition: cx 0.3s ease, cy 0.3s ease;
527
+ }
528
+ .is-clickable .sv-grid-chart-cat-hit,
529
+ .is-clickable .sv-grid-chart-slice {
530
+ cursor: pointer;
531
+ }
532
+
533
+ .sv-grid-chart-empty {
534
+ position: absolute;
535
+ inset: 0;
536
+ display: flex;
537
+ align-items: center;
538
+ justify-content: center;
539
+ color: var(--sg-muted, #94a3b8);
540
+ font-size: 13px;
541
+ pointer-events: none;
542
+ }
543
+
544
+ .sv-grid-chart-tooltip {
545
+ position: absolute;
546
+ z-index: 10;
547
+ transform: translate(-50%, calc(-100% - 12px));
548
+ pointer-events: none;
549
+ white-space: nowrap;
550
+ padding: 6px 9px;
551
+ border-radius: 6px;
552
+ background: var(--sg-fg, #0f172a);
553
+ color: var(--sg-bg, #fff);
554
+ font-size: 11.5px;
555
+ line-height: 1.4;
556
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28);
557
+ display: flex;
558
+ flex-direction: column;
559
+ gap: 1px;
560
+ }
561
+ .sv-grid-chart-tooltip.is-below {
562
+ transform: translate(-50%, 16px);
563
+ }
564
+ .sv-grid-chart-tooltip-title {
565
+ opacity: 0.75;
566
+ font-size: 10.5px;
567
+ margin-bottom: 1px;
568
+ }
569
+ .sv-grid-chart-tooltip-row {
570
+ display: flex;
571
+ align-items: center;
572
+ gap: 6px;
573
+ }
574
+ .sv-grid-chart-tooltip-dot {
575
+ width: 8px;
576
+ height: 8px;
577
+ border-radius: 2px;
578
+ flex-shrink: 0;
579
+ }
580
+ .sv-grid-chart-tooltip-row-label {
581
+ opacity: 0.85;
582
+ }
583
+ .sv-grid-chart-tooltip-row-value {
584
+ margin-left: auto;
585
+ padding-left: 10px;
586
+ font-weight: 700;
587
+ font-variant-numeric: tabular-nums;
588
+ }
589
+
590
+ .sv-grid-chart-legend {
591
+ display: flex;
592
+ flex-wrap: wrap;
593
+ gap: 6px 12px;
594
+ font-size: 12px;
595
+ color: var(--sg-fg, #0f172a);
596
+ }
597
+ .sv-grid-chart-legend-item {
598
+ display: inline-flex;
599
+ align-items: center;
600
+ gap: 6px;
601
+ border: 0;
602
+ background: transparent;
603
+ color: inherit;
604
+ font: inherit;
605
+ padding: 1px 2px;
606
+ border-radius: 4px;
607
+ cursor: pointer;
608
+ }
609
+ .sv-grid-chart-legend-item:disabled {
610
+ cursor: default;
611
+ }
612
+ .sv-grid-chart-legend-item.is-off {
613
+ color: var(--sg-muted, #94a3b8);
614
+ text-decoration: line-through;
615
+ }
616
+ .sv-grid-chart-legend-item.is-isolated {
617
+ background: var(--sg-header-bg, #f1f5f9);
618
+ font-weight: 700;
619
+ }
620
+ .sv-grid-chart-legend-more {
621
+ border: 1px dashed var(--sg-border, #cbd5e1);
622
+ background: transparent;
623
+ color: var(--sg-muted, #64748b);
624
+ font: inherit;
625
+ font-size: 11px;
626
+ padding: 1px 7px;
627
+ border-radius: 999px;
628
+ cursor: pointer;
629
+ }
630
+ .sv-grid-chart-legend-more:hover {
631
+ color: var(--sg-fg, #0f172a);
632
+ border-color: var(--sg-accent, #2563eb);
633
+ }
634
+ .sv-grid-chart-sr-only {
635
+ position: absolute;
636
+ width: 1px;
637
+ height: 1px;
638
+ padding: 0;
639
+ margin: -1px;
640
+ overflow: hidden;
641
+ clip: rect(0 0 0 0);
642
+ white-space: nowrap;
643
+ border: 0;
644
+ }
645
+ .sv-grid-chart-swatch {
646
+ width: 12px;
647
+ height: 12px;
648
+ border-radius: 3px;
649
+ border: 1.5px solid transparent;
650
+ display: inline-block;
651
+ box-sizing: border-box;
652
+ }
653
+ </style>