@agions/taroviz 1.2.0 → 1.2.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/README.md +42 -36
- package/dist/cjs/index.js +1 -0
- package/dist/{index.esm.js → esm/index.js} +73473 -59179
- package/package.json +67 -8
- package/src/adapters/__tests__/index.test.ts +2 -2
- package/src/adapters/h5/index.ts +1 -1
- package/src/adapters/index.ts +99 -167
- package/src/charts/bar/index.tsx +2 -11
- package/src/charts/common/BaseChartWrapper.tsx +2 -2
- package/src/charts/funnel/index.tsx +2 -17
- package/src/charts/gauge/index.tsx +2 -17
- package/src/charts/heatmap/index.tsx +2 -17
- package/src/charts/line/index.tsx +2 -11
- package/src/charts/pie/index.tsx +3 -6
- package/src/charts/radar/index.tsx +2 -17
- package/src/charts/scatter/index.tsx +2 -17
- package/src/charts/types.ts +503 -30
- package/src/charts/utils.ts +1 -1
- package/src/core/__tests__/platform.test.ts +1 -1
- package/src/core/animation/AnimationManager.ts +2 -2
- package/src/core/components/BaseChart.tsx +12 -18
- package/src/core/components/ErrorBoundary.tsx +458 -0
- package/src/core/echarts.ts +58 -0
- package/src/core/index.ts +4 -1
- package/src/core/utils/__tests__/common.test.ts +1 -1
- package/src/core/utils/chartInstances.ts +2 -2
- package/src/core/utils/codeGenerator/CodeGenerator.ts +2 -2
- package/src/core/utils/codeGenerator/types.ts +0 -2
- package/src/core/utils/configGenerator/ConfigGenerator.ts +12 -12
- package/src/core/utils/debug/DebugPanel.tsx +1 -1
- package/src/core/utils/debug/debugger.ts +1 -1
- package/src/core/utils/index.ts +1 -1
- package/src/core/utils/performance/PerformanceAnalyzer.ts +5 -5
- package/src/editor/ThemeEditor.tsx +9 -9
- package/src/hooks/index.ts +441 -61
- package/src/main.tsx +1 -1
- package/src/themes/index.ts +651 -256
package/src/hooks/index.ts
CHANGED
|
@@ -1,119 +1,235 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TaroViz React Hooks
|
|
3
|
-
* 提供与图表相关的React Hooks
|
|
2
|
+
* TaroViz React Hooks - 增强版
|
|
3
|
+
* 提供与图表相关的 React Hooks
|
|
4
4
|
*/
|
|
5
|
-
import { useState, useEffect, useMemo } from 'react';
|
|
6
|
-
|
|
5
|
+
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
|
7
6
|
import { getAdapter } from '../adapters';
|
|
8
|
-
import {
|
|
7
|
+
import type { EChartsOption } from 'echarts';
|
|
9
8
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// 类型定义
|
|
11
|
+
// ============================================================================
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
interface ChartInstance {
|
|
17
|
-
setOption: (option:
|
|
18
|
-
|
|
13
|
+
/** 图表实例类型 */
|
|
14
|
+
export interface ChartInstance {
|
|
15
|
+
setOption: (option: EChartsOption, notMerge?: boolean, lazyUpdate?: boolean) => void;
|
|
16
|
+
getOption: () => EChartsOption;
|
|
17
|
+
resize: (option?: { width?: number | string; height?: number | string }) => void;
|
|
19
18
|
on: (event: string, handler: EventHandler) => void;
|
|
20
19
|
off: (event: string, handler?: EventHandler) => void;
|
|
21
|
-
showLoading: (opts?:
|
|
20
|
+
showLoading: (opts?: LoadingOptions) => void;
|
|
22
21
|
hideLoading: () => void;
|
|
23
22
|
dispose: () => void;
|
|
23
|
+
isDisposed: () => boolean;
|
|
24
|
+
getWidth: () => number;
|
|
25
|
+
getHeight: () => number;
|
|
26
|
+
getDom: () => HTMLElement;
|
|
27
|
+
getDataURL?: (options?: {
|
|
28
|
+
type?: string;
|
|
29
|
+
pixelRatio?: number;
|
|
30
|
+
backgroundColor?: string;
|
|
31
|
+
}) => string;
|
|
32
|
+
getSvgData?: () => string;
|
|
33
|
+
getCompressedDataURL?: (options?: { seriesIndex?: number; dimension?: number }) => string;
|
|
34
|
+
clear?: () => void;
|
|
35
|
+
dispatchAction?: (action: { type: string; [key: string]: unknown }) => void;
|
|
24
36
|
[key: string]: any;
|
|
25
37
|
}
|
|
26
38
|
|
|
39
|
+
/** 事件处理器 */
|
|
40
|
+
export type EventHandler = (params?: unknown) => void;
|
|
41
|
+
|
|
42
|
+
/** 加载选项 */
|
|
43
|
+
export interface LoadingOptions {
|
|
44
|
+
text?: string;
|
|
45
|
+
color?: string;
|
|
46
|
+
textColor?: string;
|
|
47
|
+
maskColor?: string;
|
|
48
|
+
zlevel?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 图表配置 */
|
|
52
|
+
export interface ChartConfig {
|
|
53
|
+
width?: number | string;
|
|
54
|
+
height?: number | string;
|
|
55
|
+
renderer?: 'canvas' | 'svg';
|
|
56
|
+
theme?: string | Record<string, unknown>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 数据转换器 */
|
|
60
|
+
export type DataTransformer<T = unknown> = (data: T) => EChartsOption;
|
|
61
|
+
|
|
62
|
+
/** 响应式断点 */
|
|
63
|
+
export type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
64
|
+
|
|
65
|
+
/** 断点配置 */
|
|
66
|
+
export interface BreakpointConfig {
|
|
67
|
+
width: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 主题切换回调 */
|
|
71
|
+
export type ThemeChangeCallback = (theme: string | Record<string, unknown>) => void;
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Hooks
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
27
77
|
/**
|
|
28
|
-
* 使用图表Hook
|
|
78
|
+
* 使用图表 Hook
|
|
29
79
|
* @param chartRef 图表容器的引用
|
|
30
|
-
* @
|
|
80
|
+
* @param config 图表配置
|
|
81
|
+
* @returns [图表实例, 设置实例函数, 是否已初始化]
|
|
31
82
|
*/
|
|
32
|
-
export function useChart(
|
|
83
|
+
export function useChart(
|
|
84
|
+
chartRef: React.RefObject<HTMLElement>,
|
|
85
|
+
config?: ChartConfig
|
|
86
|
+
): [ChartInstance | null, React.Dispatch<React.SetStateAction<ChartInstance | null>>, boolean] {
|
|
33
87
|
const [instance, setInstance] = useState<ChartInstance | null>(null);
|
|
88
|
+
const [initialized, setInitialized] = useState(false);
|
|
89
|
+
const configRef = useRef(config);
|
|
90
|
+
configRef.current = config;
|
|
34
91
|
|
|
35
92
|
useEffect(() => {
|
|
36
|
-
if (chartRef.current
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// 注意:Adapter接口可能没有直接定义setComponent方法
|
|
41
|
-
// 但多数适配器实现中都有这个方法,我们可以通过类型断言使用
|
|
42
|
-
if (typeof (adapter as any).setComponent === 'function') {
|
|
43
|
-
(adapter as any).setComponent(chartRef.current);
|
|
44
|
-
}
|
|
93
|
+
if (!chartRef.current || instance) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
45
96
|
|
|
46
|
-
|
|
97
|
+
try {
|
|
98
|
+
const adapter = getAdapter(configRef.current || {});
|
|
47
99
|
const chartInstance = adapter as unknown as ChartInstance;
|
|
48
100
|
setInstance(chartInstance);
|
|
101
|
+
setInitialized(true);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Failed to initialize chart:', error);
|
|
49
104
|
}
|
|
50
105
|
|
|
51
106
|
return () => {
|
|
52
107
|
if (instance) {
|
|
53
108
|
try {
|
|
54
|
-
instance
|
|
109
|
+
const inst = instance as any;
|
|
110
|
+
if (!inst.isDisposed?.()) {
|
|
111
|
+
inst.dispose();
|
|
112
|
+
}
|
|
55
113
|
} catch (e) {
|
|
56
114
|
console.warn('Failed to dispose chart instance:', e);
|
|
57
115
|
}
|
|
116
|
+
setInstance(null);
|
|
117
|
+
setInitialized(false);
|
|
58
118
|
}
|
|
59
119
|
};
|
|
60
|
-
}, [chartRef
|
|
120
|
+
}, [chartRef]);
|
|
61
121
|
|
|
62
|
-
return [instance, setInstance]
|
|
122
|
+
return [instance, setInstance, initialized];
|
|
63
123
|
}
|
|
64
124
|
|
|
65
125
|
/**
|
|
66
|
-
* 设置图表选项Hook
|
|
126
|
+
* 设置图表选项 Hook
|
|
67
127
|
* @param instance 图表实例
|
|
68
128
|
* @param option 图表选项
|
|
69
|
-
* @param
|
|
129
|
+
* @param options 配置选项
|
|
70
130
|
*/
|
|
71
|
-
export function useOption(
|
|
131
|
+
export function useOption(
|
|
132
|
+
instance: ChartInstance | null,
|
|
133
|
+
option: EChartsOption | null,
|
|
134
|
+
options?: {
|
|
135
|
+
/** 是否不合并 */
|
|
136
|
+
notMerge?: boolean;
|
|
137
|
+
/** 是否延迟更新 */
|
|
138
|
+
lazyUpdate?: boolean;
|
|
139
|
+
/** 是否在数据变化时替换 */
|
|
140
|
+
replaceMerge?: string[];
|
|
141
|
+
/** 依赖数组 */
|
|
142
|
+
deps?: unknown[];
|
|
143
|
+
}
|
|
144
|
+
) {
|
|
145
|
+
const { notMerge = false, lazyUpdate = false, replaceMerge, deps = [] } = options || {};
|
|
146
|
+
|
|
72
147
|
useEffect(() => {
|
|
73
148
|
if (instance && option) {
|
|
74
149
|
try {
|
|
75
|
-
instance.setOption(option);
|
|
150
|
+
instance.setOption(option, notMerge, lazyUpdate);
|
|
76
151
|
} catch (e) {
|
|
77
152
|
console.warn('Failed to set chart option:', e);
|
|
78
153
|
}
|
|
79
154
|
}
|
|
80
|
-
}, [instance, option, ...deps]);
|
|
155
|
+
}, [instance, option, notMerge, lazyUpdate, replaceMerge, ...deps]);
|
|
81
156
|
}
|
|
82
157
|
|
|
83
158
|
/**
|
|
84
|
-
* 图表自适应Hook
|
|
159
|
+
* 图表自适应 Hook
|
|
85
160
|
* @param instance 图表实例
|
|
161
|
+
* @param options 配置选项
|
|
86
162
|
*/
|
|
87
|
-
export function useResize(
|
|
163
|
+
export function useResize(
|
|
164
|
+
instance: ChartInstance | null,
|
|
165
|
+
options?: {
|
|
166
|
+
/** 延迟时间 (ms) */
|
|
167
|
+
delay?: number;
|
|
168
|
+
/** 最小宽度 */
|
|
169
|
+
minWidth?: number;
|
|
170
|
+
/** 最小高度 */
|
|
171
|
+
minHeight?: number;
|
|
172
|
+
/** 是否启用 */
|
|
173
|
+
enabled?: boolean;
|
|
174
|
+
}
|
|
175
|
+
) {
|
|
176
|
+
const { delay = 300, minWidth, minHeight, enabled = true } = options || {};
|
|
177
|
+
const timeoutRef = useRef<NodeJS.Timeout>();
|
|
178
|
+
|
|
88
179
|
useEffect(() => {
|
|
89
|
-
if (!instance) {
|
|
180
|
+
if (!instance || !enabled) {
|
|
90
181
|
return;
|
|
91
182
|
}
|
|
92
183
|
|
|
93
184
|
const handleResize = () => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
} catch (e) {
|
|
97
|
-
console.warn('Failed to resize chart:', e);
|
|
185
|
+
if (timeoutRef.current) {
|
|
186
|
+
clearTimeout(timeoutRef.current);
|
|
98
187
|
}
|
|
188
|
+
|
|
189
|
+
timeoutRef.current = setTimeout(() => {
|
|
190
|
+
try {
|
|
191
|
+
const dom = instance.getDom?.();
|
|
192
|
+
if (dom) {
|
|
193
|
+
const { clientWidth, clientHeight } = dom;
|
|
194
|
+
if (minWidth && clientWidth < minWidth) return;
|
|
195
|
+
if (minHeight && clientHeight < minHeight) return;
|
|
196
|
+
}
|
|
197
|
+
instance.resize?.();
|
|
198
|
+
} catch (e) {
|
|
199
|
+
console.warn('Failed to resize chart:', e);
|
|
200
|
+
}
|
|
201
|
+
}, delay);
|
|
99
202
|
};
|
|
100
203
|
|
|
101
204
|
window.addEventListener('resize', handleResize);
|
|
102
205
|
|
|
206
|
+
// 创建一个 ResizeObserver 来监听容器大小变化
|
|
207
|
+
const dom = instance.getDom?.();
|
|
208
|
+
if (dom && typeof ResizeObserver !== 'undefined') {
|
|
209
|
+
const observer = new ResizeObserver(handleResize);
|
|
210
|
+
observer.observe(dom);
|
|
211
|
+
return () => {
|
|
212
|
+
observer.disconnect();
|
|
213
|
+
window.removeEventListener('resize', handleResize);
|
|
214
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
103
218
|
return () => {
|
|
104
219
|
window.removeEventListener('resize', handleResize);
|
|
220
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
105
221
|
};
|
|
106
|
-
}, [instance]);
|
|
222
|
+
}, [instance, delay, minWidth, minHeight, enabled]);
|
|
107
223
|
}
|
|
108
224
|
|
|
109
225
|
/**
|
|
110
|
-
* 图表事件Hook
|
|
226
|
+
* 图表事件 Hook
|
|
111
227
|
* @param instance 图表实例
|
|
112
228
|
* @param events 事件对象
|
|
113
229
|
*/
|
|
114
230
|
export function useEvents(instance: ChartInstance | null, events: Record<string, EventHandler>) {
|
|
115
231
|
useEffect(() => {
|
|
116
|
-
if (!instance) {
|
|
232
|
+
if (!instance || !events) {
|
|
117
233
|
return;
|
|
118
234
|
}
|
|
119
235
|
|
|
@@ -142,11 +258,16 @@ export function useEvents(instance: ChartInstance | null, events: Record<string,
|
|
|
142
258
|
}
|
|
143
259
|
|
|
144
260
|
/**
|
|
145
|
-
* 图表加载状态Hook
|
|
261
|
+
* 图表加载状态 Hook
|
|
146
262
|
* @param instance 图表实例
|
|
147
263
|
* @param loading 是否加载中
|
|
264
|
+
* @param options 加载选项
|
|
148
265
|
*/
|
|
149
|
-
export function useLoading(
|
|
266
|
+
export function useLoading(
|
|
267
|
+
instance: ChartInstance | null,
|
|
268
|
+
loading: boolean,
|
|
269
|
+
options?: LoadingOptions
|
|
270
|
+
) {
|
|
150
271
|
useEffect(() => {
|
|
151
272
|
if (!instance) {
|
|
152
273
|
return;
|
|
@@ -154,28 +275,33 @@ export function useLoading(instance: ChartInstance | null, loading: boolean) {
|
|
|
154
275
|
|
|
155
276
|
try {
|
|
156
277
|
if (loading) {
|
|
157
|
-
instance.showLoading();
|
|
278
|
+
instance.showLoading(options);
|
|
158
279
|
} else {
|
|
159
280
|
instance.hideLoading();
|
|
160
281
|
}
|
|
161
282
|
} catch (e) {
|
|
162
283
|
console.warn('Failed to set chart loading state:', e);
|
|
163
284
|
}
|
|
164
|
-
}, [instance, loading]);
|
|
285
|
+
}, [instance, loading, options]);
|
|
165
286
|
}
|
|
166
287
|
|
|
167
288
|
/**
|
|
168
289
|
* 使用图表主题
|
|
169
290
|
* @param theme 主题名称或配置
|
|
170
|
-
* @param darkMode
|
|
291
|
+
* @param darkMode 是否为暗色模式
|
|
171
292
|
* @returns 处理后的主题
|
|
172
293
|
*/
|
|
173
|
-
export function useChartTheme(theme: string | Record<string,
|
|
294
|
+
export function useChartTheme(theme: string | Record<string, unknown>, darkMode = false) {
|
|
174
295
|
return useMemo(() => {
|
|
175
296
|
if (typeof theme === 'string') {
|
|
176
|
-
|
|
297
|
+
// 如果是字符串,尝试获取内置主题
|
|
298
|
+
try {
|
|
299
|
+
const builtinTheme = getTheme(theme);
|
|
300
|
+
return builtinTheme || (darkMode ? 'dark' : theme);
|
|
301
|
+
} catch {
|
|
302
|
+
return darkMode ? 'dark' : theme;
|
|
303
|
+
}
|
|
177
304
|
}
|
|
178
|
-
|
|
179
305
|
return theme;
|
|
180
306
|
}, [theme, darkMode]);
|
|
181
307
|
}
|
|
@@ -186,20 +312,269 @@ export function useChartTheme(theme: string | Record<string, any>, darkMode = fa
|
|
|
186
312
|
* @param transformer 数据转换函数
|
|
187
313
|
* @returns 转换后的图表选项
|
|
188
314
|
*/
|
|
189
|
-
export function useChartData<T =
|
|
315
|
+
export function useChartData<T = unknown>(data: T | null, transformer: DataTransformer<T>) {
|
|
190
316
|
return useMemo(() => {
|
|
191
|
-
if (!data
|
|
192
|
-
return {
|
|
317
|
+
if (!data) {
|
|
318
|
+
return {};
|
|
193
319
|
}
|
|
194
|
-
|
|
195
320
|
return transformer(data);
|
|
196
321
|
}, [data, transformer]);
|
|
197
322
|
}
|
|
198
323
|
|
|
199
|
-
|
|
200
|
-
|
|
324
|
+
/**
|
|
325
|
+
* 使用响应式图表配置
|
|
326
|
+
* @param config 响应式配置
|
|
327
|
+
* @returns 当前断点和配置
|
|
328
|
+
*/
|
|
329
|
+
export function useResponsive(config?: {
|
|
330
|
+
/** 断点配置 */
|
|
331
|
+
breakpoints?: Record<Breakpoint, number>;
|
|
332
|
+
/** 默认断点 */
|
|
333
|
+
defaultBreakpoint?: Breakpoint;
|
|
334
|
+
}) {
|
|
335
|
+
const { breakpoints = { xs: 0, sm: 576, md: 768, lg: 992, xl: 1200 }, defaultBreakpoint = 'md' } =
|
|
336
|
+
config || {};
|
|
337
|
+
|
|
338
|
+
const [breakpoint, setBreakpoint] = useState<Breakpoint>(defaultBreakpoint);
|
|
339
|
+
const [windowSize, setWindowSize] = useState({ width: 0, height: 0 });
|
|
340
|
+
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
const handleResize = () => {
|
|
343
|
+
const width = window.innerWidth;
|
|
344
|
+
setWindowSize({ width, height: window.innerHeight });
|
|
345
|
+
|
|
346
|
+
// 确定当前断点
|
|
347
|
+
let current: Breakpoint = 'xs';
|
|
348
|
+
if (width >= breakpoints.xl) current = 'xl';
|
|
349
|
+
else if (width >= breakpoints.lg) current = 'lg';
|
|
350
|
+
else if (width >= breakpoints.md) current = 'md';
|
|
351
|
+
else if (width >= breakpoints.sm) current = 'sm';
|
|
352
|
+
|
|
353
|
+
setBreakpoint(current);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
handleResize();
|
|
357
|
+
window.addEventListener('resize', handleResize);
|
|
358
|
+
|
|
359
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
360
|
+
}, [breakpoints]);
|
|
361
|
+
|
|
362
|
+
return { breakpoint, windowSize };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 使用主题切换
|
|
367
|
+
* @param initialTheme 初始主题
|
|
368
|
+
* @returns [当前主题, 切换主题函数]
|
|
369
|
+
*/
|
|
370
|
+
export function useThemeSwitcher(initialTheme = 'default') {
|
|
371
|
+
const [theme, setTheme] = useState<string | Record<string, unknown>>(initialTheme);
|
|
372
|
+
const [isDark, setIsDark] = useState(false);
|
|
373
|
+
|
|
374
|
+
const switchTheme = useCallback((newTheme: string | Record<string, unknown>) => {
|
|
375
|
+
setTheme(newTheme);
|
|
376
|
+
if (typeof newTheme === 'string') {
|
|
377
|
+
setIsDark(newTheme === 'dark' || newTheme.includes('dark'));
|
|
378
|
+
}
|
|
379
|
+
}, []);
|
|
380
|
+
|
|
381
|
+
const toggleDark = useCallback(() => {
|
|
382
|
+
setIsDark((prev) => !prev);
|
|
383
|
+
setTheme((prev) => (prev === 'dark' ? 'default' : 'dark'));
|
|
384
|
+
}, []);
|
|
385
|
+
|
|
386
|
+
return { theme, isDark, switchTheme, toggleDark, setTheme };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* 使用数据轮询
|
|
391
|
+
* @param fetchFn 数据获取函数
|
|
392
|
+
* @param options 配置选项
|
|
393
|
+
* @returns [数据, 加载状态, 错误, 刷新函数]
|
|
394
|
+
*/
|
|
395
|
+
export function useDataPolling<T>(
|
|
396
|
+
fetchFn: () => Promise<T>,
|
|
397
|
+
options?: {
|
|
398
|
+
/** 轮询间隔 (ms) */
|
|
399
|
+
interval?: number;
|
|
400
|
+
/** 是否自动开始 */
|
|
401
|
+
autoStart?: boolean;
|
|
402
|
+
/** 错误重试次数 */
|
|
403
|
+
retryCount?: number;
|
|
404
|
+
/** 重试延迟 (ms) */
|
|
405
|
+
retryDelay?: number;
|
|
406
|
+
}
|
|
407
|
+
) {
|
|
408
|
+
const { interval = 5000, autoStart = false, retryCount = 3, retryDelay = 1000 } = options || {};
|
|
409
|
+
|
|
410
|
+
const [data, setData] = useState<T | null>(null);
|
|
411
|
+
const [loading, setLoading] = useState(autoStart);
|
|
412
|
+
const [error, setError] = useState<Error | null>(null);
|
|
413
|
+
const [refreshIndex, setRefreshIndex] = useState(0);
|
|
414
|
+
|
|
415
|
+
const fetchData = useCallback(async () => {
|
|
416
|
+
let retries = retryCount;
|
|
417
|
+
setLoading(true);
|
|
418
|
+
setError(null);
|
|
419
|
+
|
|
420
|
+
while (retries >= 0) {
|
|
421
|
+
try {
|
|
422
|
+
const result = await fetchFn();
|
|
423
|
+
setData(result);
|
|
424
|
+
setLoading(false);
|
|
425
|
+
return;
|
|
426
|
+
} catch (e) {
|
|
427
|
+
retries--;
|
|
428
|
+
if (retries < 0) {
|
|
429
|
+
setError(e as Error);
|
|
430
|
+
setLoading(false);
|
|
431
|
+
} else {
|
|
432
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}, [fetchFn, retryCount, retryDelay]);
|
|
437
|
+
|
|
438
|
+
useEffect(() => {
|
|
439
|
+
if (autoStart) {
|
|
440
|
+
fetchData();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (interval > 0) {
|
|
444
|
+
const timer = setInterval(fetchData, interval);
|
|
445
|
+
return () => clearInterval(timer);
|
|
446
|
+
}
|
|
447
|
+
}, [interval, autoStart, fetchData, refreshIndex]);
|
|
448
|
+
|
|
449
|
+
const refresh = useCallback(() => {
|
|
450
|
+
setRefreshIndex((prev) => prev + 1);
|
|
451
|
+
fetchData();
|
|
452
|
+
}, [fetchData]);
|
|
453
|
+
|
|
454
|
+
return { data, loading, error, refresh };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* 使用图表全屏
|
|
459
|
+
* @param chartRef 图表容器引用
|
|
460
|
+
* @returns [是否全屏, 进入/退出全屏函数]
|
|
461
|
+
*/
|
|
462
|
+
export function useFullscreen(chartRef: React.RefObject<HTMLElement>) {
|
|
463
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
464
|
+
|
|
465
|
+
const toggle = useCallback(() => {
|
|
466
|
+
if (!chartRef.current) return;
|
|
467
|
+
|
|
468
|
+
if (!isFullscreen) {
|
|
469
|
+
if (chartRef.current.requestFullscreen) {
|
|
470
|
+
chartRef.current.requestFullscreen();
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
if (document.exitFullscreen) {
|
|
474
|
+
document.exitFullscreen();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}, [chartRef, isFullscreen]);
|
|
478
|
+
|
|
479
|
+
useEffect(() => {
|
|
480
|
+
const handleChange = () => {
|
|
481
|
+
setIsFullscreen(!!document.fullscreenElement);
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
document.addEventListener('fullscreenchange', handleChange);
|
|
485
|
+
return () => document.removeEventListener('fullscreenchange', handleChange);
|
|
486
|
+
}, []);
|
|
487
|
+
|
|
488
|
+
return { isFullscreen, toggle };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* 使用图表导出
|
|
493
|
+
* @param instance 图表实例
|
|
494
|
+
* @returns 导出函数
|
|
495
|
+
*/
|
|
496
|
+
export function useExport(instance: ChartInstance | null) {
|
|
497
|
+
const inst = instance as any;
|
|
498
|
+
const exportImage = useCallback(
|
|
499
|
+
(options?: { type?: 'png' | 'jpeg'; pixelRatio?: number; backgroundColor?: string }) => {
|
|
500
|
+
if (!inst) return null;
|
|
501
|
+
const { type = 'png', pixelRatio = 2, backgroundColor } = options || {};
|
|
502
|
+
return inst.getDataURL?.({ type, pixelRatio, backgroundColor });
|
|
503
|
+
},
|
|
504
|
+
[inst]
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
const exportSVG = useCallback(() => {
|
|
508
|
+
if (!inst) return null;
|
|
509
|
+
return inst.getSvgData?.();
|
|
510
|
+
}, [inst]);
|
|
511
|
+
|
|
512
|
+
const exportCSV = useCallback(
|
|
513
|
+
(options?: { seriesIndex?: number; dimension?: number }) => {
|
|
514
|
+
if (!inst) return null;
|
|
515
|
+
return inst.getCompressedDataURL?.(options);
|
|
516
|
+
},
|
|
517
|
+
[inst]
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
return { exportImage, exportSVG, exportCSV };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* 使用图表工具
|
|
525
|
+
* @param instance 图表实例
|
|
526
|
+
* @returns 工具函数
|
|
527
|
+
*/
|
|
528
|
+
export function useChartTools(instance: ChartInstance | null) {
|
|
529
|
+
const inst = instance as any;
|
|
530
|
+
const getInstance = useCallback(() => instance, [instance]);
|
|
531
|
+
|
|
532
|
+
const clear = useCallback(() => {
|
|
533
|
+
inst?.clear?.();
|
|
534
|
+
}, [inst]);
|
|
535
|
+
|
|
536
|
+
const repaint = useCallback(() => {
|
|
537
|
+
inst?.resize?.();
|
|
538
|
+
}, [inst]);
|
|
539
|
+
|
|
540
|
+
const dispatchAction = useCallback(
|
|
541
|
+
(action: { type: string; [key: string]: unknown }) => {
|
|
542
|
+
inst?.dispatchAction?.(action);
|
|
543
|
+
},
|
|
544
|
+
[inst]
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
const showTip = useCallback(
|
|
548
|
+
(seriesIndex?: number, dataIndex?: number) => {
|
|
549
|
+
inst?.dispatchAction?.({ type: 'showTip', seriesIndex, dataIndex });
|
|
550
|
+
},
|
|
551
|
+
[inst]
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
const hideTip = useCallback(() => {
|
|
555
|
+
inst?.dispatchAction?.({ type: 'hideTip' });
|
|
556
|
+
}, [inst]);
|
|
557
|
+
|
|
558
|
+
const zoom = useCallback(
|
|
559
|
+
(start?: number, end?: number) => {
|
|
560
|
+
inst?.dispatchAction?.({
|
|
561
|
+
type: 'dataZoom',
|
|
562
|
+
start: start ?? 0,
|
|
563
|
+
end: end ?? 100,
|
|
564
|
+
});
|
|
565
|
+
},
|
|
566
|
+
[inst]
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
return { getInstance, clear, repaint, dispatchAction, showTip, hideTip, zoom };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// ============================================================================
|
|
573
|
+
// 导出
|
|
574
|
+
// ============================================================================
|
|
575
|
+
|
|
576
|
+
export const version = '1.2.0';
|
|
201
577
|
|
|
202
|
-
// 导出所有hooks到默认导出对象
|
|
203
578
|
const hooks = {
|
|
204
579
|
useChart,
|
|
205
580
|
useOption,
|
|
@@ -208,7 +583,12 @@ const hooks = {
|
|
|
208
583
|
useLoading,
|
|
209
584
|
useChartTheme,
|
|
210
585
|
useChartData,
|
|
586
|
+
useResponsive,
|
|
587
|
+
useThemeSwitcher,
|
|
588
|
+
useDataPolling,
|
|
589
|
+
useFullscreen,
|
|
590
|
+
useExport,
|
|
591
|
+
useChartTools,
|
|
211
592
|
};
|
|
212
593
|
|
|
213
|
-
// 为了同时支持具名导入和默认导入
|
|
214
594
|
export default hooks;
|
package/src/main.tsx
CHANGED
|
@@ -163,7 +163,7 @@ const TestApp = () => {
|
|
|
163
163
|
<select
|
|
164
164
|
id="theme"
|
|
165
165
|
value={darkMode ? 'dark' : 'light'}
|
|
166
|
-
onChange={e => setDarkMode(e.target.value === 'dark')}
|
|
166
|
+
onChange={(e) => setDarkMode(e.target.value === 'dark')}
|
|
167
167
|
>
|
|
168
168
|
<option value="light">Light</option>
|
|
169
169
|
<option value="dark">Dark</option>
|