@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
package/dist/chart.js ADDED
@@ -0,0 +1,732 @@
1
+ export const DEFAULT_PALETTE = [
2
+ '#2563eb',
3
+ '#16a34a',
4
+ '#f59e0b',
5
+ '#ef4444',
6
+ '#8b5cf6',
7
+ '#0ea5e9',
8
+ '#ec4899',
9
+ '#14b8a6',
10
+ ];
11
+ function round(n) {
12
+ return Math.round(n * 100) / 100;
13
+ }
14
+ function niceNum(range, roundIt) {
15
+ if (range <= 0)
16
+ return 1;
17
+ const exp = Math.floor(Math.log10(range));
18
+ const f = range / Math.pow(10, exp);
19
+ let nf;
20
+ if (roundIt)
21
+ nf = f < 1.5 ? 1 : f < 3 ? 2 : f < 7 ? 5 : 10;
22
+ else
23
+ nf = f <= 1 ? 1 : f <= 2 ? 2 : f <= 5 ? 5 : 10;
24
+ return nf * Math.pow(10, exp);
25
+ }
26
+ /** Round a [min,max] domain out to nice tick boundaries. */
27
+ export function niceScale(min, max, tickCount = 4) {
28
+ if (!Number.isFinite(min) || !Number.isFinite(max)) {
29
+ min = 0;
30
+ max = 1;
31
+ }
32
+ if (min === max) {
33
+ if (min === 0)
34
+ max = 1;
35
+ else {
36
+ min = Math.min(0, min);
37
+ max = Math.max(0, max);
38
+ }
39
+ if (min === max)
40
+ max = min + 1;
41
+ }
42
+ const range = niceNum(max - min, false);
43
+ const step = niceNum(range / Math.max(1, tickCount), true);
44
+ const nMin = Math.floor(min / step) * step;
45
+ const nMax = Math.ceil(max / step) * step;
46
+ const ticks = [];
47
+ for (let v = nMin; v <= nMax + step * 0.5; v += step)
48
+ ticks.push(round(v));
49
+ return { min: nMin, max: nMax, step, ticks };
50
+ }
51
+ function fmtTick(n) {
52
+ const abs = Math.abs(n);
53
+ if (abs >= 1000000)
54
+ return `${(n / 1000000).toFixed(abs % 1000000 ? 1 : 0)}M`;
55
+ if (abs >= 1000)
56
+ return `${(n / 1000).toFixed(abs % 1000 ? 1 : 0)}k`;
57
+ return String(Math.round(n * 100) / 100);
58
+ }
59
+ const DAY = 86400000;
60
+ /** Nice date-tick timestamps across [min, max]. */
61
+ function dateTicks(tMin, tMax) {
62
+ const span = tMax - tMin;
63
+ const step = span <= 7 * DAY ? DAY : span <= 70 * DAY ? 7 * DAY : span <= 800 * DAY ? 30 * DAY : 365 * DAY;
64
+ const ticks = [];
65
+ for (let t = Math.ceil(tMin / step) * step; t <= tMax + 1; t += step)
66
+ ticks.push(t);
67
+ if (!ticks.length)
68
+ ticks.push(tMin, tMax);
69
+ return ticks;
70
+ }
71
+ function fmtDate(t, span) {
72
+ const d = new Date(t);
73
+ if (span <= 70 * DAY)
74
+ return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
75
+ if (span <= 800 * DAY)
76
+ return d.toLocaleDateString(undefined, { month: 'short', year: '2-digit' });
77
+ return String(d.getFullYear());
78
+ }
79
+ /** Data domain for one axis, honoring stacking of its bar/area series. */
80
+ function axisDomain(list, categories, stacked, extra = []) {
81
+ let dMin = Infinity;
82
+ let dMax = -Infinity;
83
+ const note = (v) => {
84
+ if (!Number.isFinite(v))
85
+ return;
86
+ if (v < dMin)
87
+ dMin = v;
88
+ if (v > dMax)
89
+ dMax = v;
90
+ };
91
+ for (const v of extra)
92
+ note(v);
93
+ const stackable = list.filter((s) => s.kind === 'bar' || s.kind === 'area');
94
+ const lines = list.filter((s) => s.kind === 'line');
95
+ if (stacked && stackable.length) {
96
+ for (let i = 0; i < categories.length; i += 1) {
97
+ let pos = 0;
98
+ let neg = 0;
99
+ for (const s of stackable) {
100
+ const v = s.values[i] ?? 0;
101
+ if (v >= 0)
102
+ pos += v;
103
+ else
104
+ neg += v;
105
+ }
106
+ note(pos);
107
+ note(neg);
108
+ }
109
+ }
110
+ else {
111
+ for (const s of stackable)
112
+ for (const v of s.values)
113
+ note(v);
114
+ }
115
+ for (const s of lines)
116
+ for (const v of s.values)
117
+ note(v);
118
+ if (dMin === Infinity) {
119
+ dMin = 0;
120
+ dMax = 1;
121
+ }
122
+ // Bar / area charts read against a zero baseline, so always include 0.
123
+ if (stackable.length) {
124
+ dMin = Math.min(dMin, 0);
125
+ dMax = Math.max(dMax, 0);
126
+ }
127
+ return niceScale(dMin, dMax);
128
+ }
129
+ export function buildChart(spec) {
130
+ const width = spec.width ?? 520;
131
+ const height = spec.height ?? 300;
132
+ const palette = spec.palette ?? DEFAULT_PALETTE;
133
+ const stacked = !!(spec.stacked || spec.stacked100);
134
+ const series = spec.series.map((s, i) => ({
135
+ ...s,
136
+ color: s.color ?? palette[i % palette.length],
137
+ kind: (s.type ?? (spec.type === 'pie' || spec.type === 'scatter' ? 'bar' : spec.type)),
138
+ axis: s.axis ?? 'left',
139
+ }));
140
+ const legend = series.map((s) => ({ label: s.label, color: s.color }));
141
+ const empty = {
142
+ type: spec.type,
143
+ width,
144
+ height,
145
+ plot: { x: 0, y: 0, w: width, h: height },
146
+ bars: [],
147
+ lines: [],
148
+ slices: [],
149
+ yTicks: [],
150
+ y2Ticks: [],
151
+ hasRightAxis: false,
152
+ xTicks: [],
153
+ xLabelRotated: false,
154
+ legend,
155
+ donut: null,
156
+ referenceLines: [],
157
+ scatterPoints: [],
158
+ orientation: 'vertical',
159
+ valueTicks: [],
160
+ catTicks: [],
161
+ referenceLinesV: [],
162
+ };
163
+ if (spec.type === 'pie') {
164
+ const s = series[0];
165
+ if (!s)
166
+ return empty;
167
+ const total = s.values.reduce((a, b) => a + Math.max(0, b), 0) || 1;
168
+ const cx = width / 2;
169
+ const cy = height / 2;
170
+ const r = Math.min(width, height) / 2 - 10;
171
+ const innerFrac = Math.min(0.9, Math.max(0, spec.innerRadius ?? 0));
172
+ const ir = r * innerFrac;
173
+ let angle = -Math.PI / 2;
174
+ const slices = s.values.map((v, i) => {
175
+ const frac = Math.max(0, v) / total;
176
+ const a0 = angle;
177
+ const a1 = angle + frac * Math.PI * 2;
178
+ angle = a1;
179
+ const large = a1 - a0 > Math.PI ? 1 : 0;
180
+ const mid = (a0 + a1) / 2;
181
+ const labelR = (r + ir) / 2 || r * 0.6;
182
+ const ox0 = cx + r * Math.cos(a0);
183
+ const oy0 = cy + r * Math.sin(a0);
184
+ const ox1 = cx + r * Math.cos(a1);
185
+ const oy1 = cy + r * Math.sin(a1);
186
+ let path;
187
+ if (frac >= 0.999) {
188
+ path = ir
189
+ ? `M${round(cx - r)},${round(cy)} A${r},${r} 0 1 1 ${round(cx + r)},${round(cy)} A${r},${r} 0 1 1 ${round(cx - r)},${round(cy)} Z` +
190
+ `M${round(cx - ir)},${round(cy)} A${ir},${ir} 0 1 0 ${round(cx + ir)},${round(cy)} A${ir},${ir} 0 1 0 ${round(cx - ir)},${round(cy)} Z`
191
+ : `M${round(cx - r)},${round(cy)} A${r},${r} 0 1 1 ${round(cx + r)},${round(cy)} A${r},${r} 0 1 1 ${round(cx - r)},${round(cy)} Z`;
192
+ }
193
+ else if (ir > 0) {
194
+ const ix0 = cx + ir * Math.cos(a0);
195
+ const iy0 = cy + ir * Math.sin(a0);
196
+ const ix1 = cx + ir * Math.cos(a1);
197
+ const iy1 = cy + ir * Math.sin(a1);
198
+ path =
199
+ `M${round(ox0)},${round(oy0)} A${r},${r} 0 ${large} 1 ${round(ox1)},${round(oy1)} ` +
200
+ `L${round(ix1)},${round(iy1)} A${ir},${ir} 0 ${large} 0 ${round(ix0)},${round(iy0)} Z`;
201
+ }
202
+ else {
203
+ path = `M${round(cx)},${round(cy)} L${round(ox0)},${round(oy0)} A${r},${r} 0 ${large} 1 ${round(ox1)},${round(oy1)} Z`;
204
+ }
205
+ return {
206
+ path,
207
+ color: palette[i % palette.length],
208
+ label: spec.categories[i] ?? String(i),
209
+ value: v,
210
+ percent: frac * 100,
211
+ cx: round(cx + labelR * Math.cos(mid)),
212
+ cy: round(cy + labelR * Math.sin(mid)),
213
+ };
214
+ });
215
+ return {
216
+ ...empty,
217
+ slices,
218
+ legend: spec.categories.map((label, i) => ({ label, color: palette[i % palette.length] })),
219
+ donut: ir > 0 ? { cx: round(cx), cy: round(cy), r: round(ir), total: s.values.reduce((a, b) => a + Math.max(0, b), 0) } : null,
220
+ };
221
+ }
222
+ if (spec.type === 'scatter') {
223
+ const padL = 48 + (spec.yAxisTitle ? 16 : 0);
224
+ const padR = 12;
225
+ const padT = 10;
226
+ const padB = 28 + (spec.xAxisTitle ? 16 : 0);
227
+ const plotW = Math.max(1, width - padL - padR);
228
+ const plotH = Math.max(1, height - padT - padB);
229
+ const plot = { x: padL, y: padT, w: plotW, h: plotH };
230
+ let xMin = Infinity;
231
+ let xMax = -Infinity;
232
+ let yMin = Infinity;
233
+ let yMax = -Infinity;
234
+ let rMin = Infinity;
235
+ let rMax = -Infinity;
236
+ for (const s of series) {
237
+ for (const pt of s.points ?? []) {
238
+ if (Number.isFinite(pt.x)) {
239
+ xMin = Math.min(xMin, pt.x);
240
+ xMax = Math.max(xMax, pt.x);
241
+ }
242
+ if (Number.isFinite(pt.y)) {
243
+ yMin = Math.min(yMin, pt.y);
244
+ yMax = Math.max(yMax, pt.y);
245
+ }
246
+ if (pt.r != null && Number.isFinite(pt.r)) {
247
+ rMin = Math.min(rMin, pt.r);
248
+ rMax = Math.max(rMax, pt.r);
249
+ }
250
+ }
251
+ }
252
+ if (xMin === Infinity)
253
+ return { ...empty, plot };
254
+ const xDom = niceScale(xMin, xMax);
255
+ const yDom = niceScale(yMin, yMax);
256
+ const hasR = rMax > rMin;
257
+ const xOf = (v) => round(padL + ((v - xDom.min) / (xDom.max - xDom.min || 1)) * plotW);
258
+ const yOf = (v) => round(padT + plotH - ((v - yDom.min) / (yDom.max - yDom.min || 1)) * plotH);
259
+ const rOf = (r) => hasR && r != null && Number.isFinite(r)
260
+ ? round(4 + ((r - rMin) / (rMax - rMin || 1)) * 14)
261
+ : 5;
262
+ const scatterPoints = [];
263
+ for (const s of series) {
264
+ for (const pt of s.points ?? []) {
265
+ if (!Number.isFinite(pt.x) || !Number.isFinite(pt.y))
266
+ continue;
267
+ scatterPoints.push({
268
+ cx: xOf(pt.x),
269
+ cy: yOf(pt.y),
270
+ r: rOf(pt.r),
271
+ color: s.color,
272
+ label: pt.label ?? '',
273
+ series: s.label,
274
+ x: pt.x,
275
+ y: pt.y,
276
+ });
277
+ }
278
+ }
279
+ const referenceLines = (spec.referenceLines ?? []).map((ref) => ({
280
+ y: yOf(ref.value),
281
+ label: ref.label ?? fmtTick(ref.value),
282
+ color: ref.color ?? '#ef4444',
283
+ dashed: ref.dashed !== false,
284
+ }));
285
+ return {
286
+ ...empty,
287
+ plot,
288
+ scatterPoints,
289
+ referenceLines,
290
+ yTicks: yDom.ticks.map((value) => ({ value, y: yOf(value), label: fmtTick(value) })),
291
+ xTicks: xDom.ticks.map((value) => ({ label: fmtTick(value), x: xOf(value) })),
292
+ };
293
+ }
294
+ // ---- Horizontal bars ----------------------------------------------------
295
+ // Categories run down the left, bars grow rightward. Bars-only (no combo).
296
+ const horizontal = spec.orientation === 'horizontal' && series.length > 0 && series.every((s) => s.kind === 'bar');
297
+ if (horizontal) {
298
+ const maxLabel = spec.categories.reduce((m, c) => Math.max(m, c.length), 0);
299
+ const padL = Math.min(150, 18 + maxLabel * 6.4) + (spec.yAxisTitle ? 16 : 0);
300
+ const padR = 16;
301
+ const padT = 8;
302
+ const padB = 26 + (spec.xAxisTitle ? 16 : 0);
303
+ const plotW = Math.max(1, width - padL - padR);
304
+ const plotH = Math.max(1, height - padT - padB);
305
+ const plot = { x: padL, y: padT, w: plotW, h: plotH };
306
+ const refs = (spec.referenceLines ?? []).map((r) => r.value);
307
+ const dom = spec.stacked100 ? niceScale(0, 100) : axisDomain(series, spec.categories, stacked, refs);
308
+ const xOf = (v) => round(padL + ((v - dom.min) / (dom.max - dom.min || 1)) * plotW);
309
+ const n = spec.categories.length;
310
+ const slot = plotH / Math.max(1, n);
311
+ const groupPad = slot * 0.2;
312
+ const inner = slot - groupPad;
313
+ const bandTop = (i) => padT + slot * i + groupPad / 2;
314
+ const xBase = xOf(Math.min(Math.max(0, dom.min), dom.max));
315
+ const bars = [];
316
+ if (stacked) {
317
+ const totals = spec.stacked100
318
+ ? spec.categories.map((_, i) => series.reduce((sum, s) => sum + Math.abs(Number.isFinite(s.values[i]) ? s.values[i] : 0), 0) || 1)
319
+ : null;
320
+ const pos = new Array(n).fill(0);
321
+ const neg = new Array(n).fill(0);
322
+ for (const s of series) {
323
+ s.values.forEach((v, i) => {
324
+ if (!Number.isFinite(v))
325
+ return;
326
+ const vp = totals ? (v / totals[i]) * 100 : v;
327
+ let xL;
328
+ let xR;
329
+ if (vp >= 0) {
330
+ xL = xOf(pos[i]);
331
+ xR = xOf(pos[i] + vp);
332
+ pos[i] += vp;
333
+ }
334
+ else {
335
+ xL = xOf(neg[i] + vp);
336
+ xR = xOf(neg[i]);
337
+ neg[i] += vp;
338
+ }
339
+ bars.push({
340
+ x: Math.min(xL, xR),
341
+ y: round(bandTop(i)),
342
+ w: round(Math.abs(xR - xL)),
343
+ h: round(Math.max(1, inner)),
344
+ color: s.color,
345
+ label: spec.categories[i] ?? String(i),
346
+ series: s.label,
347
+ value: v,
348
+ });
349
+ });
350
+ }
351
+ }
352
+ else {
353
+ const barH = inner / series.length;
354
+ series.forEach((s, bi) => {
355
+ s.values.forEach((v, i) => {
356
+ if (!Number.isFinite(v))
357
+ return;
358
+ const xV = xOf(v);
359
+ bars.push({
360
+ x: Math.min(xV, xBase),
361
+ y: round(bandTop(i) + barH * bi),
362
+ w: round(Math.max(1, Math.abs(xV - xBase))),
363
+ h: round(Math.max(1, barH - 1)),
364
+ color: s.color,
365
+ label: spec.categories[i] ?? String(i),
366
+ series: s.label,
367
+ value: v,
368
+ });
369
+ });
370
+ });
371
+ }
372
+ const valueTicks = dom.ticks.map((value) => ({
373
+ label: spec.stacked100 ? `${fmtTick(value)}%` : fmtTick(value),
374
+ x: xOf(value),
375
+ }));
376
+ const catTicks = spec.categories.map((label, i) => ({
377
+ value: i,
378
+ y: round(bandTop(i) + inner / 2),
379
+ label,
380
+ }));
381
+ const referenceLinesV = (spec.referenceLines ?? []).map((ref) => ({
382
+ x: xOf(ref.value),
383
+ label: ref.label ?? fmtTick(ref.value),
384
+ color: ref.color ?? '#ef4444',
385
+ dashed: ref.dashed !== false,
386
+ }));
387
+ return {
388
+ ...empty,
389
+ plot,
390
+ bars,
391
+ orientation: 'horizontal',
392
+ valueTicks,
393
+ catTicks,
394
+ referenceLinesV,
395
+ xLabelRotated: false,
396
+ };
397
+ }
398
+ // ---- Cartesian (bar / line / area, possibly combo + dual axis) ----------
399
+ const leftSeries = series.filter((s) => s.axis === 'left');
400
+ const rightSeries = series.filter((s) => s.axis === 'right');
401
+ const hasRightAxis = rightSeries.length > 0;
402
+ const maxLabel = spec.categories.reduce((m, c) => Math.max(m, c.length), 0);
403
+ const xLabelRotated = spec.categories.length > 8 || maxLabel > 9;
404
+ const padL = 48 + (spec.yAxisTitle ? 16 : 0);
405
+ const padR = (hasRightAxis ? 48 : 12) + (spec.y2AxisTitle ? 16 : 0);
406
+ const padT = 10;
407
+ const padB = (xLabelRotated ? 54 : 28) + (spec.xAxisTitle ? 16 : 0);
408
+ const plotW = Math.max(1, width - padL - padR);
409
+ const plotH = Math.max(1, height - padT - padB);
410
+ const plot = { x: padL, y: padT, w: plotW, h: plotH };
411
+ const refsLeft = (spec.referenceLines ?? []).filter((r) => r.axis !== 'right').map((r) => r.value);
412
+ const refsRight = (spec.referenceLines ?? []).filter((r) => r.axis === 'right').map((r) => r.value);
413
+ const leftDom = spec.stacked100
414
+ ? niceScale(0, 100)
415
+ : axisDomain(leftSeries, spec.categories, stacked, refsLeft);
416
+ const rightDom = hasRightAxis
417
+ ? spec.stacked100
418
+ ? niceScale(0, 100)
419
+ : axisDomain(rightSeries, spec.categories, stacked, refsRight)
420
+ : null;
421
+ const yOf = (dom, v) => round(padT + plotH - ((v - dom.min) / (dom.max - dom.min || 1)) * plotH);
422
+ const yLeft = (v) => yOf(leftDom, v);
423
+ const yRight = (v) => yOf(rightDom ?? leftDom, v);
424
+ const domOf = (s) => (s.axis === 'right' ? rightDom ?? leftDom : leftDom);
425
+ const n = spec.categories.length;
426
+ const slot = plotW / Math.max(1, n);
427
+ // X positions. A time axis spaces points by actual time (irregular gaps);
428
+ // a category axis is uniform. (Bars stay uniform either way.)
429
+ const timeVals = spec.xType === 'time' ? spec.categories.map((c) => Date.parse(c)) : null;
430
+ const timeOk = !!timeVals && timeVals.some((t) => Number.isFinite(t));
431
+ const tMin = timeOk ? Math.min(...timeVals.filter(Number.isFinite)) : 0;
432
+ const tSpan = timeOk ? Math.max(...timeVals.filter(Number.isFinite)) - tMin || 1 : 1;
433
+ const xCenter = (i) => timeOk && Number.isFinite(timeVals[i])
434
+ ? round(padL + ((timeVals[i] - tMin) / tSpan) * plotW)
435
+ : round(padL + slot * i + slot / 2);
436
+ const xTicks = timeOk
437
+ ? dateTicks(tMin, tMin + tSpan).map((t) => ({
438
+ label: fmtDate(t, tSpan),
439
+ x: round(padL + ((t - tMin) / tSpan) * plotW),
440
+ }))
441
+ : spec.categories.map((label, i) => ({ label, x: xCenter(i) }));
442
+ const barSeries = series.filter((s) => s.kind === 'bar');
443
+ const bars = [];
444
+ if (barSeries.length) {
445
+ const groupPad = slot * 0.2;
446
+ if (stacked) {
447
+ const inner = slot - groupPad;
448
+ const x0 = (i) => padL + slot * i + groupPad / 2;
449
+ // Stack independently per axis so dual-axis stacks line up to their own scale.
450
+ for (const axis of ['left', 'right']) {
451
+ const axisBars = barSeries.filter((s) => s.axis === axis);
452
+ if (!axisBars.length)
453
+ continue;
454
+ const yA = axis === 'right' ? yRight : yLeft;
455
+ // 100% mode normalizes each category to its absolute total.
456
+ const totals = spec.stacked100
457
+ ? spec.categories.map((_, i) => axisBars.reduce((sum, s) => sum + Math.abs(Number.isFinite(s.values[i]) ? s.values[i] : 0), 0) || 1)
458
+ : null;
459
+ const pos = new Array(n).fill(0);
460
+ const neg = new Array(n).fill(0);
461
+ for (const s of axisBars) {
462
+ s.values.forEach((v, i) => {
463
+ if (!Number.isFinite(v))
464
+ return;
465
+ const vp = totals ? (v / totals[i]) * 100 : v;
466
+ let yTop;
467
+ let yBot;
468
+ if (vp >= 0) {
469
+ yTop = yA(pos[i] + vp);
470
+ yBot = yA(pos[i]);
471
+ pos[i] += vp;
472
+ }
473
+ else {
474
+ yTop = yA(neg[i]);
475
+ yBot = yA(neg[i] + vp);
476
+ neg[i] += vp;
477
+ }
478
+ bars.push({
479
+ x: round(x0(i)),
480
+ y: Math.min(yTop, yBot),
481
+ w: round(Math.max(1, inner)),
482
+ h: round(Math.abs(yBot - yTop)),
483
+ color: s.color,
484
+ label: spec.categories[i] ?? String(i),
485
+ series: s.label,
486
+ value: v,
487
+ });
488
+ });
489
+ }
490
+ }
491
+ }
492
+ else {
493
+ const inner = slot - groupPad;
494
+ const barW = inner / barSeries.length;
495
+ barSeries.forEach((s, bi) => {
496
+ const dom = domOf(s);
497
+ const base = yOf(dom, Math.min(Math.max(0, dom.min), dom.max));
498
+ s.values.forEach((v, i) => {
499
+ if (!Number.isFinite(v))
500
+ return;
501
+ const x = padL + slot * i + groupPad / 2 + barW * bi;
502
+ const yV = yOf(dom, v);
503
+ bars.push({
504
+ x: round(x),
505
+ y: Math.min(yV, base),
506
+ w: round(Math.max(1, barW - 1)),
507
+ h: round(Math.max(1, Math.abs(yV - base))),
508
+ color: s.color,
509
+ label: spec.categories[i] ?? String(i),
510
+ series: s.label,
511
+ value: v,
512
+ });
513
+ });
514
+ });
515
+ }
516
+ }
517
+ // Lines / areas. Stacked areas accumulate per axis; others fill to baseline.
518
+ const lines = [];
519
+ const areaCum = {
520
+ left: new Array(n).fill(0),
521
+ right: new Array(n).fill(0),
522
+ };
523
+ // 100% mode: per-axis per-category totals to normalize stacked areas to 100.
524
+ const areaTotals = { left: null, right: null };
525
+ if (spec.stacked100) {
526
+ for (const axis of ['left', 'right']) {
527
+ const areaSeries = series.filter((s) => s.kind === 'area' && s.axis === axis);
528
+ if (areaSeries.length) {
529
+ areaTotals[axis] = spec.categories.map((_, i) => areaSeries.reduce((sum, s) => sum + Math.abs(Number.isFinite(s.values[i]) ? s.values[i] : 0), 0) || 1);
530
+ }
531
+ }
532
+ }
533
+ for (const s of series) {
534
+ if (s.kind === 'bar')
535
+ continue;
536
+ const dom = domOf(s);
537
+ const yA = (v) => yOf(dom, v);
538
+ const isStackedArea = stacked && s.kind === 'area';
539
+ const px = (i) => xCenter(i);
540
+ let pts;
541
+ let baselinePts = null;
542
+ if (isStackedArea) {
543
+ // Stacked areas treat a gap as 0 so the stack stays continuous.
544
+ const cum = areaCum[s.axis];
545
+ const prev = cum.slice();
546
+ const totals = areaTotals[s.axis];
547
+ pts = s.values.map((v, i) => {
548
+ const vv = Number.isFinite(v) ? v : 0;
549
+ // 100% mode positions by share of the category total; value stays original.
550
+ const norm = totals ? (vv / totals[i]) * 100 : vv;
551
+ const c = (cum[i] ?? 0) + norm;
552
+ cum[i] = c;
553
+ return { x: px(i), y: yA(c), label: spec.categories[i] ?? String(i), value: v, defined: Number.isFinite(v) };
554
+ });
555
+ baselinePts = prev.map((c, i) => ({ x: px(i), y: yA(c) }));
556
+ }
557
+ else {
558
+ pts = s.values.map((v, i) => {
559
+ const ok = Number.isFinite(v);
560
+ return { x: px(i), y: ok ? yA(v) : NaN, label: spec.categories[i] ?? String(i), value: v, defined: ok };
561
+ });
562
+ }
563
+ // Build the line in segments, breaking at gaps (undefined points).
564
+ let path = '';
565
+ let pen = false;
566
+ for (const p of pts) {
567
+ if (!p.defined) {
568
+ pen = false;
569
+ continue;
570
+ }
571
+ path += `${pen ? 'L' : 'M'}${p.x},${p.y} `;
572
+ pen = true;
573
+ }
574
+ path = path.trim();
575
+ let areaPath = '';
576
+ if (s.kind === 'area' && pts.length) {
577
+ if (baselinePts) {
578
+ const top = pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ');
579
+ const back = baselinePts.slice().reverse().map((p) => `L${p.x},${p.y}`).join(' ');
580
+ areaPath = `${top} ${back} Z`;
581
+ }
582
+ else {
583
+ // One filled polygon per contiguous run of defined points.
584
+ const baseY = round(yA(Math.min(Math.max(0, dom.min), dom.max)));
585
+ const runs = [];
586
+ let cur = [];
587
+ for (const p of pts) {
588
+ if (p.defined)
589
+ cur.push(p);
590
+ else if (cur.length) {
591
+ runs.push(cur);
592
+ cur = [];
593
+ }
594
+ }
595
+ if (cur.length)
596
+ runs.push(cur);
597
+ areaPath = runs
598
+ .map((run) => {
599
+ const top = run.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ');
600
+ return `${top} L${run[run.length - 1].x},${baseY} L${run[0].x},${baseY} Z`;
601
+ })
602
+ .join(' ');
603
+ }
604
+ }
605
+ lines.push({ path, areaPath, color: s.color, label: s.label, points: pts });
606
+ }
607
+ const tickFor = (dom) => dom.ticks.map((value) => ({ value, y: yOf(dom, value), label: fmtTick(value) }));
608
+ const referenceLines = (spec.referenceLines ?? []).map((ref) => {
609
+ const dom = ref.axis === 'right' ? (rightDom ?? leftDom) : leftDom;
610
+ return {
611
+ y: yOf(dom, ref.value),
612
+ label: ref.label ?? fmtTick(ref.value),
613
+ color: ref.color ?? '#ef4444',
614
+ dashed: ref.dashed !== false,
615
+ };
616
+ });
617
+ return {
618
+ ...empty,
619
+ plot,
620
+ bars,
621
+ lines,
622
+ yTicks: tickFor(leftDom),
623
+ y2Ticks: rightDom ? tickFor(rightDom) : [],
624
+ hasRightAxis,
625
+ xTicks,
626
+ xLabelRotated: timeOk ? false : xLabelRotated,
627
+ referenceLines,
628
+ };
629
+ }
630
+ /**
631
+ * Aggregate flat rows into a chart spec. Group by a category field, reduce a
632
+ * value field per group. Three multi-series shapes:
633
+ * - `value: 'revenue'` -> one series
634
+ * - `value: ['revenue','cost']` -> one series per value field
635
+ * - `value: 'sales', series: 'region'` -> pivot: one series per distinct
636
+ * value of the `series` field
637
+ */
638
+ export function rowsToChartSpec(rows, opts) {
639
+ const reduce = opts.reduce ?? 'sum';
640
+ const valueFields = Array.isArray(opts.value) ? opts.value : [opts.value];
641
+ const reduceCell = (sum, count) => reduce === 'count' ? count : reduce === 'avg' ? (count ? sum / count : 0) : sum;
642
+ const categories = [];
643
+ const catIndex = new Map();
644
+ const ensureCat = (key) => {
645
+ let idx = catIndex.get(key);
646
+ if (idx === undefined) {
647
+ idx = categories.length;
648
+ catIndex.set(key, idx);
649
+ categories.push(key);
650
+ }
651
+ return idx;
652
+ };
653
+ // Series keyed by name -> per-category {sum,count}.
654
+ const seriesMap = new Map();
655
+ const ensureSeries = (name) => {
656
+ let arr = seriesMap.get(name);
657
+ if (!arr) {
658
+ arr = [];
659
+ seriesMap.set(name, arr);
660
+ }
661
+ return arr;
662
+ };
663
+ for (const row of rows) {
664
+ const cat = String(row[opts.category] ?? '');
665
+ const ci = ensureCat(cat);
666
+ if (opts.series) {
667
+ const sName = String(row[opts.series] ?? '');
668
+ const arr = ensureSeries(sName);
669
+ const num = Number(row[valueFields[0]]);
670
+ const cell = (arr[ci] ?? (arr[ci] = { sum: 0, count: 0 }));
671
+ if (Number.isFinite(num)) {
672
+ cell.sum += num;
673
+ cell.count += 1;
674
+ }
675
+ }
676
+ else {
677
+ for (const vf of valueFields) {
678
+ const arr = ensureSeries(vf);
679
+ const num = Number(row[vf]);
680
+ const cell = (arr[ci] ?? (arr[ci] = { sum: 0, count: 0 }));
681
+ if (Number.isFinite(num)) {
682
+ cell.sum += num;
683
+ cell.count += 1;
684
+ }
685
+ }
686
+ }
687
+ }
688
+ const entries = [...seriesMap.entries()].map(([name, arr]) => ({
689
+ label: opts.series ? name : opts.seriesLabel && valueFields.length === 1 ? opts.seriesLabel : name,
690
+ values: categories.map((_, i) => {
691
+ const cell = arr[i] ?? { sum: 0, count: 0 };
692
+ return reduceCell(cell.sum, cell.count);
693
+ }),
694
+ }));
695
+ // ---- Sort + top-N -----------------------------------------------------
696
+ const totals = categories.map((_, i) => entries.reduce((sum, e) => sum + (Number.isFinite(e.values[i]) ? e.values[i] : 0), 0));
697
+ const sort = opts.sort ?? (opts.topN ? 'value-desc' : 'none');
698
+ let order = categories.map((_, i) => i);
699
+ if (sort === 'value-desc')
700
+ order.sort((a, b) => totals[b] - totals[a]);
701
+ else if (sort === 'value-asc')
702
+ order.sort((a, b) => totals[a] - totals[b]);
703
+ else if (sort === 'category')
704
+ order.sort((a, b) => categories[a].localeCompare(categories[b]));
705
+ let finalCategories;
706
+ let finalSeries;
707
+ if (opts.topN && order.length > opts.topN) {
708
+ const keep = order.slice(0, opts.topN);
709
+ const rest = order.slice(opts.topN);
710
+ finalCategories = keep.map((i) => categories[i]).concat(opts.otherLabel ?? 'Other');
711
+ finalSeries = entries.map((e) => ({
712
+ label: e.label,
713
+ values: keep
714
+ .map((i) => e.values[i])
715
+ .concat(rest.reduce((sum, i) => sum + (Number.isFinite(e.values[i]) ? e.values[i] : 0), 0)),
716
+ }));
717
+ }
718
+ else {
719
+ finalCategories = order.map((i) => categories[i]);
720
+ finalSeries = entries.map((e) => ({ label: e.label, values: order.map((i) => e.values[i]) }));
721
+ }
722
+ return {
723
+ type: opts.type,
724
+ categories: finalCategories,
725
+ series: finalSeries,
726
+ width: opts.width,
727
+ height: opts.height,
728
+ stacked: opts.stacked,
729
+ stacked100: opts.stacked100,
730
+ palette: opts.palette,
731
+ };
732
+ }