@pyreon/charts 0.10.0 → 0.11.1

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 (42) hide show
  1. package/lib/analysis/index.js.html +1 -1
  2. package/lib/analysis/manual.js.html +1 -1
  3. package/lib/{charts-Ckh2qxB5.js → charts-lo2KeDld.js} +164 -164
  4. package/lib/charts-lo2KeDld.js.map +1 -0
  5. package/lib/{components-BcPePBeS.js → components-ClWy1Ztp.js} +128 -128
  6. package/lib/components-ClWy1Ztp.js.map +1 -0
  7. package/lib/{core-9w0g6EOI.js → core-BiuQ3y-t.js} +11 -11
  8. package/lib/core-BiuQ3y-t.js.map +1 -0
  9. package/lib/{createSeriesData-DOHScdgk.js → createSeriesData-BGy-6PqI.js} +6 -6
  10. package/lib/createSeriesData-BGy-6PqI.js.map +1 -0
  11. package/lib/{customGraphicKeyframeAnimation-CvkEkSt_.js → customGraphicKeyframeAnimation-BIbJI8ew.js} +61 -61
  12. package/lib/customGraphicKeyframeAnimation-BIbJI8ew.js.map +1 -0
  13. package/lib/{graphic-CPJ2K90a.js → graphic-Bt7SEwll.js} +59 -59
  14. package/lib/graphic-Bt7SEwll.js.map +1 -0
  15. package/lib/index.js +60 -52
  16. package/lib/index.js.map +1 -1
  17. package/lib/manual.js +60 -52
  18. package/lib/manual.js.map +1 -1
  19. package/lib/{parseGeoJson-BlBe5Vig.js → parseGeoJson-NjUY1feF.js} +115 -115
  20. package/lib/parseGeoJson-NjUY1feF.js.map +1 -0
  21. package/lib/{renderers-Dytryvoy.js → renderers-BnAAXHfG.js} +16 -16
  22. package/lib/renderers-BnAAXHfG.js.map +1 -0
  23. package/lib/types/index.d.ts +3 -3
  24. package/lib/types/index.d.ts.map +1 -1
  25. package/lib/types/manual.d.ts +3 -3
  26. package/lib/types/manual.d.ts.map +1 -1
  27. package/package.json +14 -7
  28. package/src/chart-component.tsx +18 -10
  29. package/src/index.ts +3 -3
  30. package/src/loader.ts +70 -73
  31. package/src/manual.ts +4 -4
  32. package/src/tests/charts.test.tsx +431 -126
  33. package/src/types.ts +8 -9
  34. package/src/use-chart.ts +8 -8
  35. package/lib/charts-Ckh2qxB5.js.map +0 -1
  36. package/lib/components-BcPePBeS.js.map +0 -1
  37. package/lib/core-9w0g6EOI.js.map +0 -1
  38. package/lib/createSeriesData-DOHScdgk.js.map +0 -1
  39. package/lib/customGraphicKeyframeAnimation-CvkEkSt_.js.map +0 -1
  40. package/lib/graphic-CPJ2K90a.js.map +0 -1
  41. package/lib/parseGeoJson-BlBe5Vig.js.map +0 -1
  42. package/lib/renderers-Dytryvoy.js.map +0 -1
@@ -1,18 +1,13 @@
1
- import { mount } from '@pyreon/runtime-dom'
2
- import { Chart } from '../chart-component'
3
- import {
4
- _resetLoader,
5
- ensureModules,
6
- getCore,
7
- getCoreSync,
8
- manualUse,
9
- } from '../loader'
1
+ import { signal } from "@pyreon/reactivity"
2
+ import { mount } from "@pyreon/runtime-dom"
3
+ import { Chart } from "../chart-component"
4
+ import { _resetLoader, ensureModules, getCore, getCoreSync, manualUse } from "../loader"
10
5
 
11
6
  // ─── Helpers ──────────────────────────────────────────────────────────────────
12
7
 
13
8
  function mountWith<T>(fn: () => T): { result: T; unmount: () => void } {
14
9
  let result: T | undefined
15
- const el = document.createElement('div')
10
+ const el = document.createElement("div")
16
11
  document.body.appendChild(el)
17
12
  const Child = () => {
18
13
  result = fn()
@@ -34,123 +29,120 @@ afterEach(() => {
34
29
 
35
30
  // ─── Loader ───────────────────────────────────────────────────────────────────
36
31
 
37
- describe('loader', () => {
38
- it('lazily loads echarts/core on first call', async () => {
32
+ describe("loader", () => {
33
+ it("lazily loads echarts/core on first call", async () => {
39
34
  const core = await getCore()
40
35
  expect(core).toBeDefined()
41
- expect(typeof core.init).toBe('function')
42
- expect(typeof core.use).toBe('function')
36
+ expect(typeof core.init).toBe("function")
37
+ expect(typeof core.use).toBe("function")
43
38
  })
44
39
 
45
- it('caches core module on subsequent calls', async () => {
40
+ it("caches core module on subsequent calls", async () => {
46
41
  const core1 = await getCore()
47
42
  const core2 = await getCore()
48
43
  expect(core1).toBe(core2)
49
44
  })
50
45
 
51
- it('auto-detects BarChart from series type', async () => {
46
+ it("auto-detects BarChart from series type", async () => {
52
47
  const core = await ensureModules({
53
- series: [{ type: 'bar', data: [1, 2, 3] }],
48
+ series: [{ type: "bar", data: [1, 2, 3] }],
54
49
  })
55
50
  expect(core).toBeDefined()
56
51
  // If it didn't throw, the modules were registered successfully
57
52
  })
58
53
 
59
- it('auto-detects PieChart from series type', async () => {
54
+ it("auto-detects PieChart from series type", async () => {
60
55
  const core = await ensureModules({
61
- series: [{ type: 'pie', data: [{ value: 1 }] }],
56
+ series: [{ type: "pie", data: [{ value: 1 }] }],
62
57
  })
63
58
  expect(core).toBeDefined()
64
59
  })
65
60
 
66
- it('auto-detects LineChart from series type', async () => {
61
+ it("auto-detects LineChart from series type", async () => {
67
62
  const core = await ensureModules({
68
- series: [{ type: 'line', data: [1, 2, 3] }],
63
+ series: [{ type: "line", data: [1, 2, 3] }],
69
64
  })
70
65
  expect(core).toBeDefined()
71
66
  })
72
67
 
73
- it('auto-detects components from config keys', async () => {
68
+ it("auto-detects components from config keys", async () => {
74
69
  const core = await ensureModules({
75
- tooltip: { trigger: 'axis' },
70
+ tooltip: { trigger: "axis" },
76
71
  legend: {},
77
- xAxis: { type: 'category' },
78
- yAxis: { type: 'value' },
79
- series: [{ type: 'bar', data: [1] }],
72
+ xAxis: { type: "category" },
73
+ yAxis: { type: "value" },
74
+ series: [{ type: "bar", data: [1] }],
80
75
  })
81
76
  expect(core).toBeDefined()
82
77
  })
83
78
 
84
- it('auto-detects series features (markPoint, markLine, markArea)', async () => {
79
+ it("auto-detects series features (markPoint, markLine, markArea)", async () => {
85
80
  const core = await ensureModules({
86
81
  series: [
87
82
  {
88
- type: 'line',
83
+ type: "line",
89
84
  data: [1, 2, 3],
90
- markPoint: { data: [{ type: 'max' }] },
91
- markLine: { data: [{ type: 'average' }] },
92
- markArea: { data: [[{ xAxis: 'A' }, { xAxis: 'B' }]] },
85
+ markPoint: { data: [{ type: "max" }] },
86
+ markLine: { data: [{ type: "average" }] },
87
+ markArea: { data: [[{ xAxis: "A" }, { xAxis: "B" }]] },
93
88
  },
94
89
  ],
95
90
  })
96
91
  expect(core).toBeDefined()
97
92
  })
98
93
 
99
- it('loads multiple chart types in one config', async () => {
94
+ it("loads multiple chart types in one config", async () => {
100
95
  const core = await ensureModules({
101
96
  series: [
102
- { type: 'bar', data: [1, 2] },
103
- { type: 'line', data: [3, 4] },
104
- { type: 'scatter', data: [[1, 2]] },
97
+ { type: "bar", data: [1, 2] },
98
+ { type: "line", data: [3, 4] },
99
+ { type: "scatter", data: [[1, 2]] },
105
100
  ],
106
101
  })
107
102
  expect(core).toBeDefined()
108
103
  })
109
104
 
110
- it('caches modules across calls', async () => {
105
+ it("caches modules across calls", async () => {
111
106
  // First call loads BarChart
112
- await ensureModules({ series: [{ type: 'bar', data: [1] }] })
107
+ await ensureModules({ series: [{ type: "bar", data: [1] }] })
113
108
  // Second call should be instant (cached)
114
109
  const start = performance.now()
115
- await ensureModules({ series: [{ type: 'bar', data: [2] }] })
110
+ await ensureModules({ series: [{ type: "bar", data: [2] }] })
116
111
  const duration = performance.now() - start
117
112
  // Should be near-instant since modules are cached
118
113
  expect(duration).toBeLessThan(50)
119
114
  })
120
115
 
121
- it('handles series as single object (not array)', async () => {
116
+ it("handles series as single object (not array)", async () => {
122
117
  const core = await ensureModules({
123
- series: { type: 'bar', data: [1, 2, 3] },
118
+ series: { type: "bar", data: [1, 2, 3] },
124
119
  })
125
120
  expect(core).toBeDefined()
126
121
  })
127
122
 
128
- it('handles empty series gracefully', async () => {
123
+ it("handles empty series gracefully", async () => {
129
124
  const core = await ensureModules({ series: [] })
130
125
  expect(core).toBeDefined()
131
126
  })
132
127
 
133
- it('handles config with no series', async () => {
134
- const core = await ensureModules({ title: { text: 'Hello' } })
128
+ it("handles config with no series", async () => {
129
+ const core = await ensureModules({ title: { text: "Hello" } })
135
130
  expect(core).toBeDefined()
136
131
  })
137
132
 
138
- it('ignores unknown chart types', async () => {
133
+ it("ignores unknown chart types", async () => {
139
134
  const core = await ensureModules({
140
- series: [{ type: 'nonexistent', data: [1] }],
135
+ series: [{ type: "nonexistent", data: [1] }],
141
136
  })
142
137
  expect(core).toBeDefined()
143
138
  })
144
139
 
145
- it('loads SVG renderer when specified', async () => {
146
- const core = await ensureModules(
147
- { series: [{ type: 'bar', data: [1] }] },
148
- 'svg',
149
- )
140
+ it("loads SVG renderer when specified", async () => {
141
+ const core = await ensureModules({ series: [{ type: "bar", data: [1] }] }, "svg")
150
142
  expect(core).toBeDefined()
151
143
  })
152
144
 
153
- it('resets state with _resetLoader', async () => {
145
+ it("resets state with _resetLoader", async () => {
154
146
  await getCore() // load core
155
147
  _resetLoader()
156
148
  // After reset, core should be null internally but getCore still works
@@ -158,35 +150,35 @@ describe('loader', () => {
158
150
  expect(core).toBeDefined()
159
151
  })
160
152
 
161
- it('getCoreSync returns null before loading', () => {
153
+ it("getCoreSync returns null before loading", () => {
162
154
  _resetLoader()
163
155
  expect(getCoreSync()).toBeNull()
164
156
  })
165
157
 
166
- it('getCoreSync returns core after loading', async () => {
158
+ it("getCoreSync returns core after loading", async () => {
167
159
  await getCore()
168
160
  expect(getCoreSync()).not.toBeNull()
169
- expect(typeof getCoreSync()!.init).toBe('function')
161
+ expect(typeof getCoreSync()!.init).toBe("function")
170
162
  })
171
163
 
172
- it('manualUse registers modules when core is loaded', async () => {
164
+ it("manualUse registers modules when core is loaded", async () => {
173
165
  await getCore() // ensure core is loaded
174
166
  // Should not throw — registers module with core.use()
175
- const { CanvasRenderer } = await import('echarts/renderers')
167
+ const { CanvasRenderer } = await import("echarts/renderers")
176
168
  expect(() => manualUse(CanvasRenderer)).not.toThrow()
177
169
  })
178
170
 
179
- it('manualUse queues modules when core is not yet loaded', async () => {
171
+ it("manualUse queues modules when core is not yet loaded", async () => {
180
172
  _resetLoader()
181
173
  // Core not loaded yet — should queue, not throw
182
- const { CanvasRenderer } = await import('echarts/renderers')
174
+ const { CanvasRenderer } = await import("echarts/renderers")
183
175
  expect(() => manualUse(CanvasRenderer)).not.toThrow()
184
176
  })
185
177
 
186
- it('loads radar component for radar config key', async () => {
178
+ it("loads radar component for radar config key", async () => {
187
179
  const core = await ensureModules({
188
- radar: { indicator: [{ name: 'A' }] },
189
- series: [{ type: 'radar', data: [{ value: [1] }] }],
180
+ radar: { indicator: [{ name: "A" }] },
181
+ series: [{ type: "radar", data: [{ value: [1] }] }],
190
182
  })
191
183
  expect(core).toBeDefined()
192
184
  })
@@ -194,36 +186,36 @@ describe('loader', () => {
194
186
 
195
187
  // ─── Chart component ─────────────────────────────────────────────────────────
196
188
 
197
- describe('Chart component', () => {
198
- it('renders a div element', () => {
199
- const container = document.createElement('div')
189
+ describe("Chart component", () => {
190
+ it("renders a div element", () => {
191
+ const container = document.createElement("div")
200
192
  document.body.appendChild(container)
201
193
 
202
194
  const unmount = mount(
203
195
  <Chart
204
196
  options={() => ({
205
- series: [{ type: 'bar', data: [1, 2, 3] }],
197
+ series: [{ type: "bar", data: [1, 2, 3] }],
206
198
  })}
207
199
  style="height: 300px"
208
200
  />,
209
201
  container,
210
202
  )
211
203
 
212
- const div = container.querySelector('div')
204
+ const div = container.querySelector("div")
213
205
  expect(div).not.toBeNull()
214
206
 
215
207
  unmount()
216
208
  container.remove()
217
209
  })
218
210
 
219
- it('applies style and class props to container', () => {
220
- const container = document.createElement('div')
211
+ it("applies style and class props to container", () => {
212
+ const container = document.createElement("div")
221
213
  document.body.appendChild(container)
222
214
 
223
215
  const unmount = mount(
224
216
  <Chart
225
217
  options={() => ({
226
- series: [{ type: 'bar', data: [1] }],
218
+ series: [{ type: "bar", data: [1] }],
227
219
  })}
228
220
  style="height: 400px; width: 100%"
229
221
  class="revenue-chart"
@@ -231,9 +223,9 @@ describe('Chart component', () => {
231
223
  container,
232
224
  )
233
225
 
234
- const div = container.querySelector('div')
235
- expect(div?.getAttribute('style')).toContain('height: 400px')
236
- expect(div?.getAttribute('class')).toContain('revenue-chart')
226
+ const div = container.querySelector("div")
227
+ expect(div?.getAttribute("style")).toContain("height: 400px")
228
+ expect(div?.getAttribute("class")).toContain("revenue-chart")
237
229
 
238
230
  unmount()
239
231
  container.remove()
@@ -242,31 +234,31 @@ describe('Chart component', () => {
242
234
 
243
235
  // ─── useChart basic API ──────────────────────────────────────────────────────
244
236
 
245
- describe('useChart API', () => {
237
+ describe("useChart API", () => {
246
238
  // Import useChart lazily to avoid module-level echarts import issues
247
- it('returns the correct shape', async () => {
248
- const { useChart } = await import('../use-chart')
239
+ it("returns the correct shape", async () => {
240
+ const { useChart } = await import("../use-chart")
249
241
 
250
242
  const { result: chart, unmount } = mountWith(() =>
251
243
  useChart(() => ({
252
- series: [{ type: 'bar', data: [1, 2, 3] }],
244
+ series: [{ type: "bar", data: [1, 2, 3] }],
253
245
  })),
254
246
  )
255
247
 
256
- expect(typeof chart.ref).toBe('function')
248
+ expect(typeof chart.ref).toBe("function")
257
249
  expect(chart.instance()).toBeNull()
258
250
  expect(chart.loading()).toBe(true)
259
- expect(typeof chart.resize).toBe('function')
251
+ expect(typeof chart.resize).toBe("function")
260
252
 
261
253
  unmount()
262
254
  })
263
255
 
264
- it('resize does not throw when no instance', async () => {
265
- const { useChart } = await import('../use-chart')
256
+ it("resize does not throw when no instance", async () => {
257
+ const { useChart } = await import("../use-chart")
266
258
 
267
259
  const { result: chart, unmount } = mountWith(() =>
268
260
  useChart(() => ({
269
- series: [{ type: 'bar', data: [1] }],
261
+ series: [{ type: "bar", data: [1] }],
270
262
  })),
271
263
  )
272
264
 
@@ -274,12 +266,12 @@ describe('useChart API', () => {
274
266
  unmount()
275
267
  })
276
268
 
277
- it('loading starts as true', async () => {
278
- const { useChart } = await import('../use-chart')
269
+ it("loading starts as true", async () => {
270
+ const { useChart } = await import("../use-chart")
279
271
 
280
272
  const { result: chart, unmount } = mountWith(() =>
281
273
  useChart(() => ({
282
- series: [{ type: 'pie', data: [{ value: 1 }] }],
274
+ series: [{ type: "pie", data: [{ value: 1 }] }],
283
275
  })),
284
276
  )
285
277
 
@@ -290,30 +282,30 @@ describe('useChart API', () => {
290
282
 
291
283
  // ─── All supported chart types ──────────────────────────────────────────────
292
284
 
293
- describe('supported chart types', () => {
285
+ describe("supported chart types", () => {
294
286
  const chartTypes = [
295
- 'bar',
296
- 'line',
297
- 'pie',
298
- 'scatter',
299
- 'radar',
300
- 'heatmap',
301
- 'treemap',
302
- 'sunburst',
303
- 'sankey',
304
- 'funnel',
305
- 'gauge',
306
- 'graph',
307
- 'tree',
308
- 'boxplot',
309
- 'candlestick',
310
- 'parallel',
311
- 'themeRiver',
312
- 'effectScatter',
313
- 'lines',
314
- 'pictorialBar',
315
- 'custom',
316
- 'map',
287
+ "bar",
288
+ "line",
289
+ "pie",
290
+ "scatter",
291
+ "radar",
292
+ "heatmap",
293
+ "treemap",
294
+ "sunburst",
295
+ "sankey",
296
+ "funnel",
297
+ "gauge",
298
+ "graph",
299
+ "tree",
300
+ "boxplot",
301
+ "candlestick",
302
+ "parallel",
303
+ "themeRiver",
304
+ "effectScatter",
305
+ "lines",
306
+ "pictorialBar",
307
+ "custom",
308
+ "map",
317
309
  ]
318
310
 
319
311
  for (const type of chartTypes) {
@@ -329,25 +321,25 @@ describe('supported chart types', () => {
329
321
 
330
322
  // ─── All supported component keys ───────────────────────────────────────────
331
323
 
332
- describe('supported component keys', () => {
324
+ describe("supported component keys", () => {
333
325
  const componentKeys = [
334
- 'tooltip',
335
- 'legend',
336
- 'title',
337
- 'toolbox',
338
- 'dataZoom',
339
- 'visualMap',
340
- 'timeline',
341
- 'graphic',
342
- 'brush',
343
- 'calendar',
344
- 'dataset',
345
- 'aria',
346
- 'grid',
347
- 'xAxis',
348
- 'yAxis',
349
- 'polar',
350
- 'geo',
326
+ "tooltip",
327
+ "legend",
328
+ "title",
329
+ "toolbox",
330
+ "dataZoom",
331
+ "visualMap",
332
+ "timeline",
333
+ "graphic",
334
+ "brush",
335
+ "calendar",
336
+ "dataset",
337
+ "aria",
338
+ "grid",
339
+ "xAxis",
340
+ "yAxis",
341
+ "polar",
342
+ "geo",
351
343
  ]
352
344
 
353
345
  for (const key of componentKeys) {
@@ -355,9 +347,322 @@ describe('supported component keys', () => {
355
347
  _resetLoader()
356
348
  const core = await ensureModules({
357
349
  [key]: {},
358
- series: [{ type: 'bar', data: [1] }],
350
+ series: [{ type: "bar", data: [1] }],
359
351
  })
360
352
  expect(core).toBeDefined()
361
353
  })
362
354
  }
363
355
  })
356
+
357
+ // ─── Option validation ──────────────────────────────────────────────────────
358
+
359
+ describe("option validation", () => {
360
+ it("handles empty options object", async () => {
361
+ const core = await ensureModules({})
362
+ expect(core).toBeDefined()
363
+ })
364
+
365
+ it("throws on null series entries", async () => {
366
+ await expect(
367
+ ensureModules({
368
+ series: [null as any, { type: "bar", data: [1] }],
369
+ }),
370
+ ).rejects.toThrow()
371
+ })
372
+
373
+ it("handles series with no type property", async () => {
374
+ const core = await ensureModules({
375
+ series: [{ data: [1, 2, 3] }],
376
+ })
377
+ expect(core).toBeDefined()
378
+ })
379
+
380
+ it("handles undefined series value", async () => {
381
+ const core = await ensureModules({ series: undefined })
382
+ expect(core).toBeDefined()
383
+ })
384
+ })
385
+
386
+ // ─── Multiple chart instances ───────────────────────────────────────────────
387
+
388
+ describe("multiple chart instances", () => {
389
+ it("creates independent useChart instances", async () => {
390
+ const { useChart } = await import("../use-chart")
391
+
392
+ const { result: chart1, unmount: unmount1 } = mountWith(() =>
393
+ useChart(() => ({
394
+ series: [{ type: "bar", data: [1, 2, 3] }],
395
+ })),
396
+ )
397
+
398
+ const { result: chart2, unmount: unmount2 } = mountWith(() =>
399
+ useChart(() => ({
400
+ series: [{ type: "line", data: [4, 5, 6] }],
401
+ })),
402
+ )
403
+
404
+ // Each instance has its own signals
405
+ expect(chart1.instance).not.toBe(chart2.instance)
406
+ expect(chart1.loading).not.toBe(chart2.loading)
407
+ expect(chart1.error).not.toBe(chart2.error)
408
+
409
+ unmount1()
410
+ unmount2()
411
+ })
412
+
413
+ it("disposing one chart does not affect another", async () => {
414
+ const { useChart } = await import("../use-chart")
415
+
416
+ const { unmount: unmount1 } = mountWith(() =>
417
+ useChart(() => ({
418
+ series: [{ type: "bar", data: [1] }],
419
+ })),
420
+ )
421
+
422
+ const { result: chart2, unmount: unmount2 } = mountWith(() =>
423
+ useChart(() => ({
424
+ series: [{ type: "bar", data: [2] }],
425
+ })),
426
+ )
427
+
428
+ // Unmount first chart
429
+ unmount1()
430
+
431
+ // Second chart should still be functional
432
+ expect(chart2.loading()).toBe(true)
433
+ expect(chart2.error()).toBeNull()
434
+
435
+ unmount2()
436
+ })
437
+ })
438
+
439
+ // ─── Resize observer cleanup ────────────────────────────────────────────────
440
+
441
+ describe("resize observer cleanup", () => {
442
+ it("chart instance is set to null on unmount", async () => {
443
+ const { useChart } = await import("../use-chart")
444
+
445
+ const { result: chart, unmount } = mountWith(() =>
446
+ useChart(() => ({
447
+ series: [{ type: "bar", data: [1] }],
448
+ })),
449
+ )
450
+
451
+ // Before unmount — instance is null (no container bound)
452
+ expect(chart.instance()).toBeNull()
453
+
454
+ unmount()
455
+
456
+ // After unmount — instance remains null (was never created)
457
+ expect(chart.instance()).toBeNull()
458
+ })
459
+
460
+ it("onUnmount disposes the chart instance when it exists", async () => {
461
+ const { useChart } = await import("../use-chart")
462
+
463
+ const el = document.createElement("div")
464
+ document.body.appendChild(el)
465
+
466
+ const mountEl = document.createElement("div")
467
+ document.body.appendChild(mountEl)
468
+
469
+ let chartResult: ReturnType<typeof useChart> | undefined
470
+ const Child = () => {
471
+ chartResult = useChart(() => ({
472
+ series: [{ type: "bar", data: [1] }],
473
+ }))
474
+ chartResult.ref(el)
475
+ return null
476
+ }
477
+ const unmount = mount(<Child />, mountEl)
478
+
479
+ // Wait for async module load + chart init
480
+ await new Promise((r) => setTimeout(r, 300))
481
+
482
+ // Whether init succeeded or not (happy-dom has no real dimensions),
483
+ // unmount should not throw
484
+ expect(() => unmount()).not.toThrow()
485
+
486
+ // After unmount, instance should be null (cleaned up)
487
+ expect(chartResult!.instance()).toBeNull()
488
+
489
+ mountEl.remove()
490
+ el.remove()
491
+ })
492
+ })
493
+
494
+ // ─── Theme switching ────────────────────────────────────────────────────────
495
+
496
+ describe("theme config", () => {
497
+ it("passes theme to useChart config", async () => {
498
+ const { useChart } = await import("../use-chart")
499
+
500
+ const { result: chart, unmount } = mountWith(() =>
501
+ useChart(
502
+ () => ({
503
+ series: [{ type: "bar", data: [1] }],
504
+ }),
505
+ { theme: "dark" },
506
+ ),
507
+ )
508
+
509
+ // Chart instance not yet created (no container), but API is valid
510
+ expect(chart.instance()).toBeNull()
511
+ expect(chart.error()).toBeNull()
512
+
513
+ unmount()
514
+ })
515
+
516
+ it("Chart component accepts theme prop", () => {
517
+ const container = document.createElement("div")
518
+ document.body.appendChild(container)
519
+
520
+ const unmount = mount(
521
+ <Chart
522
+ options={() => ({
523
+ series: [{ type: "bar", data: [1] }],
524
+ })}
525
+ theme="dark"
526
+ style="height: 300px"
527
+ />,
528
+ container,
529
+ )
530
+
531
+ const div = container.querySelector("div")
532
+ expect(div).not.toBeNull()
533
+
534
+ unmount()
535
+ container.remove()
536
+ })
537
+ })
538
+
539
+ // ─── Error signal ───────────────────────────────────────────────────────────
540
+
541
+ describe("error signal", () => {
542
+ it("error is null initially", async () => {
543
+ const { useChart } = await import("../use-chart")
544
+
545
+ const { result: chart, unmount } = mountWith(() =>
546
+ useChart(() => ({
547
+ series: [{ type: "bar", data: [1] }],
548
+ })),
549
+ )
550
+
551
+ expect(chart.error()).toBeNull()
552
+ unmount()
553
+ })
554
+
555
+ it("error is set when optionsFn throws during init", async () => {
556
+ const { useChart } = await import("../use-chart")
557
+
558
+ const { result: chart, unmount } = mountWith(() =>
559
+ useChart(() => {
560
+ throw new Error("Bad options")
561
+ }),
562
+ )
563
+
564
+ // Trigger init by setting a container
565
+ const el = document.createElement("div")
566
+ document.body.appendChild(el)
567
+ chart.ref(el)
568
+
569
+ await new Promise((r) => setTimeout(r, 100))
570
+
571
+ expect(chart.error()).not.toBeNull()
572
+ expect(chart.error()!.message).toBe("Bad options")
573
+ expect(chart.loading()).toBe(false)
574
+
575
+ unmount()
576
+ el.remove()
577
+ })
578
+
579
+ it("error is set when optionsFn throws non-Error value during init", async () => {
580
+ const { useChart } = await import("../use-chart")
581
+
582
+ const { result: chart, unmount } = mountWith(() =>
583
+ useChart(() => {
584
+ throw "string failure"
585
+ }),
586
+ )
587
+
588
+ const el = document.createElement("div")
589
+ document.body.appendChild(el)
590
+ chart.ref(el)
591
+
592
+ await new Promise((r) => setTimeout(r, 100))
593
+
594
+ expect(chart.error()).not.toBeNull()
595
+ expect(chart.error()!.message).toBe("string failure")
596
+
597
+ unmount()
598
+ el.remove()
599
+ })
600
+
601
+ it("useChart returns error signal in result shape", async () => {
602
+ const { useChart } = await import("../use-chart")
603
+
604
+ const { result: chart, unmount } = mountWith(() =>
605
+ useChart(() => ({
606
+ series: [{ type: "bar", data: [1] }],
607
+ })),
608
+ )
609
+
610
+ expect(typeof chart.error).toBe("function")
611
+ expect(chart.error()).toBeNull()
612
+
613
+ unmount()
614
+ })
615
+ })
616
+
617
+ // ─── Chart component events ─────────────────────────────────────────────────
618
+
619
+ describe("Chart component events", () => {
620
+ it("renders with event handler props without throwing", () => {
621
+ const container = document.createElement("div")
622
+ document.body.appendChild(container)
623
+
624
+ const onClick = vi.fn()
625
+ const onMouseover = vi.fn()
626
+
627
+ const unmount = mount(
628
+ <Chart
629
+ options={() => ({
630
+ series: [{ type: "bar", data: [1] }],
631
+ })}
632
+ style="height: 300px"
633
+ onClick={onClick}
634
+ onMouseover={onMouseover}
635
+ />,
636
+ container,
637
+ )
638
+
639
+ expect(container.querySelector("div")).not.toBeNull()
640
+
641
+ unmount()
642
+ container.remove()
643
+ })
644
+ })
645
+
646
+ // ─── Reactive options with signal ───────────────────────────────────────────
647
+
648
+ describe("reactive options", () => {
649
+ it("useChart accepts optionsFn that reads signals", async () => {
650
+ const { useChart } = await import("../use-chart")
651
+
652
+ const data = signal([1, 2, 3])
653
+
654
+ const { result: chart, unmount } = mountWith(() =>
655
+ useChart(() => ({
656
+ series: [{ type: "bar", data: data() }],
657
+ })),
658
+ )
659
+
660
+ expect(chart.loading()).toBe(true)
661
+ expect(chart.error()).toBeNull()
662
+
663
+ // Updating the signal should not throw
664
+ data.set([4, 5, 6])
665
+
666
+ unmount()
667
+ })
668
+ })