@pyreon/charts 0.6.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.
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/analysis/manual.js.html +5406 -0
- package/lib/charts-Ckh2qxB5.js +17571 -0
- package/lib/charts-Ckh2qxB5.js.map +1 -0
- package/lib/components-BcPePBeS.js +13688 -0
- package/lib/components-BcPePBeS.js.map +1 -0
- package/lib/core-9w0g6EOI.js +200 -0
- package/lib/core-9w0g6EOI.js.map +1 -0
- package/lib/createSeriesData-DOHScdgk.js +412 -0
- package/lib/createSeriesData-DOHScdgk.js.map +1 -0
- package/lib/customGraphicKeyframeAnimation-CvkEkSt_.js +8524 -0
- package/lib/customGraphicKeyframeAnimation-CvkEkSt_.js.map +1 -0
- package/lib/graphic-CPJ2K90a.js +10771 -0
- package/lib/graphic-CPJ2K90a.js.map +1 -0
- package/lib/index.js +309 -0
- package/lib/index.js.map +1 -0
- package/lib/manual.js +334 -0
- package/lib/manual.js.map +1 -0
- package/lib/parseGeoJson-BlBe5Vig.js +18498 -0
- package/lib/parseGeoJson-BlBe5Vig.js.map +1 -0
- package/lib/renderers-Dytryvoy.js +2044 -0
- package/lib/renderers-Dytryvoy.js.map +1 -0
- package/lib/types/charts.d.ts +7968 -0
- package/lib/types/charts.d.ts.map +1 -0
- package/lib/types/components.d.ts +4074 -0
- package/lib/types/components.d.ts.map +1 -0
- package/lib/types/core.d.ts +98 -0
- package/lib/types/core.d.ts.map +1 -0
- package/lib/types/createSeriesData.d.ts +325 -0
- package/lib/types/createSeriesData.d.ts.map +1 -0
- package/lib/types/customGraphicKeyframeAnimation.d.ts +3531 -0
- package/lib/types/customGraphicKeyframeAnimation.d.ts.map +1 -0
- package/lib/types/graphic.d.ts +4043 -0
- package/lib/types/graphic.d.ts.map +1 -0
- package/lib/types/index.d.ts +255 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index2.d.ts +2221 -0
- package/lib/types/index2.d.ts.map +1 -0
- package/lib/types/manual.d.ts +279 -0
- package/lib/types/manual.d.ts.map +1 -0
- package/lib/types/manual2.d.ts +2238 -0
- package/lib/types/manual2.d.ts.map +1 -0
- package/lib/types/parseGeoJson.d.ts +8695 -0
- package/lib/types/parseGeoJson.d.ts.map +1 -0
- package/lib/types/renderers.d.ts +995 -0
- package/lib/types/renderers.d.ts.map +1 -0
- package/package.json +58 -0
- package/src/chart-component.tsx +53 -0
- package/src/index.ts +64 -0
- package/src/loader.ts +222 -0
- package/src/manual.ts +36 -0
- package/src/tests/charts.test.tsx +363 -0
- package/src/types.ts +118 -0
- package/src/use-chart.ts +136 -0
|
@@ -0,0 +1,363 @@
|
|
|
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'
|
|
10
|
+
|
|
11
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function mountWith<T>(fn: () => T): { result: T; unmount: () => void } {
|
|
14
|
+
let result: T | undefined
|
|
15
|
+
const el = document.createElement('div')
|
|
16
|
+
document.body.appendChild(el)
|
|
17
|
+
const Child = () => {
|
|
18
|
+
result = fn()
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
const unmount = mount(<Child />, el)
|
|
22
|
+
return {
|
|
23
|
+
result: result!,
|
|
24
|
+
unmount: () => {
|
|
25
|
+
unmount()
|
|
26
|
+
el.remove()
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
_resetLoader()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// ─── Loader ───────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
describe('loader', () => {
|
|
38
|
+
it('lazily loads echarts/core on first call', async () => {
|
|
39
|
+
const core = await getCore()
|
|
40
|
+
expect(core).toBeDefined()
|
|
41
|
+
expect(typeof core.init).toBe('function')
|
|
42
|
+
expect(typeof core.use).toBe('function')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('caches core module on subsequent calls', async () => {
|
|
46
|
+
const core1 = await getCore()
|
|
47
|
+
const core2 = await getCore()
|
|
48
|
+
expect(core1).toBe(core2)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('auto-detects BarChart from series type', async () => {
|
|
52
|
+
const core = await ensureModules({
|
|
53
|
+
series: [{ type: 'bar', data: [1, 2, 3] }],
|
|
54
|
+
})
|
|
55
|
+
expect(core).toBeDefined()
|
|
56
|
+
// If it didn't throw, the modules were registered successfully
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('auto-detects PieChart from series type', async () => {
|
|
60
|
+
const core = await ensureModules({
|
|
61
|
+
series: [{ type: 'pie', data: [{ value: 1 }] }],
|
|
62
|
+
})
|
|
63
|
+
expect(core).toBeDefined()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('auto-detects LineChart from series type', async () => {
|
|
67
|
+
const core = await ensureModules({
|
|
68
|
+
series: [{ type: 'line', data: [1, 2, 3] }],
|
|
69
|
+
})
|
|
70
|
+
expect(core).toBeDefined()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('auto-detects components from config keys', async () => {
|
|
74
|
+
const core = await ensureModules({
|
|
75
|
+
tooltip: { trigger: 'axis' },
|
|
76
|
+
legend: {},
|
|
77
|
+
xAxis: { type: 'category' },
|
|
78
|
+
yAxis: { type: 'value' },
|
|
79
|
+
series: [{ type: 'bar', data: [1] }],
|
|
80
|
+
})
|
|
81
|
+
expect(core).toBeDefined()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('auto-detects series features (markPoint, markLine, markArea)', async () => {
|
|
85
|
+
const core = await ensureModules({
|
|
86
|
+
series: [
|
|
87
|
+
{
|
|
88
|
+
type: 'line',
|
|
89
|
+
data: [1, 2, 3],
|
|
90
|
+
markPoint: { data: [{ type: 'max' }] },
|
|
91
|
+
markLine: { data: [{ type: 'average' }] },
|
|
92
|
+
markArea: { data: [[{ xAxis: 'A' }, { xAxis: 'B' }]] },
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
})
|
|
96
|
+
expect(core).toBeDefined()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('loads multiple chart types in one config', async () => {
|
|
100
|
+
const core = await ensureModules({
|
|
101
|
+
series: [
|
|
102
|
+
{ type: 'bar', data: [1, 2] },
|
|
103
|
+
{ type: 'line', data: [3, 4] },
|
|
104
|
+
{ type: 'scatter', data: [[1, 2]] },
|
|
105
|
+
],
|
|
106
|
+
})
|
|
107
|
+
expect(core).toBeDefined()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('caches modules across calls', async () => {
|
|
111
|
+
// First call loads BarChart
|
|
112
|
+
await ensureModules({ series: [{ type: 'bar', data: [1] }] })
|
|
113
|
+
// Second call should be instant (cached)
|
|
114
|
+
const start = performance.now()
|
|
115
|
+
await ensureModules({ series: [{ type: 'bar', data: [2] }] })
|
|
116
|
+
const duration = performance.now() - start
|
|
117
|
+
// Should be near-instant since modules are cached
|
|
118
|
+
expect(duration).toBeLessThan(50)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('handles series as single object (not array)', async () => {
|
|
122
|
+
const core = await ensureModules({
|
|
123
|
+
series: { type: 'bar', data: [1, 2, 3] },
|
|
124
|
+
})
|
|
125
|
+
expect(core).toBeDefined()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('handles empty series gracefully', async () => {
|
|
129
|
+
const core = await ensureModules({ series: [] })
|
|
130
|
+
expect(core).toBeDefined()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('handles config with no series', async () => {
|
|
134
|
+
const core = await ensureModules({ title: { text: 'Hello' } })
|
|
135
|
+
expect(core).toBeDefined()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('ignores unknown chart types', async () => {
|
|
139
|
+
const core = await ensureModules({
|
|
140
|
+
series: [{ type: 'nonexistent', data: [1] }],
|
|
141
|
+
})
|
|
142
|
+
expect(core).toBeDefined()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('loads SVG renderer when specified', async () => {
|
|
146
|
+
const core = await ensureModules(
|
|
147
|
+
{ series: [{ type: 'bar', data: [1] }] },
|
|
148
|
+
'svg',
|
|
149
|
+
)
|
|
150
|
+
expect(core).toBeDefined()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('resets state with _resetLoader', async () => {
|
|
154
|
+
await getCore() // load core
|
|
155
|
+
_resetLoader()
|
|
156
|
+
// After reset, core should be null internally but getCore still works
|
|
157
|
+
const core = await getCore()
|
|
158
|
+
expect(core).toBeDefined()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('getCoreSync returns null before loading', () => {
|
|
162
|
+
_resetLoader()
|
|
163
|
+
expect(getCoreSync()).toBeNull()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('getCoreSync returns core after loading', async () => {
|
|
167
|
+
await getCore()
|
|
168
|
+
expect(getCoreSync()).not.toBeNull()
|
|
169
|
+
expect(typeof getCoreSync()!.init).toBe('function')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('manualUse registers modules when core is loaded', async () => {
|
|
173
|
+
await getCore() // ensure core is loaded
|
|
174
|
+
// Should not throw — registers module with core.use()
|
|
175
|
+
const { CanvasRenderer } = await import('echarts/renderers')
|
|
176
|
+
expect(() => manualUse(CanvasRenderer)).not.toThrow()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('manualUse queues modules when core is not yet loaded', async () => {
|
|
180
|
+
_resetLoader()
|
|
181
|
+
// Core not loaded yet — should queue, not throw
|
|
182
|
+
const { CanvasRenderer } = await import('echarts/renderers')
|
|
183
|
+
expect(() => manualUse(CanvasRenderer)).not.toThrow()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('loads radar component for radar config key', async () => {
|
|
187
|
+
const core = await ensureModules({
|
|
188
|
+
radar: { indicator: [{ name: 'A' }] },
|
|
189
|
+
series: [{ type: 'radar', data: [{ value: [1] }] }],
|
|
190
|
+
})
|
|
191
|
+
expect(core).toBeDefined()
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// ─── Chart component ─────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
describe('Chart component', () => {
|
|
198
|
+
it('renders a div element', () => {
|
|
199
|
+
const container = document.createElement('div')
|
|
200
|
+
document.body.appendChild(container)
|
|
201
|
+
|
|
202
|
+
const unmount = mount(
|
|
203
|
+
<Chart
|
|
204
|
+
options={() => ({
|
|
205
|
+
series: [{ type: 'bar', data: [1, 2, 3] }],
|
|
206
|
+
})}
|
|
207
|
+
style="height: 300px"
|
|
208
|
+
/>,
|
|
209
|
+
container,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
const div = container.querySelector('div')
|
|
213
|
+
expect(div).not.toBeNull()
|
|
214
|
+
|
|
215
|
+
unmount()
|
|
216
|
+
container.remove()
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('applies style and class props to container', () => {
|
|
220
|
+
const container = document.createElement('div')
|
|
221
|
+
document.body.appendChild(container)
|
|
222
|
+
|
|
223
|
+
const unmount = mount(
|
|
224
|
+
<Chart
|
|
225
|
+
options={() => ({
|
|
226
|
+
series: [{ type: 'bar', data: [1] }],
|
|
227
|
+
})}
|
|
228
|
+
style="height: 400px; width: 100%"
|
|
229
|
+
class="revenue-chart"
|
|
230
|
+
/>,
|
|
231
|
+
container,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
const div = container.querySelector('div')
|
|
235
|
+
expect(div?.getAttribute('style')).toContain('height: 400px')
|
|
236
|
+
expect(div?.getAttribute('class')).toContain('revenue-chart')
|
|
237
|
+
|
|
238
|
+
unmount()
|
|
239
|
+
container.remove()
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
// ─── useChart basic API ──────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
describe('useChart API', () => {
|
|
246
|
+
// Import useChart lazily to avoid module-level echarts import issues
|
|
247
|
+
it('returns the correct shape', async () => {
|
|
248
|
+
const { useChart } = await import('../use-chart')
|
|
249
|
+
|
|
250
|
+
const { result: chart, unmount } = mountWith(() =>
|
|
251
|
+
useChart(() => ({
|
|
252
|
+
series: [{ type: 'bar', data: [1, 2, 3] }],
|
|
253
|
+
})),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
expect(typeof chart.ref).toBe('function')
|
|
257
|
+
expect(chart.instance()).toBeNull()
|
|
258
|
+
expect(chart.loading()).toBe(true)
|
|
259
|
+
expect(typeof chart.resize).toBe('function')
|
|
260
|
+
|
|
261
|
+
unmount()
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('resize does not throw when no instance', async () => {
|
|
265
|
+
const { useChart } = await import('../use-chart')
|
|
266
|
+
|
|
267
|
+
const { result: chart, unmount } = mountWith(() =>
|
|
268
|
+
useChart(() => ({
|
|
269
|
+
series: [{ type: 'bar', data: [1] }],
|
|
270
|
+
})),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
expect(() => chart.resize()).not.toThrow()
|
|
274
|
+
unmount()
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('loading starts as true', async () => {
|
|
278
|
+
const { useChart } = await import('../use-chart')
|
|
279
|
+
|
|
280
|
+
const { result: chart, unmount } = mountWith(() =>
|
|
281
|
+
useChart(() => ({
|
|
282
|
+
series: [{ type: 'pie', data: [{ value: 1 }] }],
|
|
283
|
+
})),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
expect(chart.loading()).toBe(true)
|
|
287
|
+
unmount()
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// ─── All supported chart types ──────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
describe('supported chart types', () => {
|
|
294
|
+
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',
|
|
317
|
+
]
|
|
318
|
+
|
|
319
|
+
for (const type of chartTypes) {
|
|
320
|
+
it(`loads modules for type: ${type}`, async () => {
|
|
321
|
+
_resetLoader()
|
|
322
|
+
const core = await ensureModules({
|
|
323
|
+
series: [{ type, data: [1] }],
|
|
324
|
+
})
|
|
325
|
+
expect(core).toBeDefined()
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
// ─── All supported component keys ───────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
describe('supported component keys', () => {
|
|
333
|
+
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',
|
|
351
|
+
]
|
|
352
|
+
|
|
353
|
+
for (const key of componentKeys) {
|
|
354
|
+
it(`loads component for config key: ${key}`, async () => {
|
|
355
|
+
_resetLoader()
|
|
356
|
+
const core = await ensureModules({
|
|
357
|
+
[key]: {},
|
|
358
|
+
series: [{ type: 'bar', data: [1] }],
|
|
359
|
+
})
|
|
360
|
+
expect(core).toBeDefined()
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
})
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { Props } from '@pyreon/core'
|
|
2
|
+
import type { Signal } from '@pyreon/reactivity'
|
|
3
|
+
import type { ComposeOption, EChartsOption, SetOptionOpts } from 'echarts'
|
|
4
|
+
import type { ECharts } from 'echarts/core'
|
|
5
|
+
|
|
6
|
+
// ─── Re-export ECharts types for consumer convenience ────────────────────────
|
|
7
|
+
|
|
8
|
+
// Re-export series option types
|
|
9
|
+
// Re-export component option types
|
|
10
|
+
export type {
|
|
11
|
+
BarSeriesOption,
|
|
12
|
+
BoxplotSeriesOption,
|
|
13
|
+
CandlestickSeriesOption,
|
|
14
|
+
DataZoomComponentOption,
|
|
15
|
+
FunnelSeriesOption,
|
|
16
|
+
GaugeSeriesOption,
|
|
17
|
+
GraphSeriesOption,
|
|
18
|
+
GridComponentOption,
|
|
19
|
+
HeatmapSeriesOption,
|
|
20
|
+
LegendComponentOption,
|
|
21
|
+
LineSeriesOption,
|
|
22
|
+
PieSeriesOption,
|
|
23
|
+
RadarSeriesOption,
|
|
24
|
+
SankeySeriesOption,
|
|
25
|
+
ScatterSeriesOption,
|
|
26
|
+
SunburstSeriesOption,
|
|
27
|
+
TitleComponentOption,
|
|
28
|
+
ToolboxComponentOption,
|
|
29
|
+
TooltipComponentOption,
|
|
30
|
+
TreemapSeriesOption,
|
|
31
|
+
TreeSeriesOption,
|
|
32
|
+
VisualMapComponentOption,
|
|
33
|
+
} from 'echarts'
|
|
34
|
+
export type { ComposeOption, ECharts, EChartsOption, SetOptionOpts }
|
|
35
|
+
|
|
36
|
+
// ─── Event types (duck-typed to avoid echarts dual-package type conflicts) ───
|
|
37
|
+
|
|
38
|
+
/** Chart event params — duck-typed to work across echarts entry points */
|
|
39
|
+
export interface ChartEventParams {
|
|
40
|
+
componentType?: string
|
|
41
|
+
seriesType?: string
|
|
42
|
+
seriesIndex?: number
|
|
43
|
+
seriesName?: string
|
|
44
|
+
name?: string
|
|
45
|
+
dataIndex?: number
|
|
46
|
+
data?: unknown
|
|
47
|
+
dataType?: string
|
|
48
|
+
value?: unknown
|
|
49
|
+
color?: string
|
|
50
|
+
event?: Event
|
|
51
|
+
[key: string]: unknown
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Chart config ────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Configuration for useChart.
|
|
58
|
+
*/
|
|
59
|
+
export interface UseChartConfig {
|
|
60
|
+
/** ECharts theme — 'dark', a registered theme name, or a theme object */
|
|
61
|
+
theme?: string | Record<string, unknown>
|
|
62
|
+
/** Renderer — 'canvas' (default, best performance) or 'svg' */
|
|
63
|
+
renderer?: 'canvas' | 'svg'
|
|
64
|
+
/** ECharts locale — 'EN' (default), 'ZH', etc. */
|
|
65
|
+
locale?: string
|
|
66
|
+
/** Whether to replace all options instead of merging — default: false */
|
|
67
|
+
notMerge?: boolean
|
|
68
|
+
/** Whether to batch updates — default: true */
|
|
69
|
+
lazyUpdate?: boolean
|
|
70
|
+
/** Device pixel ratio — default: window.devicePixelRatio */
|
|
71
|
+
devicePixelRatio?: number
|
|
72
|
+
/** Width override — default: container width */
|
|
73
|
+
width?: number
|
|
74
|
+
/** Height override — default: container height */
|
|
75
|
+
height?: number
|
|
76
|
+
/** Called when chart instance is created */
|
|
77
|
+
onInit?: (instance: ECharts) => void
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Return type of useChart.
|
|
82
|
+
*/
|
|
83
|
+
export interface UseChartResult {
|
|
84
|
+
/** Bind to container element via ref */
|
|
85
|
+
ref: (el: Element | null) => void
|
|
86
|
+
/** The ECharts instance — null until mounted and modules loaded */
|
|
87
|
+
instance: Signal<ECharts | null>
|
|
88
|
+
/** True while ECharts modules are being dynamically imported */
|
|
89
|
+
loading: Signal<boolean>
|
|
90
|
+
/** Error signal — set if chart init or setOption throws */
|
|
91
|
+
error: Signal<Error | null>
|
|
92
|
+
/** Manually trigger resize */
|
|
93
|
+
resize: () => void
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Props for the <Chart /> component.
|
|
98
|
+
* Generic parameter narrows the option type for exact autocomplete.
|
|
99
|
+
*/
|
|
100
|
+
export interface ChartProps<TOption extends EChartsOption = EChartsOption>
|
|
101
|
+
extends Props {
|
|
102
|
+
/** Reactive ECharts option config — fully typed */
|
|
103
|
+
options: () => TOption
|
|
104
|
+
/** ECharts theme */
|
|
105
|
+
theme?: string | Record<string, unknown>
|
|
106
|
+
/** Renderer — 'canvas' (default) or 'svg' */
|
|
107
|
+
renderer?: 'canvas' | 'svg'
|
|
108
|
+
/** CSS style for the container div */
|
|
109
|
+
style?: string
|
|
110
|
+
/** CSS class for the container div */
|
|
111
|
+
class?: string
|
|
112
|
+
/** Click event handler */
|
|
113
|
+
onClick?: (params: ChartEventParams) => void
|
|
114
|
+
/** Mouseover event handler */
|
|
115
|
+
onMouseover?: (params: ChartEventParams) => void
|
|
116
|
+
/** Mouseout event handler */
|
|
117
|
+
onMouseout?: (params: ChartEventParams) => void
|
|
118
|
+
}
|
package/src/use-chart.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { onUnmount } from '@pyreon/core'
|
|
2
|
+
import { effect, signal } from '@pyreon/reactivity'
|
|
3
|
+
import type { EChartsOption } from 'echarts'
|
|
4
|
+
import { ensureModules } from './loader'
|
|
5
|
+
import type { UseChartConfig, UseChartResult } from './types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Reactive ECharts hook. Creates a chart instance bound to a container
|
|
9
|
+
* element, with automatic module lazy-loading, signal tracking, resize
|
|
10
|
+
* handling, error capture, and cleanup.
|
|
11
|
+
*
|
|
12
|
+
* Generic parameter `TOption` narrows the option type for exact autocomplete.
|
|
13
|
+
* Use `ComposeOption<SeriesUnion>` from ECharts to restrict to specific chart types.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* // Default — accepts any ECharts option
|
|
18
|
+
* const chart = useChart(() => ({
|
|
19
|
+
* series: [{ type: 'bar', data: revenue() }],
|
|
20
|
+
* }))
|
|
21
|
+
*
|
|
22
|
+
* // Strict — only bar + line allowed, full autocomplete
|
|
23
|
+
* import type { ComposeOption, BarSeriesOption, LineSeriesOption } from '@pyreon/charts'
|
|
24
|
+
* type MyChartOption = ComposeOption<BarSeriesOption | LineSeriesOption>
|
|
25
|
+
*
|
|
26
|
+
* const chart = useChart<MyChartOption>(() => ({
|
|
27
|
+
* series: [{ type: 'bar', data: revenue() }], // ✓
|
|
28
|
+
* }))
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function useChart<TOption extends EChartsOption = EChartsOption>(
|
|
32
|
+
optionsFn: () => TOption,
|
|
33
|
+
config?: UseChartConfig,
|
|
34
|
+
): UseChartResult {
|
|
35
|
+
const instance = signal<import('echarts/core').ECharts | null>(null)
|
|
36
|
+
const loading = signal(true)
|
|
37
|
+
const error = signal<Error | null>(null)
|
|
38
|
+
const container = signal<HTMLElement | null>(null)
|
|
39
|
+
const renderer = config?.renderer ?? 'canvas'
|
|
40
|
+
|
|
41
|
+
let observer: ResizeObserver | null = null
|
|
42
|
+
let initialized = false
|
|
43
|
+
|
|
44
|
+
// Initialize chart when container is bound
|
|
45
|
+
effect(() => {
|
|
46
|
+
const el = container()
|
|
47
|
+
if (!el || initialized) return
|
|
48
|
+
|
|
49
|
+
initialized = true
|
|
50
|
+
|
|
51
|
+
let opts: EChartsOption
|
|
52
|
+
try {
|
|
53
|
+
opts = optionsFn()
|
|
54
|
+
} catch (err) {
|
|
55
|
+
error.set(err instanceof Error ? err : new Error(String(err)))
|
|
56
|
+
loading.set(false)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Load required ECharts modules, then create chart
|
|
61
|
+
ensureModules(opts as Record<string, unknown>, renderer)
|
|
62
|
+
.then((core) => {
|
|
63
|
+
// Guard: component may have unmounted during async load
|
|
64
|
+
if (!container.peek()) return
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const chart = core.init(el, config?.theme as any, {
|
|
68
|
+
renderer,
|
|
69
|
+
locale: config?.locale,
|
|
70
|
+
devicePixelRatio: config?.devicePixelRatio,
|
|
71
|
+
width: config?.width,
|
|
72
|
+
height: config?.height,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
chart.setOption(opts)
|
|
76
|
+
instance.set(chart)
|
|
77
|
+
loading.set(false)
|
|
78
|
+
error.set(null)
|
|
79
|
+
|
|
80
|
+
config?.onInit?.(chart)
|
|
81
|
+
|
|
82
|
+
// ResizeObserver for auto-resize
|
|
83
|
+
observer = new ResizeObserver(() => {
|
|
84
|
+
chart.resize()
|
|
85
|
+
})
|
|
86
|
+
observer.observe(el)
|
|
87
|
+
} catch (err) {
|
|
88
|
+
error.set(err instanceof Error ? err : new Error(String(err)))
|
|
89
|
+
loading.set(false)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
.catch((err) => {
|
|
93
|
+
error.set(err instanceof Error ? err : new Error(String(err)))
|
|
94
|
+
loading.set(false)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Reactive updates — re-run when signals in optionsFn change
|
|
99
|
+
effect(() => {
|
|
100
|
+
const chart = instance()
|
|
101
|
+
if (!chart) return
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const opts = optionsFn()
|
|
105
|
+
chart.setOption(opts, {
|
|
106
|
+
notMerge: config?.notMerge ?? false,
|
|
107
|
+
lazyUpdate: config?.lazyUpdate ?? true,
|
|
108
|
+
})
|
|
109
|
+
error.set(null)
|
|
110
|
+
} catch (err) {
|
|
111
|
+
error.set(err instanceof Error ? err : new Error(String(err)))
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// Cleanup on unmount
|
|
116
|
+
onUnmount(() => {
|
|
117
|
+
observer?.disconnect()
|
|
118
|
+
observer = null
|
|
119
|
+
|
|
120
|
+
const chart = instance.peek()
|
|
121
|
+
if (chart) {
|
|
122
|
+
chart.dispose()
|
|
123
|
+
instance.set(null)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
initialized = false
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
ref: (el: Element | null) => container.set(el as HTMLElement | null),
|
|
131
|
+
instance,
|
|
132
|
+
loading,
|
|
133
|
+
error,
|
|
134
|
+
resize: () => instance.peek()?.resize(),
|
|
135
|
+
}
|
|
136
|
+
}
|