@svgrid/grid 1.1.2 → 1.2.3

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 (143) hide show
  1. package/dist/SvGrid.css +294 -16
  2. package/dist/SvGrid.svelte +4 -1
  3. package/dist/SvGridDropdown.svelte +7 -3
  4. package/dist/cdn/svgrid.js +13506 -0
  5. package/dist/cdn/svgrid.svelte-external.js +11190 -0
  6. package/package.json +12 -2
  7. package/src/SvGrid.css +294 -16
  8. package/src/SvGrid.svelte +4 -1
  9. package/src/SvGridDropdown.svelte +7 -3
  10. package/dist/SvGrid.helpers.test.d.ts +0 -1
  11. package/dist/SvGrid.helpers.test.js +0 -298
  12. package/dist/SvGridChart.test.d.ts +0 -1
  13. package/dist/SvGridChart.test.js +0 -62
  14. package/dist/a11y.contract.test.d.ts +0 -1
  15. package/dist/a11y.contract.test.js +0 -39
  16. package/dist/a11y.test.d.ts +0 -1
  17. package/dist/a11y.test.js +0 -44
  18. package/dist/aligned-grids.test.d.ts +0 -1
  19. package/dist/aligned-grids.test.js +0 -75
  20. package/dist/build-api.coverage.test.d.ts +0 -20
  21. package/dist/build-api.coverage.test.js +0 -505
  22. package/dist/cell-formatting.test.d.ts +0 -1
  23. package/dist/cell-formatting.test.js +0 -234
  24. package/dist/cell-render.test.d.ts +0 -1
  25. package/dist/cell-render.test.js +0 -338
  26. package/dist/chart-export.test.d.ts +0 -1
  27. package/dist/chart-export.test.js +0 -302
  28. package/dist/chart.coverage.test.d.ts +0 -1
  29. package/dist/chart.coverage.test.js +0 -748
  30. package/dist/chart.test.d.ts +0 -1
  31. package/dist/chart.test.js +0 -289
  32. package/dist/clipboard.test.d.ts +0 -1
  33. package/dist/clipboard.test.js +0 -700
  34. package/dist/collaboration.coverage.test.d.ts +0 -1
  35. package/dist/collaboration.coverage.test.js +0 -200
  36. package/dist/collaboration.test.d.ts +0 -1
  37. package/dist/collaboration.test.js +0 -65
  38. package/dist/column-groups.test.d.ts +0 -1
  39. package/dist/column-groups.test.js +0 -56
  40. package/dist/column-types.test.d.ts +0 -1
  41. package/dist/column-types.test.js +0 -62
  42. package/dist/columns.test.d.ts +0 -1
  43. package/dist/columns.test.js +0 -625
  44. package/dist/conditional-formatting.test.d.ts +0 -1
  45. package/dist/conditional-formatting.test.js +0 -87
  46. package/dist/core.coverage.test.d.ts +0 -1
  47. package/dist/core.coverage.test.js +0 -186
  48. package/dist/core.performance.test.d.ts +0 -1
  49. package/dist/core.performance.test.js +0 -29
  50. package/dist/createGrid.test.d.ts +0 -1
  51. package/dist/createGrid.test.js +0 -9
  52. package/dist/editing.test.d.ts +0 -1
  53. package/dist/editing.test.js +0 -732
  54. package/dist/editors/cell-editors.coverage.test.d.ts +0 -1
  55. package/dist/editors/cell-editors.coverage.test.js +0 -139
  56. package/dist/editors/cell-editors.test.d.ts +0 -1
  57. package/dist/editors/cell-editors.test.js +0 -75
  58. package/dist/facet-buckets.test.d.ts +0 -1
  59. package/dist/facet-buckets.test.js +0 -296
  60. package/dist/fill-patterns.test.d.ts +0 -1
  61. package/dist/fill-patterns.test.js +0 -81
  62. package/dist/filter-operators.test.d.ts +0 -1
  63. package/dist/filter-operators.test.js +0 -135
  64. package/dist/filtering/excel-filters.test.d.ts +0 -1
  65. package/dist/filtering/excel-filters.test.js +0 -116
  66. package/dist/filtering/locale-filter.test.d.ts +0 -13
  67. package/dist/filtering/locale-filter.test.js +0 -189
  68. package/dist/flex-render.test.d.ts +0 -1
  69. package/dist/flex-render.test.js +0 -142
  70. package/dist/group-aggregate.test.d.ts +0 -1
  71. package/dist/group-aggregate.test.js +0 -32
  72. package/dist/hyperformula-adapter.test.d.ts +0 -1
  73. package/dist/hyperformula-adapter.test.js +0 -205
  74. package/dist/keyboard-handlers.coverage.test.d.ts +0 -1
  75. package/dist/keyboard-handlers.coverage.test.js +0 -495
  76. package/dist/keyboard.test.d.ts +0 -1
  77. package/dist/keyboard.test.js +0 -27
  78. package/dist/menus.test.d.ts +0 -1
  79. package/dist/menus.test.js +0 -560
  80. package/dist/named-views.coverage.test.d.ts +0 -1
  81. package/dist/named-views.coverage.test.js +0 -180
  82. package/dist/named-views.test.d.ts +0 -1
  83. package/dist/named-views.test.js +0 -57
  84. package/dist/new-features.test.d.ts +0 -1
  85. package/dist/new-features.test.js +0 -183
  86. package/dist/render-component.test.d.ts +0 -1
  87. package/dist/render-component.test.js +0 -41
  88. package/dist/row-drag.test.d.ts +0 -1
  89. package/dist/row-drag.test.js +0 -142
  90. package/dist/row-resize.test.d.ts +0 -1
  91. package/dist/row-resize.test.js +0 -329
  92. package/dist/scroll-sync.test.d.ts +0 -1
  93. package/dist/scroll-sync.test.js +0 -290
  94. package/dist/selection.multi-range.test.d.ts +0 -1
  95. package/dist/selection.multi-range.test.js +0 -55
  96. package/dist/selection.test.d.ts +0 -1
  97. package/dist/selection.test.js +0 -647
  98. package/dist/server-data-source.coverage.test.d.ts +0 -1
  99. package/dist/server-data-source.coverage.test.js +0 -154
  100. package/dist/server-data-source.test.d.ts +0 -1
  101. package/dist/server-data-source.test.js +0 -81
  102. package/dist/sparkline.test.d.ts +0 -1
  103. package/dist/sparkline.test.js +0 -52
  104. package/dist/spreadsheet.test.d.ts +0 -1
  105. package/dist/spreadsheet.test.js +0 -446
  106. package/dist/subscribe.test.d.ts +0 -1
  107. package/dist/subscribe.test.js +0 -93
  108. package/dist/svgrid.api-extensions.test.d.ts +0 -14
  109. package/dist/svgrid.api-extensions.test.js +0 -334
  110. package/dist/svgrid.api.test.d.ts +0 -15
  111. package/dist/svgrid.api.test.js +0 -169
  112. package/dist/svgrid.behavior.test.d.ts +0 -15
  113. package/dist/svgrid.behavior.test.js +0 -625
  114. package/dist/svgrid.column-reorder.test.d.ts +0 -15
  115. package/dist/svgrid.column-reorder.test.js +0 -224
  116. package/dist/svgrid.comments-autocomplete.test.d.ts +0 -1
  117. package/dist/svgrid.comments-autocomplete.test.js +0 -96
  118. package/dist/svgrid.context-menu.test.d.ts +0 -1
  119. package/dist/svgrid.context-menu.test.js +0 -102
  120. package/dist/svgrid.features.test.d.ts +0 -1
  121. package/dist/svgrid.features.test.js +0 -129
  122. package/dist/svgrid.interaction.test.d.ts +0 -9
  123. package/dist/svgrid.interaction.test.js +0 -349
  124. package/dist/svgrid.locale-filtering.test.d.ts +0 -15
  125. package/dist/svgrid.locale-filtering.test.js +0 -261
  126. package/dist/svgrid.new-features.wrapper.test.d.ts +0 -1
  127. package/dist/svgrid.new-features.wrapper.test.js +0 -194
  128. package/dist/svgrid.row-pinning.test.d.ts +0 -14
  129. package/dist/svgrid.row-pinning.test.js +0 -250
  130. package/dist/svgrid.set-filter.test.d.ts +0 -17
  131. package/dist/svgrid.set-filter.test.js +0 -268
  132. package/dist/svgrid.wrapper.test.d.ts +0 -1
  133. package/dist/svgrid.wrapper.test.js +0 -59
  134. package/dist/test-setup.d.ts +0 -1
  135. package/dist/test-setup.js +0 -35
  136. package/dist/transaction.test.d.ts +0 -1
  137. package/dist/transaction.test.js +0 -89
  138. package/dist/virtualization/column-virtualizer.test.d.ts +0 -1
  139. package/dist/virtualization/column-virtualizer.test.js +0 -25
  140. package/dist/virtualization/scroll-scaling.test.d.ts +0 -1
  141. package/dist/virtualization/scroll-scaling.test.js +0 -127
  142. package/dist/virtualization/virtualizer.test.d.ts +0 -1
  143. package/dist/virtualization/virtualizer.test.js +0 -41
@@ -1,748 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { buildChart, rowsToChartSpec, niceLogScale, niceScale, sampleGradient, pickContrastText, linearTrend, simpleMovingAverage, exponentialMovingAverage, computeOverlay, buildLinePath, } from "./chart";
3
- // ---------------------------------------------------------------------------
4
- // Pure scale + color helpers
5
- // ---------------------------------------------------------------------------
6
- describe("niceLogScale", () => {
7
- it("snaps to decade boundaries spanning the data", () => {
8
- const s = niceLogScale(3, 4000);
9
- expect(s.min).toBe(1);
10
- expect(s.max).toBe(10000);
11
- expect(s.ticks).toEqual([1, 10, 100, 1000, 10000]);
12
- });
13
- it("repairs a non-positive / non-finite min", () => {
14
- const s = niceLogScale(0, 100);
15
- expect(s.min).toBe(1);
16
- expect(s.ticks[0]).toBe(1);
17
- });
18
- it("repairs a max that is <= min", () => {
19
- const s = niceLogScale(10, 5);
20
- // max forced to min*10 -> decade [10, 100]
21
- expect(s.max).toBeGreaterThanOrEqual(100);
22
- });
23
- });
24
- describe("niceScale edge handling", () => {
25
- it("non-finite inputs collapse to a [0,1] domain", () => {
26
- const s = niceScale(NaN, Infinity);
27
- expect(s.min).toBe(0);
28
- expect(s.max).toBeGreaterThanOrEqual(1);
29
- });
30
- it("min === max === 0 widens to [0,1]", () => {
31
- const s = niceScale(0, 0);
32
- expect(s.min).toBe(0);
33
- expect(s.max).toBe(1);
34
- });
35
- it("min === max (non-zero positive) includes 0 as the floor", () => {
36
- const s = niceScale(5, 5);
37
- expect(s.min).toBeLessThanOrEqual(0);
38
- expect(s.max).toBeGreaterThanOrEqual(5);
39
- });
40
- it("min === max (non-zero negative) includes 0 as the ceiling", () => {
41
- const s = niceScale(-5, -5);
42
- expect(s.min).toBeLessThanOrEqual(-5);
43
- expect(s.max).toBeGreaterThanOrEqual(0);
44
- });
45
- });
46
- describe("sampleGradient", () => {
47
- it("returns a placeholder for an empty stop list", () => {
48
- expect(sampleGradient([], 0.5)).toBe("#888");
49
- });
50
- it("a single stop ignores t", () => {
51
- expect(sampleGradient(["#ff0000"], 0.3)).toBe("#ff0000");
52
- });
53
- it("clamps t below 0 to the first stop and above 1 to the last", () => {
54
- expect(sampleGradient(["#000000", "#ffffff"], -2)).toBe("#000000");
55
- expect(sampleGradient(["#000000", "#ffffff"], 5)).toBe("#ffffff");
56
- });
57
- it("interpolates linearly in RGB at the midpoint", () => {
58
- // halfway between black and white -> grey ~#7f7f7f / #808080
59
- const mid = sampleGradient(["#000000", "#ffffff"], 0.5);
60
- expect(mid).toMatch(/^#(7f7f7f|808080)$/);
61
- });
62
- it("falls back gracefully when a stop is not parseable hex", () => {
63
- // 'red' is not #rrggbb -> hexToRgb null -> returns the i-th stop
64
- const out = sampleGradient(["red", "#ffffff"], 0.25);
65
- expect(out).toBe("red");
66
- });
67
- });
68
- describe("pickContrastText", () => {
69
- it("returns dark text on a light background", () => {
70
- expect(pickContrastText("#ffffff")).toBe("#0f172a");
71
- });
72
- it("returns light text on a dark background", () => {
73
- expect(pickContrastText("#000000")).toBe("#ffffff");
74
- });
75
- it("falls back to dark text for an unparseable color", () => {
76
- expect(pickContrastText("not-a-color")).toBe("#0f172a");
77
- });
78
- });
79
- // ---------------------------------------------------------------------------
80
- // Overlay math (trend / moving averages)
81
- // ---------------------------------------------------------------------------
82
- describe("linearTrend", () => {
83
- it("fits a straight line through perfectly linear data", () => {
84
- const out = linearTrend([0, 2, 4, 6]);
85
- expect(out).toEqual([0, 2, 4, 6]);
86
- });
87
- it("returns NaNs when fewer than 2 finite points exist", () => {
88
- expect(linearTrend([5]).every(Number.isNaN)).toBe(true);
89
- expect(linearTrend([NaN, NaN]).every(Number.isNaN)).toBe(true);
90
- });
91
- it("returns the mean when the x-variance is degenerate", () => {
92
- // Two identical-index points can't happen here, but a flat series with a
93
- // single repeated value still fits a horizontal line at the mean.
94
- const out = linearTrend([3, 3, 3]);
95
- for (const v of out)
96
- expect(v).toBeCloseTo(3);
97
- });
98
- it("ignores non-finite points when fitting", () => {
99
- const out = linearTrend([0, NaN, 4]);
100
- expect(out[0]).toBeCloseTo(0);
101
- expect(out[2]).toBeCloseTo(4);
102
- });
103
- });
104
- describe("simpleMovingAverage", () => {
105
- it("period < 1 returns a copy of the input", () => {
106
- const input = [1, 2, 3];
107
- const out = simpleMovingAverage(input, 0);
108
- expect(out).toEqual(input);
109
- expect(out).not.toBe(input);
110
- });
111
- it("trails NaN until the window is full, then averages", () => {
112
- const out = simpleMovingAverage([2, 4, 6, 8], 2);
113
- expect(Number.isNaN(out[0])).toBe(true);
114
- expect(out[1]).toBeCloseTo(3);
115
- expect(out[2]).toBeCloseTo(5);
116
- expect(out[3]).toBeCloseTo(7);
117
- });
118
- it("skips non-finite values inside the window", () => {
119
- const out = simpleMovingAverage([2, NaN, 6, 8], 2);
120
- expect(out.length).toBe(4);
121
- });
122
- });
123
- describe("exponentialMovingAverage", () => {
124
- it("seeds on the first finite value then smooths", () => {
125
- const out = exponentialMovingAverage([10, 20], 1);
126
- expect(out[0]).toBeCloseTo(10);
127
- expect(out[1]).toBeGreaterThan(10);
128
- });
129
- it("carries the previous value across a non-finite point", () => {
130
- const out = exponentialMovingAverage([10, NaN, 20], 3);
131
- expect(out[1]).toBeCloseTo(out[0]);
132
- });
133
- it("yields NaN before the first finite value", () => {
134
- const out = exponentialMovingAverage([NaN, 5], 2);
135
- expect(Number.isNaN(out[0])).toBe(true);
136
- expect(out[1]).toBeCloseTo(5);
137
- });
138
- });
139
- describe("computeOverlay", () => {
140
- it("dispatches 'linear' to the regression fit", () => {
141
- expect(computeOverlay([0, 2, 4], "linear")).toEqual([0, 2, 4]);
142
- });
143
- it("dispatches sma:N to the simple moving average", () => {
144
- const out = computeOverlay([2, 4, 6], "sma:2");
145
- expect(Number.isNaN(out[0])).toBe(true);
146
- expect(out[1]).toBeCloseTo(3);
147
- });
148
- it("dispatches ema:N to the exponential moving average", () => {
149
- const out = computeOverlay([10, 20, 30], "ema:2");
150
- expect(out[0]).toBeCloseTo(10);
151
- });
152
- it("returns all-NaN for an unrecognised overlay spec", () => {
153
- const out = computeOverlay([1, 2, 3], "bogus:7");
154
- expect(out.every(Number.isNaN)).toBe(true);
155
- });
156
- });
157
- describe("buildLinePath", () => {
158
- it("breaks the polyline at undefined gaps (multiple M commands)", () => {
159
- const path = buildLinePath([
160
- { x: 0, y: 0, defined: true },
161
- { x: 1, y: 1, defined: false },
162
- { x: 2, y: 2, defined: true },
163
- ], false);
164
- expect((path.match(/M/g) ?? []).length).toBe(2);
165
- });
166
- it("smooth mode emits cubic bezier (C) segments", () => {
167
- const path = buildLinePath([
168
- { x: 0, y: 0, defined: true },
169
- { x: 1, y: 2, defined: true },
170
- { x: 2, y: 1, defined: true },
171
- ], true);
172
- expect(path).toContain("C");
173
- });
174
- it("smooth two-point run is a straight line", () => {
175
- const path = buildLinePath([
176
- { x: 0, y: 0, defined: true },
177
- { x: 1, y: 1, defined: true },
178
- ], true);
179
- expect(path).toContain("L");
180
- });
181
- });
182
- // ---------------------------------------------------------------------------
183
- // buildChart: log scales, time vs category, overlays, annotations, patterns
184
- // ---------------------------------------------------------------------------
185
- describe("buildChart: log y-scale", () => {
186
- it("plots bars from the axis floor up; ticks are decade boundaries", () => {
187
- const g = buildChart({
188
- type: "bar",
189
- yScale: "log",
190
- categories: ["A", "B", "C"],
191
- series: [{ label: "s", values: [10, 1000, 100000] }],
192
- });
193
- // decade ticks
194
- expect(g.yTicks.some((t) => t.value === 10)).toBe(true);
195
- expect(g.yTicks.some((t) => t.value === 100000)).toBe(true);
196
- expect(g.bars).toHaveLength(3);
197
- });
198
- it("drops non-positive bar values on a log axis", () => {
199
- const g = buildChart({
200
- type: "bar",
201
- yScale: "log",
202
- categories: ["A", "B"],
203
- series: [{ label: "s", values: [0, 100] }],
204
- });
205
- // the zero-value bar is omitted (invalid in log space)
206
- expect(g.bars).toHaveLength(1);
207
- expect(g.bars[0].value).toBe(100);
208
- });
209
- it("a dual-axis chart can log-scale the right axis independently", () => {
210
- const g = buildChart({
211
- type: "bar",
212
- y2Scale: "log",
213
- categories: ["A", "B"],
214
- series: [
215
- { label: "rev", values: [10, 20], axis: "left" },
216
- { label: "hits", values: [100, 100000], axis: "right", type: "line" },
217
- ],
218
- });
219
- expect(g.hasRightAxis).toBe(true);
220
- expect(g.y2Ticks.length).toBeGreaterThan(1);
221
- });
222
- });
223
- describe("buildChart: time axis edge cases", () => {
224
- it("falls back to category spacing when no date parses", () => {
225
- const g = buildChart({
226
- type: "line",
227
- xType: "time",
228
- categories: ["not-a-date", "also-bad"],
229
- series: [{ label: "s", values: [1, 2] }],
230
- });
231
- // unparseable -> uniform category positions, labels are the raw strings
232
- expect(g.xTicks.map((t) => t.label)).toEqual(["not-a-date", "also-bad"]);
233
- });
234
- });
235
- describe("buildChart: overlays", () => {
236
- it("adds a dashed trend overlay line parallel to the series", () => {
237
- const g = buildChart({
238
- type: "line",
239
- categories: ["A", "B", "C", "D"],
240
- series: [{ label: "s", values: [1, 3, 2, 5], overlay: "linear" }],
241
- });
242
- expect(g.overlays).toHaveLength(1);
243
- expect(g.overlays[0].label).toContain("linear");
244
- expect(g.overlays[0].path.startsWith("M")).toBe(true);
245
- });
246
- it("honours overlayColor when provided", () => {
247
- const g = buildChart({
248
- type: "line",
249
- categories: ["A", "B"],
250
- series: [
251
- {
252
- label: "s",
253
- values: [1, 2],
254
- overlay: "sma:2",
255
- overlayColor: "#123456",
256
- },
257
- ],
258
- });
259
- expect(g.overlays[0].color).toBe("#123456");
260
- });
261
- });
262
- describe("buildChart: annotations", () => {
263
- it("anchors a category annotation to a named series value", () => {
264
- const g = buildChart({
265
- type: "line",
266
- categories: ["A", "B", "C"],
267
- series: [{ label: "rev", values: [10, 20, 30] }],
268
- annotations: [{ at: { category: "B", series: "rev" }, label: "Spike" }],
269
- });
270
- expect(g.annotations).toHaveLength(1);
271
- expect(g.annotations[0].label).toBe("Spike");
272
- expect(Number.isFinite(g.annotations[0].x)).toBe(true);
273
- expect(g.annotations[0].placement).toBe("top");
274
- });
275
- it("skips annotations whose category is not present", () => {
276
- const g = buildChart({
277
- type: "line",
278
- categories: ["A", "B"],
279
- series: [{ label: "s", values: [1, 2] }],
280
- annotations: [{ at: { category: "Z" }, label: "Nope" }],
281
- });
282
- expect(g.annotations).toHaveLength(0);
283
- });
284
- it("resolves a raw x/y data-space annotation against the left axis", () => {
285
- const g = buildChart({
286
- type: "line",
287
- categories: ["A", "B"],
288
- series: [{ label: "s", values: [10, 20] }],
289
- annotations: [
290
- { at: { x: 0, y: 15 }, label: "Mid", color: "#abcdef", placement: "bottom" },
291
- ],
292
- });
293
- expect(g.annotations).toHaveLength(1);
294
- expect(g.annotations[0].color).toBe("#abcdef");
295
- expect(g.annotations[0].placement).toBe("bottom");
296
- });
297
- });
298
- describe("buildChart: confidence band", () => {
299
- it("builds a band path when upper+lower envelopes are supplied", () => {
300
- const g = buildChart({
301
- type: "line",
302
- categories: ["A", "B", "C"],
303
- series: [
304
- {
305
- label: "s",
306
- values: [10, 20, 30],
307
- upperValues: [12, 23, 34],
308
- lowerValues: [8, 17, 26],
309
- },
310
- ],
311
- });
312
- expect(g.lines[0].bandPath).toBeTruthy();
313
- expect(g.lines[0].bandPath.endsWith("Z")).toBe(true);
314
- });
315
- it("omits the band when the envelope lengths don't align", () => {
316
- const g = buildChart({
317
- type: "line",
318
- categories: ["A", "B"],
319
- series: [
320
- { label: "s", values: [1, 2], upperValues: [3], lowerValues: [0] },
321
- ],
322
- });
323
- expect(g.lines[0].bandPath).toBe("");
324
- });
325
- });
326
- describe("buildChart: smooth line", () => {
327
- it("smooth produces a curved (C) path", () => {
328
- const g = buildChart({
329
- type: "line",
330
- categories: ["A", "B", "C"],
331
- series: [{ label: "s", values: [1, 3, 2], smooth: true }],
332
- });
333
- expect(g.lines[0].path).toContain("C");
334
- });
335
- it("smooth area builds a closed filled path", () => {
336
- const g = buildChart({
337
- type: "area",
338
- categories: ["A", "B", "C"],
339
- series: [{ label: "s", values: [1, 3, 2], smooth: "monotone" }],
340
- });
341
- expect(g.lines[0].areaPath.endsWith("Z")).toBe(true);
342
- });
343
- });
344
- // ---------------------------------------------------------------------------
345
- // buildChart: specialty chart types
346
- // ---------------------------------------------------------------------------
347
- describe("buildChart: waterfall", () => {
348
- it("running totals + connectors + total bars", () => {
349
- const g = buildChart({
350
- type: "waterfall",
351
- categories: ["Start", "Up", "Down", "End"],
352
- series: [{ label: "s", values: [100, 50, -30, 0] }],
353
- waterfallTotals: [false, false, false, true],
354
- });
355
- expect(g.bars).toHaveLength(4);
356
- // total bar (index 3) spans from 0 to the running cumulative (120)
357
- expect(g.bars[3].value).toBeCloseTo(120);
358
- // a connector line exists between bar tops
359
- expect(g.lines[0].path).toContain("M");
360
- });
361
- it("colors positive/negative/total via waterfallColors overrides", () => {
362
- const g = buildChart({
363
- type: "waterfall",
364
- categories: ["A", "B", "T"],
365
- series: [{ label: "s", values: [10, -5, 0] }],
366
- waterfallTotals: [false, false, true],
367
- waterfallColors: { positive: "#0f0", negative: "#f00", total: "#00f" },
368
- });
369
- expect(g.bars[0].color).toBe("#0f0");
370
- expect(g.bars[1].color).toBe("#f00");
371
- expect(g.bars[2].color).toBe("#00f");
372
- });
373
- it("returns the empty geometry when there is no series", () => {
374
- const g = buildChart({ type: "waterfall", categories: ["A"], series: [] });
375
- expect(g.bars).toHaveLength(0);
376
- });
377
- });
378
- describe("buildChart: funnel", () => {
379
- it("trapezoids with conversion + dropoff metrics", () => {
380
- const g = buildChart({
381
- type: "funnel",
382
- categories: ["Visits", "Signups", "Paid"],
383
- series: [{ label: "s", values: [1000, 400, 100] }],
384
- });
385
- expect(g.funnelSegments).toHaveLength(3);
386
- expect(g.funnelSegments[0].conversion).toBeCloseTo(1);
387
- expect(g.funnelSegments[2].conversion).toBeCloseTo(0.1);
388
- // dropoff from 1000 -> 400 is 60%
389
- expect(g.funnelSegments[1].dropoff).toBeCloseTo(0.6);
390
- expect(g.funnelSegments[0].path).toContain("Z");
391
- });
392
- it("returns empty geometry for an empty series", () => {
393
- const g = buildChart({ type: "funnel", categories: [], series: [{ label: "s", values: [] }] });
394
- expect(g.funnelSegments).toHaveLength(0);
395
- });
396
- });
397
- describe("buildChart: radar", () => {
398
- it("one polygon per series + axis spokes + rings", () => {
399
- const g = buildChart({
400
- type: "radar",
401
- categories: ["Speed", "Power", "Range", "Cost"],
402
- series: [
403
- { label: "X", values: [3, 5, 2, 4] },
404
- { label: "Y", values: [5, 1, 4, 3] },
405
- ],
406
- });
407
- expect(g.radarAxes).toHaveLength(4);
408
- expect(g.radarSeries).toHaveLength(2);
409
- expect(g.radarRings).toHaveLength(5);
410
- expect(g.radarSeries[0].path.endsWith("Z")).toBe(true);
411
- expect(g.radarCenter).not.toBeNull();
412
- });
413
- it("returns empty geometry with no series or categories", () => {
414
- expect(buildChart({ type: "radar", categories: [], series: [] }).radarSeries).toHaveLength(0);
415
- expect(buildChart({ type: "radar", categories: ["A"], series: [] }).radarAxes).toHaveLength(0);
416
- });
417
- it("guards against an all-zero max (vMax defaults to 1)", () => {
418
- const g = buildChart({
419
- type: "radar",
420
- categories: ["A", "B"],
421
- series: [{ label: "z", values: [0, 0] }],
422
- });
423
- expect(g.radarSeries[0].path).toBeTruthy();
424
- });
425
- });
426
- describe("buildChart: calendar heatmap", () => {
427
- it("emits one cell per day with month ticks + a legend", () => {
428
- const g = buildChart({
429
- type: "calendar",
430
- categories: [],
431
- series: [],
432
- calendarValues: [
433
- { date: "2026-01-05", value: 3 },
434
- { date: "2026-01-06", value: 9 },
435
- { date: "2026-02-02", value: 5 },
436
- ],
437
- });
438
- expect(g.calendarCells.length).toBeGreaterThan(7);
439
- expect(g.calendarLegend).toHaveLength(5);
440
- // a defined day carries its value + a non-transparent color
441
- const defined = g.calendarCells.find((c) => c.date === "2026-01-05");
442
- expect(defined.defined).toBe(true);
443
- expect(defined.value).toBe(3);
444
- expect(defined.color).not.toBe("transparent");
445
- // an undefined day in the range is blank
446
- const blank = g.calendarCells.find((c) => !c.defined);
447
- expect(blank.color).toBe("transparent");
448
- expect(g.calendarMonthTicks.length).toBeGreaterThanOrEqual(1);
449
- });
450
- it("honours explicit calendarStart even with no values", () => {
451
- const g = buildChart({
452
- type: "calendar",
453
- categories: [],
454
- series: [],
455
- calendarStart: "2026-03-01",
456
- calendarEnd: "2026-03-14",
457
- });
458
- expect(g.calendarCells.length).toBeGreaterThan(0);
459
- });
460
- it("returns empty geometry with neither values nor a start", () => {
461
- const g = buildChart({ type: "calendar", categories: [], series: [] });
462
- expect(g.calendarCells).toHaveLength(0);
463
- });
464
- });
465
- describe("buildChart: gauge", () => {
466
- it("track + value arcs, ticks, needle, clamped value", () => {
467
- const g = buildChart({
468
- type: "gauge",
469
- categories: [],
470
- series: [],
471
- gaugeValue: 150, // clamped to max
472
- gaugeMin: 0,
473
- gaugeMax: 100,
474
- gaugeTarget: 80,
475
- gaugeRanges: [
476
- { from: 0, to: 50, color: "#f00" },
477
- { from: 50, to: 100, color: "#0f0" },
478
- ],
479
- gaugeUnit: "%",
480
- });
481
- expect(g.gauge).not.toBeNull();
482
- expect(g.gauge.value).toBe(100); // clamped
483
- expect(g.gauge.unit).toBe("%");
484
- expect(g.gauge.trackPath).toContain("A");
485
- expect(g.gauge.valuePath).toContain("A");
486
- expect(g.gauge.ticks.length).toBeGreaterThan(0);
487
- expect(g.gauge.target).not.toBeNull();
488
- // value 100 sits in the second band
489
- expect(g.gauge.valueColor).toBe("#0f0");
490
- expect(g.gauge.needle.path).toContain("Z");
491
- });
492
- it("no target marker when gaugeTarget is unset", () => {
493
- const g = buildChart({
494
- type: "gauge",
495
- categories: [],
496
- series: [],
497
- gaugeValue: 40,
498
- });
499
- expect(g.gauge.target).toBeNull();
500
- expect(g.gauge.valueColor).toBeNull();
501
- });
502
- });
503
- describe("buildChart: treemap", () => {
504
- it("lays out leaf + branch cells with depth", () => {
505
- const g = buildChart({
506
- type: "treemap",
507
- categories: [],
508
- series: [],
509
- width: 400,
510
- height: 300,
511
- treemap: {
512
- name: "root",
513
- children: [
514
- {
515
- name: "A",
516
- children: [
517
- { name: "A1", value: 30 },
518
- { name: "A2", value: 20 },
519
- ],
520
- },
521
- { name: "B", value: 50 },
522
- ],
523
- },
524
- });
525
- expect(g.treemapCells.length).toBeGreaterThan(0);
526
- // both leaves + a parent cell present; depths >= 0
527
- const names = g.treemapCells.map((c) => c.name);
528
- expect(names).toContain("B");
529
- expect(g.treemapCells.every((c) => c.depth >= 0)).toBe(true);
530
- });
531
- it("returns empty geometry with no root", () => {
532
- const g = buildChart({ type: "treemap", categories: [], series: [] });
533
- expect(g.treemapCells).toHaveLength(0);
534
- });
535
- });
536
- describe("buildChart: sankey", () => {
537
- it("places nodes in columns and builds curved ribbon links", () => {
538
- const g = buildChart({
539
- type: "sankey",
540
- categories: [],
541
- series: [],
542
- sankeyNodes: [
543
- { id: "a", label: "A" },
544
- { id: "b", label: "B" },
545
- { id: "c", label: "C" },
546
- ],
547
- sankeyLinks: [
548
- { source: "a", target: "b", value: 10 },
549
- { source: "b", target: "c", value: 6 },
550
- { source: "a", target: "c", value: 4 },
551
- ],
552
- });
553
- expect(g.sankeyNodes.length).toBe(3);
554
- expect(g.sankeyLinks.length).toBe(3);
555
- // columns increase along the flow
556
- const colOf = (id) => g.sankeyNodes.find((n) => n.id === id).column;
557
- expect(colOf("a")).toBeLessThan(colOf("c"));
558
- expect(g.sankeyLinks[0].path).toContain("C");
559
- });
560
- it("returns empty geometry when nodes or links are missing", () => {
561
- expect(buildChart({ type: "sankey", categories: [], series: [], sankeyNodes: [], sankeyLinks: [] })
562
- .sankeyNodes).toHaveLength(0);
563
- });
564
- });
565
- describe("buildChart: heatmap", () => {
566
- it("one cell per (row,col), legend + axis ticks, contrast text", () => {
567
- const g = buildChart({
568
- type: "heatmap",
569
- categories: ["Q1", "Q2", "Q3"],
570
- series: [
571
- { label: "North", values: [1, 5, 9] },
572
- { label: "South", values: [2, 6, 8] },
573
- ],
574
- });
575
- expect(g.heatmapCells).toHaveLength(6);
576
- expect(g.heatmapRowTicks.map((t) => t.label)).toEqual(["North", "South"]);
577
- expect(g.heatmapColTicks.map((t) => t.label)).toEqual(["Q1", "Q2", "Q3"]);
578
- expect(g.heatmapLegend).toHaveLength(5);
579
- for (const c of g.heatmapCells) {
580
- expect(["#0f172a", "#ffffff"]).toContain(c.textColor);
581
- }
582
- });
583
- it("diverging scale is auto-picked for signed data", () => {
584
- const g = buildChart({
585
- type: "heatmap",
586
- categories: ["A", "B"],
587
- series: [{ label: "r", values: [-5, 5] }],
588
- });
589
- expect(g.heatmapCells).toHaveLength(2);
590
- });
591
- it("a custom color-scale array is honoured", () => {
592
- const g = buildChart({
593
- type: "heatmap",
594
- categories: ["A", "B"],
595
- series: [{ label: "r", values: [0, 10] }],
596
- colorScale: ["#000000", "#ffffff"],
597
- });
598
- expect(g.heatmapCells).toHaveLength(2);
599
- });
600
- it("returns empty geometry with no series", () => {
601
- const g = buildChart({ type: "heatmap", categories: ["A"], series: [] });
602
- expect(g.heatmapCells).toHaveLength(0);
603
- });
604
- it("dark theme uses the dark ramps without error", () => {
605
- const g = buildChart({
606
- type: "heatmap",
607
- categories: ["A"],
608
- series: [{ label: "r", values: [3] }],
609
- }, "dark");
610
- expect(g.heatmapCells).toHaveLength(1);
611
- });
612
- });
613
- describe("buildChart: stacked horizontal + dual-axis stacks", () => {
614
- it("stacked horizontal bars share a category band", () => {
615
- const g = buildChart({
616
- type: "bar",
617
- orientation: "horizontal",
618
- stacked: true,
619
- categories: ["A"],
620
- series: [
621
- { label: "x", values: [10] },
622
- { label: "y", values: [20] },
623
- ],
624
- });
625
- expect(g.orientation).toBe("horizontal");
626
- expect(g.bars).toHaveLength(2);
627
- // stacked: second segment starts where the first ended
628
- expect(g.bars[1].x).toBeGreaterThanOrEqual(g.bars[0].x);
629
- });
630
- it("stacked100 horizontal renders percent value ticks", () => {
631
- const g = buildChart({
632
- type: "bar",
633
- orientation: "horizontal",
634
- stacked100: true,
635
- categories: ["A"],
636
- series: [
637
- { label: "x", values: [1] },
638
- { label: "y", values: [3] },
639
- ],
640
- });
641
- expect(g.valueTicks.some((t) => t.label.includes("%"))).toBe(true);
642
- });
643
- });
644
- // ---------------------------------------------------------------------------
645
- // rowsToChartSpec: remaining branches
646
- // ---------------------------------------------------------------------------
647
- describe("rowsToChartSpec: sort + idField + topN rowIds", () => {
648
- const rows = [
649
- { id: "r1", region: "EMEA", revenue: 100 },
650
- { id: "r2", region: "APAC", revenue: 200 },
651
- { id: "r3", region: "AMER", revenue: 50 },
652
- { id: "r4", region: "EMEA", revenue: 80 },
653
- ];
654
- it("sort 'value-asc' orders categories by ascending total", () => {
655
- const spec = rowsToChartSpec(rows, {
656
- type: "bar",
657
- category: "region",
658
- value: "revenue",
659
- sort: "value-asc",
660
- });
661
- // totals: EMEA=180, APAC=200, AMER=50 -> asc: AMER, EMEA, APAC
662
- expect(spec.categories).toEqual(["AMER", "EMEA", "APAC"]);
663
- });
664
- it("sort 'category' orders categories alphabetically", () => {
665
- const spec = rowsToChartSpec(rows, {
666
- type: "bar",
667
- category: "region",
668
- value: "revenue",
669
- sort: "category",
670
- });
671
- expect(spec.categories).toEqual(["AMER", "APAC", "EMEA"]);
672
- });
673
- it("idField populates rowIds parallel to values", () => {
674
- const spec = rowsToChartSpec(rows, {
675
- type: "bar",
676
- category: "region",
677
- value: "revenue",
678
- idField: "id",
679
- });
680
- const emea = spec.categories.indexOf("EMEA");
681
- expect(spec.series[0].rowIds[emea]).toEqual(["r1", "r4"]);
682
- });
683
- it("topN with idField buckets remainder rowIds into 'Other'", () => {
684
- const spec = rowsToChartSpec(rows, {
685
- type: "bar",
686
- category: "region",
687
- value: "revenue",
688
- idField: "id",
689
- topN: 1,
690
- otherLabel: "Rest",
691
- });
692
- // top category by total is APAC (200); the rest collapse into "Rest"
693
- expect(spec.categories).toEqual(["APAC", "Rest"]);
694
- const otherIds = spec.series[0].rowIds[1];
695
- // EMEA (r1,r4) + AMER (r3) bucketed together
696
- expect(otherIds.sort()).toEqual(["r1", "r3", "r4"]);
697
- });
698
- it("seriesLabel renames the single value series", () => {
699
- const spec = rowsToChartSpec(rows, {
700
- type: "bar",
701
- category: "region",
702
- value: "revenue",
703
- seriesLabel: "Sales",
704
- });
705
- expect(spec.series[0].label).toBe("Sales");
706
- });
707
- it("pivot via the series field with idField tracks ids per series", () => {
708
- const spec = rowsToChartSpec(rows, {
709
- type: "bar",
710
- category: "region",
711
- value: "revenue",
712
- series: "region",
713
- idField: "id",
714
- });
715
- expect(spec.series.length).toBeGreaterThan(0);
716
- expect(spec.series[0].rowIds).toBeTruthy();
717
- });
718
- it("non-numeric value cells are skipped from the aggregate", () => {
719
- const dirty = [
720
- { region: "A", revenue: 10 },
721
- { region: "A", revenue: "n/a" },
722
- { region: "A", revenue: 20 },
723
- ];
724
- const spec = rowsToChartSpec(dirty, {
725
- type: "bar",
726
- category: "region",
727
- value: "revenue",
728
- reduce: "count",
729
- });
730
- // only 2 finite numbers counted
731
- expect(spec.series[0].values).toEqual([2]);
732
- });
733
- it("passes width/height/stacked/palette through to the spec", () => {
734
- const spec = rowsToChartSpec(rows, {
735
- type: "bar",
736
- category: "region",
737
- value: "revenue",
738
- width: 600,
739
- height: 400,
740
- stacked: true,
741
- palette: ["#111", "#222"],
742
- });
743
- expect(spec.width).toBe(600);
744
- expect(spec.height).toBe(400);
745
- expect(spec.stacked).toBe(true);
746
- expect(spec.palette).toEqual(["#111", "#222"]);
747
- });
748
- });