@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.
- package/lib/analysis/index.js.html +1 -1
- package/lib/analysis/manual.js.html +1 -1
- package/lib/{charts-Ckh2qxB5.js → charts-lo2KeDld.js} +164 -164
- package/lib/charts-lo2KeDld.js.map +1 -0
- package/lib/{components-BcPePBeS.js → components-ClWy1Ztp.js} +128 -128
- package/lib/components-ClWy1Ztp.js.map +1 -0
- package/lib/{core-9w0g6EOI.js → core-BiuQ3y-t.js} +11 -11
- package/lib/core-BiuQ3y-t.js.map +1 -0
- package/lib/{createSeriesData-DOHScdgk.js → createSeriesData-BGy-6PqI.js} +6 -6
- package/lib/createSeriesData-BGy-6PqI.js.map +1 -0
- package/lib/{customGraphicKeyframeAnimation-CvkEkSt_.js → customGraphicKeyframeAnimation-BIbJI8ew.js} +61 -61
- package/lib/customGraphicKeyframeAnimation-BIbJI8ew.js.map +1 -0
- package/lib/{graphic-CPJ2K90a.js → graphic-Bt7SEwll.js} +59 -59
- package/lib/graphic-Bt7SEwll.js.map +1 -0
- package/lib/index.js +60 -52
- package/lib/index.js.map +1 -1
- package/lib/manual.js +60 -52
- package/lib/manual.js.map +1 -1
- package/lib/{parseGeoJson-BlBe5Vig.js → parseGeoJson-NjUY1feF.js} +115 -115
- package/lib/parseGeoJson-NjUY1feF.js.map +1 -0
- package/lib/{renderers-Dytryvoy.js → renderers-BnAAXHfG.js} +16 -16
- package/lib/renderers-BnAAXHfG.js.map +1 -0
- package/lib/types/index.d.ts +3 -3
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/manual.d.ts +3 -3
- package/lib/types/manual.d.ts.map +1 -1
- package/package.json +14 -7
- package/src/chart-component.tsx +18 -10
- package/src/index.ts +3 -3
- package/src/loader.ts +70 -73
- package/src/manual.ts +4 -4
- package/src/tests/charts.test.tsx +431 -126
- package/src/types.ts +8 -9
- package/src/use-chart.ts +8 -8
- package/lib/charts-Ckh2qxB5.js.map +0 -1
- package/lib/components-BcPePBeS.js.map +0 -1
- package/lib/core-9w0g6EOI.js.map +0 -1
- package/lib/createSeriesData-DOHScdgk.js.map +0 -1
- package/lib/customGraphicKeyframeAnimation-CvkEkSt_.js.map +0 -1
- package/lib/graphic-CPJ2K90a.js.map +0 -1
- package/lib/parseGeoJson-BlBe5Vig.js.map +0 -1
- package/lib/renderers-Dytryvoy.js.map +0 -1
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
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(
|
|
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(
|
|
38
|
-
it(
|
|
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(
|
|
42
|
-
expect(typeof core.use).toBe(
|
|
36
|
+
expect(typeof core.init).toBe("function")
|
|
37
|
+
expect(typeof core.use).toBe("function")
|
|
43
38
|
})
|
|
44
39
|
|
|
45
|
-
it(
|
|
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(
|
|
46
|
+
it("auto-detects BarChart from series type", async () => {
|
|
52
47
|
const core = await ensureModules({
|
|
53
|
-
series: [{ type:
|
|
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(
|
|
54
|
+
it("auto-detects PieChart from series type", async () => {
|
|
60
55
|
const core = await ensureModules({
|
|
61
|
-
series: [{ type:
|
|
56
|
+
series: [{ type: "pie", data: [{ value: 1 }] }],
|
|
62
57
|
})
|
|
63
58
|
expect(core).toBeDefined()
|
|
64
59
|
})
|
|
65
60
|
|
|
66
|
-
it(
|
|
61
|
+
it("auto-detects LineChart from series type", async () => {
|
|
67
62
|
const core = await ensureModules({
|
|
68
|
-
series: [{ type:
|
|
63
|
+
series: [{ type: "line", data: [1, 2, 3] }],
|
|
69
64
|
})
|
|
70
65
|
expect(core).toBeDefined()
|
|
71
66
|
})
|
|
72
67
|
|
|
73
|
-
it(
|
|
68
|
+
it("auto-detects components from config keys", async () => {
|
|
74
69
|
const core = await ensureModules({
|
|
75
|
-
tooltip: { trigger:
|
|
70
|
+
tooltip: { trigger: "axis" },
|
|
76
71
|
legend: {},
|
|
77
|
-
xAxis: { type:
|
|
78
|
-
yAxis: { type:
|
|
79
|
-
series: [{ type:
|
|
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(
|
|
79
|
+
it("auto-detects series features (markPoint, markLine, markArea)", async () => {
|
|
85
80
|
const core = await ensureModules({
|
|
86
81
|
series: [
|
|
87
82
|
{
|
|
88
|
-
type:
|
|
83
|
+
type: "line",
|
|
89
84
|
data: [1, 2, 3],
|
|
90
|
-
markPoint: { data: [{ type:
|
|
91
|
-
markLine: { data: [{ type:
|
|
92
|
-
markArea: { data: [[{ xAxis:
|
|
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(
|
|
94
|
+
it("loads multiple chart types in one config", async () => {
|
|
100
95
|
const core = await ensureModules({
|
|
101
96
|
series: [
|
|
102
|
-
{ type:
|
|
103
|
-
{ type:
|
|
104
|
-
{ type:
|
|
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(
|
|
105
|
+
it("caches modules across calls", async () => {
|
|
111
106
|
// First call loads BarChart
|
|
112
|
-
await ensureModules({ series: [{ type:
|
|
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:
|
|
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(
|
|
116
|
+
it("handles series as single object (not array)", async () => {
|
|
122
117
|
const core = await ensureModules({
|
|
123
|
-
series: { type:
|
|
118
|
+
series: { type: "bar", data: [1, 2, 3] },
|
|
124
119
|
})
|
|
125
120
|
expect(core).toBeDefined()
|
|
126
121
|
})
|
|
127
122
|
|
|
128
|
-
it(
|
|
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(
|
|
134
|
-
const core = await ensureModules({ title: { text:
|
|
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(
|
|
133
|
+
it("ignores unknown chart types", async () => {
|
|
139
134
|
const core = await ensureModules({
|
|
140
|
-
series: [{ type:
|
|
135
|
+
series: [{ type: "nonexistent", data: [1] }],
|
|
141
136
|
})
|
|
142
137
|
expect(core).toBeDefined()
|
|
143
138
|
})
|
|
144
139
|
|
|
145
|
-
it(
|
|
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(
|
|
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(
|
|
153
|
+
it("getCoreSync returns null before loading", () => {
|
|
162
154
|
_resetLoader()
|
|
163
155
|
expect(getCoreSync()).toBeNull()
|
|
164
156
|
})
|
|
165
157
|
|
|
166
|
-
it(
|
|
158
|
+
it("getCoreSync returns core after loading", async () => {
|
|
167
159
|
await getCore()
|
|
168
160
|
expect(getCoreSync()).not.toBeNull()
|
|
169
|
-
expect(typeof getCoreSync()!.init).toBe(
|
|
161
|
+
expect(typeof getCoreSync()!.init).toBe("function")
|
|
170
162
|
})
|
|
171
163
|
|
|
172
|
-
it(
|
|
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(
|
|
167
|
+
const { CanvasRenderer } = await import("echarts/renderers")
|
|
176
168
|
expect(() => manualUse(CanvasRenderer)).not.toThrow()
|
|
177
169
|
})
|
|
178
170
|
|
|
179
|
-
it(
|
|
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(
|
|
174
|
+
const { CanvasRenderer } = await import("echarts/renderers")
|
|
183
175
|
expect(() => manualUse(CanvasRenderer)).not.toThrow()
|
|
184
176
|
})
|
|
185
177
|
|
|
186
|
-
it(
|
|
178
|
+
it("loads radar component for radar config key", async () => {
|
|
187
179
|
const core = await ensureModules({
|
|
188
|
-
radar: { indicator: [{ name:
|
|
189
|
-
series: [{ type:
|
|
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(
|
|
198
|
-
it(
|
|
199
|
-
const container = document.createElement(
|
|
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:
|
|
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(
|
|
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(
|
|
220
|
-
const container = document.createElement(
|
|
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:
|
|
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(
|
|
235
|
-
expect(div?.getAttribute(
|
|
236
|
-
expect(div?.getAttribute(
|
|
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(
|
|
237
|
+
describe("useChart API", () => {
|
|
246
238
|
// Import useChart lazily to avoid module-level echarts import issues
|
|
247
|
-
it(
|
|
248
|
-
const { useChart } = await import(
|
|
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:
|
|
244
|
+
series: [{ type: "bar", data: [1, 2, 3] }],
|
|
253
245
|
})),
|
|
254
246
|
)
|
|
255
247
|
|
|
256
|
-
expect(typeof chart.ref).toBe(
|
|
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(
|
|
251
|
+
expect(typeof chart.resize).toBe("function")
|
|
260
252
|
|
|
261
253
|
unmount()
|
|
262
254
|
})
|
|
263
255
|
|
|
264
|
-
it(
|
|
265
|
-
const { useChart } = await import(
|
|
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:
|
|
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(
|
|
278
|
-
const { useChart } = await import(
|
|
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:
|
|
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(
|
|
285
|
+
describe("supported chart types", () => {
|
|
294
286
|
const chartTypes = [
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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(
|
|
324
|
+
describe("supported component keys", () => {
|
|
333
325
|
const componentKeys = [
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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:
|
|
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
|
+
})
|