@agions/taroviz 1.7.0 → 1.10.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/README.md +7 -3
- package/dist/cjs/index.js +1 -1
- package/dist/esm/index.js +960 -136
- package/package.json +1 -1
- package/src/adapters/__tests__/index.test.ts +4 -2
- package/src/adapters/h5/index.ts +16 -0
- package/src/adapters/types.ts +28 -120
- package/src/charts/boxplot/types.ts +5 -3
- package/src/charts/common/BaseChartWrapper.tsx +193 -32
- package/src/charts/liquid/index.tsx +6 -5
- package/src/charts/liquid/types.ts +4 -4
- package/src/charts/parallel/types.ts +6 -3
- package/src/charts/tree/types.ts +4 -4
- package/src/charts/types.ts +1 -1
- package/src/core/animation/AnimationManager.ts +69 -42
- package/src/core/components/Annotation.tsx +12 -10
- package/src/core/components/BaseChart.tsx +75 -12
- package/src/core/components/ErrorBoundary.tsx +30 -17
- package/src/core/components/LazyChart.tsx +14 -9
- package/src/core/themes/ThemeManager.ts +33 -0
- package/src/core/types/common.ts +21 -110
- package/src/core/types/index.ts +4 -135
- package/src/core/types/platform.ts +38 -230
- package/src/core/utils/chartUtils.ts +8 -3
- package/src/core/utils/export/ExportUtils.ts +10 -1
- package/src/core/utils/performance/PerformanceAnalyzer.ts +21 -1
- package/src/core/utils/performance/types.ts +5 -0
- package/src/hooks/__tests__/index.test.tsx +7 -5
- package/src/hooks/index.ts +23 -1
- package/src/hooks/useAnimation.ts +427 -0
- package/src/hooks/useChartHistory.ts +273 -0
- package/src/hooks/useChartSelection.ts +350 -0
- package/src/hooks/usePerformance.ts +291 -0
- package/src/themes/__tests__/index.test.ts +7 -13
|
@@ -2,17 +2,20 @@
|
|
|
2
2
|
* 平行坐标图类型定义
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type { EChartsType, ECElementEvent } from 'echarts';
|
|
6
|
+
import type { LoadingOptions } from '../types';
|
|
7
|
+
|
|
5
8
|
export type ParallelChartProps = {
|
|
6
9
|
option?: ParallelOption;
|
|
7
10
|
width?: string | number;
|
|
8
11
|
height?: string | number;
|
|
9
12
|
className?: string;
|
|
10
13
|
style?: React.CSSProperties;
|
|
11
|
-
onEvents?: Record<string, (params:
|
|
14
|
+
onEvents?: Record<string, (params: ECElementEvent) => void>;
|
|
12
15
|
loading?: boolean;
|
|
13
|
-
loadingOption?:
|
|
16
|
+
loadingOption?: LoadingOptions;
|
|
14
17
|
theme?: string;
|
|
15
|
-
onChartReady?: (chart:
|
|
18
|
+
onChartReady?: (chart: EChartsType) => void;
|
|
16
19
|
opts?: {
|
|
17
20
|
devicePixelRatio?: number;
|
|
18
21
|
renderer?: 'canvas' | 'svg';
|
package/src/charts/tree/types.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 树图类型定义
|
|
3
3
|
* ECharts 内置 tree 类型
|
|
4
4
|
*/
|
|
5
|
-
import type { EChartsOption } from 'echarts';
|
|
5
|
+
import type { EChartsOption, EChartsType, ECElementEvent } from 'echarts';
|
|
6
6
|
|
|
7
7
|
// ============================================================================
|
|
8
8
|
// 树图数据节点
|
|
@@ -166,9 +166,9 @@ export interface TreeChartProps {
|
|
|
166
166
|
/** 加载配置 */
|
|
167
167
|
loadingOption?: Record<string, unknown>;
|
|
168
168
|
/** 图表初始化回调 */
|
|
169
|
-
onChartInit?: (chart:
|
|
169
|
+
onChartInit?: (chart: EChartsType) => void;
|
|
170
170
|
/** 图表就绪回调 */
|
|
171
|
-
onChartReady?: (chart:
|
|
171
|
+
onChartReady?: (chart: EChartsType) => void;
|
|
172
172
|
/** 事件回调 */
|
|
173
|
-
onEvents?: Record<string, (params:
|
|
173
|
+
onEvents?: Record<string, (params: ECElementEvent) => void>;
|
|
174
174
|
}
|
package/src/charts/types.ts
CHANGED
|
@@ -681,7 +681,7 @@ export interface SankeyChartProps extends BaseChartProps {
|
|
|
681
681
|
nodeWidth?: number;
|
|
682
682
|
|
|
683
683
|
/** 节点排序方式 */
|
|
684
|
-
nodeSort?: 'ascending' | 'descending' | 'none' | ((a:
|
|
684
|
+
nodeSort?: 'ascending' | 'descending' | 'none' | ((a: unknown, b: unknown) => number);
|
|
685
685
|
|
|
686
686
|
/** 边的曲度 */
|
|
687
687
|
linkCurveness?: number;
|
|
@@ -13,21 +13,33 @@ import {
|
|
|
13
13
|
} from './types';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Checks the OS/browser prefers-reduced-motion setting.
|
|
17
|
+
* Returns true if the user has requested reduced motion.
|
|
18
|
+
*/
|
|
19
|
+
function prefersReducedMotion(): boolean {
|
|
20
|
+
if (typeof window === 'undefined') return false;
|
|
21
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Professional animation presets following frontend-design-pro skill guidelines:
|
|
26
|
+
* - Easing: cubic-bezier(0.16, 1, 0.3, 1) ("cubicOut") for natural deceleration
|
|
27
|
+
* - Durations: 100-200ms micro, 300-500ms transitions, never >600ms
|
|
28
|
+
* - NO bounce/elastic — anti-patterns that feel廉价 (cheap)
|
|
17
29
|
*/
|
|
18
30
|
const DEFAULT_ANIMATION_PRESETS: AnimationPreset[] = [
|
|
19
31
|
{
|
|
20
32
|
name: 'default',
|
|
21
|
-
description: '默认动画配置',
|
|
33
|
+
description: '默认动画配置 — 专业级 (300-500ms, cubicOut)',
|
|
22
34
|
config: {
|
|
23
35
|
enabled: true,
|
|
24
|
-
duration:
|
|
36
|
+
duration: 400,
|
|
25
37
|
easing: 'cubicOut',
|
|
26
|
-
appearDuration:
|
|
38
|
+
appearDuration: 450,
|
|
27
39
|
appearEasing: 'cubicOut',
|
|
28
|
-
updateDuration:
|
|
40
|
+
updateDuration: 300,
|
|
29
41
|
updateEasing: 'cubicOut',
|
|
30
|
-
disappearDuration:
|
|
42
|
+
disappearDuration: 250,
|
|
31
43
|
disappearEasing: 'cubicIn',
|
|
32
44
|
threshold: 1000,
|
|
33
45
|
progressive: true,
|
|
@@ -36,17 +48,17 @@ const DEFAULT_ANIMATION_PRESETS: AnimationPreset[] = [
|
|
|
36
48
|
},
|
|
37
49
|
{
|
|
38
50
|
name: 'fast',
|
|
39
|
-
description: '快速动画配置',
|
|
51
|
+
description: '快速动画配置 — 微交互 (150-200ms)',
|
|
40
52
|
config: {
|
|
41
53
|
enabled: true,
|
|
42
|
-
duration:
|
|
43
|
-
easing: '
|
|
44
|
-
appearDuration:
|
|
45
|
-
appearEasing: '
|
|
46
|
-
updateDuration:
|
|
47
|
-
updateEasing: '
|
|
48
|
-
disappearDuration:
|
|
49
|
-
disappearEasing: '
|
|
54
|
+
duration: 150,
|
|
55
|
+
easing: 'cubicOut',
|
|
56
|
+
appearDuration: 200,
|
|
57
|
+
appearEasing: 'cubicOut',
|
|
58
|
+
updateDuration: 150,
|
|
59
|
+
updateEasing: 'cubicOut',
|
|
60
|
+
disappearDuration: 100,
|
|
61
|
+
disappearEasing: 'cubicIn',
|
|
50
62
|
threshold: 2000,
|
|
51
63
|
progressive: true,
|
|
52
64
|
progressiveStep: 1000,
|
|
@@ -54,55 +66,57 @@ const DEFAULT_ANIMATION_PRESETS: AnimationPreset[] = [
|
|
|
54
66
|
},
|
|
55
67
|
{
|
|
56
68
|
name: 'slow',
|
|
57
|
-
description: '慢速动画配置',
|
|
69
|
+
description: '慢速动画配置 — 页面过渡 (500-600ms, capped)',
|
|
58
70
|
config: {
|
|
59
71
|
enabled: true,
|
|
60
|
-
duration:
|
|
72
|
+
duration: 500,
|
|
61
73
|
easing: 'cubicInOut',
|
|
62
|
-
appearDuration:
|
|
74
|
+
appearDuration: 600,
|
|
63
75
|
appearEasing: 'cubicInOut',
|
|
64
|
-
updateDuration:
|
|
76
|
+
updateDuration: 400,
|
|
65
77
|
updateEasing: 'cubicInOut',
|
|
66
|
-
disappearDuration:
|
|
78
|
+
disappearDuration: 300,
|
|
67
79
|
disappearEasing: 'cubicInOut',
|
|
68
80
|
threshold: 500,
|
|
69
81
|
progressive: true,
|
|
70
82
|
progressiveStep: 250,
|
|
71
83
|
},
|
|
72
84
|
},
|
|
85
|
+
// DEPRECATED — bounce is an anti-pattern per frontend-design-pro skill
|
|
73
86
|
{
|
|
74
87
|
name: 'bounce',
|
|
75
|
-
description: '
|
|
88
|
+
description: '[已废弃] 弹跳动画 — 请使用 default 或 fast',
|
|
76
89
|
config: {
|
|
77
|
-
enabled:
|
|
78
|
-
duration:
|
|
79
|
-
easing: '
|
|
80
|
-
appearDuration:
|
|
81
|
-
appearEasing: '
|
|
82
|
-
updateDuration:
|
|
83
|
-
updateEasing: '
|
|
84
|
-
disappearDuration:
|
|
85
|
-
disappearEasing: '
|
|
90
|
+
enabled: false, // disabled by default — anti-pattern
|
|
91
|
+
duration: 0,
|
|
92
|
+
easing: 'cubicOut',
|
|
93
|
+
appearDuration: 0,
|
|
94
|
+
appearEasing: 'cubicOut',
|
|
95
|
+
updateDuration: 0,
|
|
96
|
+
updateEasing: 'cubicOut',
|
|
97
|
+
disappearDuration: 0,
|
|
98
|
+
disappearEasing: 'cubicIn',
|
|
86
99
|
threshold: 500,
|
|
87
|
-
progressive:
|
|
100
|
+
progressive: false,
|
|
88
101
|
progressiveStep: 250,
|
|
89
102
|
},
|
|
90
103
|
},
|
|
104
|
+
// DEPRECATED — elastic is an anti-pattern per frontend-design-pro skill
|
|
91
105
|
{
|
|
92
106
|
name: 'elastic',
|
|
93
|
-
description: '
|
|
107
|
+
description: '[已废弃] 弹性动画 — 请使用 default 或 fast',
|
|
94
108
|
config: {
|
|
95
|
-
enabled:
|
|
96
|
-
duration:
|
|
97
|
-
easing: '
|
|
98
|
-
appearDuration:
|
|
99
|
-
appearEasing: '
|
|
100
|
-
updateDuration:
|
|
101
|
-
updateEasing: '
|
|
102
|
-
disappearDuration:
|
|
103
|
-
disappearEasing: '
|
|
109
|
+
enabled: false, // disabled by default — anti-pattern
|
|
110
|
+
duration: 0,
|
|
111
|
+
easing: 'cubicOut',
|
|
112
|
+
appearDuration: 0,
|
|
113
|
+
appearEasing: 'cubicOut',
|
|
114
|
+
updateDuration: 0,
|
|
115
|
+
updateEasing: 'cubicOut',
|
|
116
|
+
disappearDuration: 0,
|
|
117
|
+
disappearEasing: 'cubicIn',
|
|
104
118
|
threshold: 500,
|
|
105
|
-
progressive:
|
|
119
|
+
progressive: false,
|
|
106
120
|
progressiveStep: 250,
|
|
107
121
|
},
|
|
108
122
|
},
|
|
@@ -213,6 +227,7 @@ export class AnimationManager {
|
|
|
213
227
|
|
|
214
228
|
/**
|
|
215
229
|
* 根据数据量和动画类型获取优化后的动画配置
|
|
230
|
+
* 尊重 prefers-reduced-motion 无障碍设置
|
|
216
231
|
*/
|
|
217
232
|
public getOptimizedConfig(
|
|
218
233
|
config: Partial<AnimationConfig> = {},
|
|
@@ -224,6 +239,18 @@ export class AnimationManager {
|
|
|
224
239
|
...config,
|
|
225
240
|
};
|
|
226
241
|
|
|
242
|
+
// Respect OS/browser prefers-reduced-motion setting (WCAG)
|
|
243
|
+
if (prefersReducedMotion()) {
|
|
244
|
+
return {
|
|
245
|
+
...mergedConfig,
|
|
246
|
+
enabled: false,
|
|
247
|
+
duration: 0,
|
|
248
|
+
appearDuration: 0,
|
|
249
|
+
updateDuration: 0,
|
|
250
|
+
disappearDuration: 0,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
227
254
|
// 根据数据量优化动画
|
|
228
255
|
if (mergedConfig.threshold && dataLength > mergedConfig.threshold) {
|
|
229
256
|
mergedConfig.enabled = false;
|
|
@@ -32,7 +32,7 @@ export interface MarkAreaStyle {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* 标记线数据点
|
|
35
|
+
* 标记线数据点 — 支持常规坐标和 ECharts 统计类型
|
|
36
36
|
*/
|
|
37
37
|
export interface MarkLineDataPoint {
|
|
38
38
|
/** X轴值 */
|
|
@@ -41,6 +41,8 @@ export interface MarkLineDataPoint {
|
|
|
41
41
|
yAxis?: string | number;
|
|
42
42
|
/** 数据点名称 */
|
|
43
43
|
name?: string;
|
|
44
|
+
/** ECharts 统计/特殊类型('average' | 'max' | 'min' | 'median') */
|
|
45
|
+
type?: string;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -97,7 +99,7 @@ export interface ScatterAnnotationConfig {
|
|
|
97
99
|
data: Array<{
|
|
98
100
|
coord: [number | string, number];
|
|
99
101
|
value?: number;
|
|
100
|
-
name
|
|
102
|
+
name: string;
|
|
101
103
|
}>;
|
|
102
104
|
/** 符号类型 */
|
|
103
105
|
symbol?: string;
|
|
@@ -109,7 +111,7 @@ export interface ScatterAnnotationConfig {
|
|
|
109
111
|
label?: {
|
|
110
112
|
show?: boolean;
|
|
111
113
|
position?: string;
|
|
112
|
-
formatter?: string | ((value:
|
|
114
|
+
formatter?: string | ((value: unknown) => string);
|
|
113
115
|
color?: string;
|
|
114
116
|
};
|
|
115
117
|
}
|
|
@@ -223,14 +225,14 @@ export function convertAnnotationToScatter(
|
|
|
223
225
|
itemStyle,
|
|
224
226
|
label: {
|
|
225
227
|
show: label?.show !== false,
|
|
226
|
-
position: label?.position || 'top',
|
|
228
|
+
position: label?.position || ('top' as const),
|
|
227
229
|
formatter: label?.formatter,
|
|
228
230
|
color: label?.color || '#333',
|
|
229
231
|
},
|
|
230
232
|
data,
|
|
231
233
|
},
|
|
232
|
-
}
|
|
233
|
-
];
|
|
234
|
+
},
|
|
235
|
+
] as unknown as EChartsOption['series'];
|
|
234
236
|
}
|
|
235
237
|
|
|
236
238
|
/**
|
|
@@ -269,28 +271,28 @@ export function useAnnotation(props: AnnotationProps): EChartsOption {
|
|
|
269
271
|
export const AnnotationPresets = {
|
|
270
272
|
/** 平均线 */
|
|
271
273
|
averageLine: (color = '#1890ff'): MarkLineConfig => ({
|
|
272
|
-
data: [{ type: 'average', name: '平均值' }]
|
|
274
|
+
data: [{ type: 'average', name: '平均值' }],
|
|
273
275
|
lineStyle: { color, type: 'dashed', width: 2 },
|
|
274
276
|
label: { show: true, position: 'end', color },
|
|
275
277
|
}),
|
|
276
278
|
|
|
277
279
|
/** 最大值线 */
|
|
278
280
|
maxLine: (color = '#f5222d'): MarkLineConfig => ({
|
|
279
|
-
data: [{ type: 'max', name: '最大值' }]
|
|
281
|
+
data: [{ type: 'max', name: '最大值' }],
|
|
280
282
|
lineStyle: { color, type: 'dashed', width: 2 },
|
|
281
283
|
label: { show: true, position: 'end', color },
|
|
282
284
|
}),
|
|
283
285
|
|
|
284
286
|
/** 最小值线 */
|
|
285
287
|
minLine: (color = '#52c41a'): MarkLineConfig => ({
|
|
286
|
-
data: [{ type: 'min', name: '最小值' }]
|
|
288
|
+
data: [{ type: 'min', name: '最小值' }],
|
|
287
289
|
lineStyle: { color, type: 'dashed', width: 2 },
|
|
288
290
|
label: { show: true, position: 'end', color },
|
|
289
291
|
}),
|
|
290
292
|
|
|
291
293
|
/** 警戒线 */
|
|
292
294
|
thresholdLine: (value: number, color = '#faad14'): MarkLineConfig => ({
|
|
293
|
-
data: [{ yAxis: value, name: '警戒线' }]
|
|
295
|
+
data: [{ yAxis: value, name: '警戒线' }],
|
|
294
296
|
lineStyle: { color, type: 'solid', width: 2 },
|
|
295
297
|
label: { show: true, position: 'start', color },
|
|
296
298
|
}),
|
|
@@ -9,6 +9,7 @@ import React, { useEffect, useRef, useMemo, useCallback } from 'react';
|
|
|
9
9
|
|
|
10
10
|
import { generateEChartsAnimationConfig } from '../animation';
|
|
11
11
|
import { EChartsOption, EChartsType, AnimationConfig } from '../types';
|
|
12
|
+
import type { DataZoomComponentOption } from 'echarts';
|
|
12
13
|
import { registerChart, removeChart, getChart } from '../utils/chartInstances';
|
|
13
14
|
import { DebugPanel, DebugPanelOptions, updateDebugInfo } from '../utils/debug';
|
|
14
15
|
import { PerformanceAnalyzer } from '../utils/performance';
|
|
@@ -21,8 +22,27 @@ import type { BaseChartProps } from '../../charts/types';
|
|
|
21
22
|
// ============================================================================
|
|
22
23
|
|
|
23
24
|
/** 图表事件参数类型 */
|
|
24
|
-
export interface ChartEventParams {
|
|
25
|
-
|
|
25
|
+
export interface ChartEventParams extends Record<string, unknown> {
|
|
26
|
+
componentType?: string;
|
|
27
|
+
componentSubType?: string;
|
|
28
|
+
componentIndex?: number;
|
|
29
|
+
seriesType?: string;
|
|
30
|
+
seriesIndex?: number;
|
|
31
|
+
seriesId?: string;
|
|
32
|
+
seriesName?: string;
|
|
33
|
+
name?: string;
|
|
34
|
+
dataIndex?: number;
|
|
35
|
+
data?: unknown;
|
|
36
|
+
dataType?: string;
|
|
37
|
+
value?: unknown;
|
|
38
|
+
color?: string;
|
|
39
|
+
borderColor?: string;
|
|
40
|
+
dimensionNames?: string[];
|
|
41
|
+
encode?: Record<string, number[]>;
|
|
42
|
+
marker?: string;
|
|
43
|
+
status?: string;
|
|
44
|
+
dimensionIndex?: number;
|
|
45
|
+
percent?: number;
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
/** 图表导出选项 */
|
|
@@ -75,8 +95,8 @@ export interface ChartProps {
|
|
|
75
95
|
enableZoom?: boolean;
|
|
76
96
|
onZoom?: (data: { start: number; end: number; dataZoomIndex: number }) => void;
|
|
77
97
|
enableDataFiltering?: boolean;
|
|
78
|
-
filters?: Record<string,
|
|
79
|
-
onDataFiltered?: (filteredData:
|
|
98
|
+
filters?: Record<string, string | number | boolean | string[] | null>;
|
|
99
|
+
onDataFiltered?: (filteredData: unknown[], filters: Record<string, unknown>) => void;
|
|
80
100
|
enableLegendInteraction?: boolean;
|
|
81
101
|
legendInteractionMode?: 'single' | 'multiple' | 'all';
|
|
82
102
|
onLegendSelect?: (params: { name: string; selected: Record<string, boolean> }) => void;
|
|
@@ -146,7 +166,7 @@ const BaseChart: React.FC<ChartProps> = (props) => {
|
|
|
146
166
|
} = props;
|
|
147
167
|
|
|
148
168
|
// Refs
|
|
149
|
-
const chartInstanceRef = useRef<
|
|
169
|
+
const chartInstanceRef = useRef<EChartsType | null>(null);
|
|
150
170
|
const performanceRef = useRef({
|
|
151
171
|
initStartTime: 0,
|
|
152
172
|
initEndTime: 0,
|
|
@@ -162,7 +182,7 @@ const BaseChart: React.FC<ChartProps> = (props) => {
|
|
|
162
182
|
isScrolling: false,
|
|
163
183
|
});
|
|
164
184
|
const oldOptionRef = useRef<EChartsOption | undefined>(option);
|
|
165
|
-
const adapterRef = useRef<
|
|
185
|
+
const adapterRef = useRef<unknown>(null);
|
|
166
186
|
const debugConfigRef = useRef<DebugPanelOptions | null>(null);
|
|
167
187
|
const performanceAnalyzerRef = useRef<PerformanceAnalyzer | null>(null);
|
|
168
188
|
|
|
@@ -206,6 +226,22 @@ const BaseChart: React.FC<ChartProps> = (props) => {
|
|
|
206
226
|
}
|
|
207
227
|
}
|
|
208
228
|
|
|
229
|
+
// Inject dataZoom when enableZoom is true (keyboard-accessible zoom)
|
|
230
|
+
if (_enableZoom) {
|
|
231
|
+
processed = JSON.parse(JSON.stringify(processed));
|
|
232
|
+
// Avoid duplicate dataZoom entries
|
|
233
|
+
const existingDzArr = Array.isArray(processed.dataZoom)
|
|
234
|
+
? processed.dataZoom as DataZoomComponentOption[]
|
|
235
|
+
: processed.dataZoom ? [processed.dataZoom as DataZoomComponentOption] : [];
|
|
236
|
+
if (!existingDzArr.some((dz) => dz?.type === 'inside')) {
|
|
237
|
+
processed.dataZoom = [
|
|
238
|
+
...(existingDzArr || []),
|
|
239
|
+
// Inside (mouse wheel + keyboard) — wired to keyboard nav in BaseChartWrapper
|
|
240
|
+
{ type: 'inside', start: 0, end: 100, zoomOnMouseWheel: true, moveOnMouseMove: false },
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
209
245
|
// Apply animation config
|
|
210
246
|
const dataLength = calculateDataLength(processed);
|
|
211
247
|
const animConfig = generateEChartsAnimationConfig(animation, dataLength);
|
|
@@ -213,6 +249,7 @@ const BaseChart: React.FC<ChartProps> = (props) => {
|
|
|
213
249
|
}, [
|
|
214
250
|
option,
|
|
215
251
|
animation,
|
|
252
|
+
_enableZoom,
|
|
216
253
|
enableDataFiltering,
|
|
217
254
|
filters,
|
|
218
255
|
virtualScroll,
|
|
@@ -223,13 +260,14 @@ const BaseChart: React.FC<ChartProps> = (props) => {
|
|
|
223
260
|
|
|
224
261
|
// Internal chartInit that wraps the user's callback
|
|
225
262
|
const handleChartInit = useCallback(
|
|
226
|
-
(instance:
|
|
263
|
+
(instance: EChartsType) => {
|
|
227
264
|
chartInstanceRef.current = instance;
|
|
228
|
-
adapterRef.current = instance;
|
|
265
|
+
adapterRef.current = instance as unknown;
|
|
229
266
|
|
|
230
267
|
// Performance monitoring init
|
|
231
268
|
if (enablePerformanceMonitoring) {
|
|
232
269
|
performanceAnalyzerRef.current = PerformanceAnalyzer.getInstance({
|
|
270
|
+
chartId,
|
|
233
271
|
enabled: true,
|
|
234
272
|
metrics: ['initTime', 'renderTime', 'updateTime', 'dataSize', 'frameRate'],
|
|
235
273
|
sampleInterval: 1000,
|
|
@@ -394,15 +432,40 @@ const BaseChart: React.FC<ChartProps> = (props) => {
|
|
|
394
432
|
}
|
|
395
433
|
}, [option, onPerformance]);
|
|
396
434
|
|
|
397
|
-
// Data update callback
|
|
435
|
+
// Data update callback — supports debounce
|
|
436
|
+
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
437
|
+
|
|
398
438
|
useEffect(() => {
|
|
399
|
-
if (onDataUpdate
|
|
439
|
+
if (!onDataUpdate || dataUpdateOptions?.enabled === false) return;
|
|
440
|
+
|
|
441
|
+
const delay = dataUpdateOptions?.debounceDelay ?? 0;
|
|
442
|
+
|
|
443
|
+
if (debounceTimerRef.current) {
|
|
444
|
+
clearTimeout(debounceTimerRef.current);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (delay > 0) {
|
|
448
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
449
|
+
const oldOpt = oldOptionRef.current;
|
|
450
|
+
if (oldOpt !== option) {
|
|
451
|
+
onDataUpdate(oldOpt, option);
|
|
452
|
+
oldOptionRef.current = option;
|
|
453
|
+
}
|
|
454
|
+
}, delay);
|
|
455
|
+
} else {
|
|
400
456
|
const oldOpt = oldOptionRef.current;
|
|
401
457
|
if (oldOpt !== option) {
|
|
402
458
|
onDataUpdate(oldOpt, option);
|
|
403
459
|
oldOptionRef.current = option;
|
|
404
460
|
}
|
|
405
461
|
}
|
|
462
|
+
|
|
463
|
+
return () => {
|
|
464
|
+
if (debounceTimerRef.current) {
|
|
465
|
+
clearTimeout(debounceTimerRef.current);
|
|
466
|
+
debounceTimerRef.current = null;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
406
469
|
}, [option, onDataUpdate, dataUpdateOptions]);
|
|
407
470
|
|
|
408
471
|
// Cleanup on unmount
|
|
@@ -413,7 +476,7 @@ const BaseChart: React.FC<ChartProps> = (props) => {
|
|
|
413
476
|
performanceAnalyzerRef.current.dispose();
|
|
414
477
|
performanceAnalyzerRef.current = null;
|
|
415
478
|
}
|
|
416
|
-
if (adapterRef.current) adapterRef.current.dispose();
|
|
479
|
+
if (adapterRef.current) (adapterRef.current as EChartsType).dispose();
|
|
417
480
|
};
|
|
418
481
|
}, [chartId]);
|
|
419
482
|
|
|
@@ -422,7 +485,7 @@ const BaseChart: React.FC<ChartProps> = (props) => {
|
|
|
422
485
|
};
|
|
423
486
|
|
|
424
487
|
const wrapperProps: BaseChartProps & { chartType: string } = {
|
|
425
|
-
option: wrappedOption as
|
|
488
|
+
option: wrappedOption as unknown as Record<string, unknown>,
|
|
426
489
|
width,
|
|
427
490
|
height,
|
|
428
491
|
theme: typeof theme === 'string' ? theme : (theme as Record<string, unknown>),
|
|
@@ -65,25 +65,30 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
65
65
|
return fallback(error, this.handleReset);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
//
|
|
68
|
+
// 默认错误展示(使用 CSS 变量,与 ThemeManager 对齐)
|
|
69
69
|
return (
|
|
70
70
|
<div
|
|
71
|
+
role="alert"
|
|
72
|
+
aria-live="assertive"
|
|
71
73
|
style={{
|
|
72
74
|
display: 'flex',
|
|
73
75
|
flexDirection: 'column',
|
|
74
76
|
alignItems: 'center',
|
|
75
77
|
justifyContent: 'center',
|
|
76
|
-
padding: '
|
|
77
|
-
backgroundColor: '#fff',
|
|
78
|
-
border: '1px solid #ff4d4f',
|
|
79
|
-
borderRadius: '8px',
|
|
80
|
-
color: '#333',
|
|
78
|
+
padding: 'var(--tv-border-radius, 16px)',
|
|
79
|
+
backgroundColor: 'var(--tv-bg-color, #fff)',
|
|
80
|
+
border: '1px solid var(--tv-error-color, #ff4d4f)',
|
|
81
|
+
borderRadius: 'var(--tv-border-radius, 8px)',
|
|
82
|
+
color: 'var(--tv-text-color, #333)',
|
|
81
83
|
minHeight: '200px',
|
|
84
|
+
fontFamily: 'var(--tv-font-family, sans-serif)',
|
|
82
85
|
}}
|
|
83
86
|
>
|
|
84
|
-
<div style={{ fontSize: '48px', marginBottom: '16px' }}>⚠️</div>
|
|
85
|
-
<h3 style={{ margin: '0 0 12px', color: '#ff4d4f' }}
|
|
86
|
-
|
|
87
|
+
<div style={{ fontSize: '48px', marginBottom: '16px' }} aria-hidden="true">⚠️</div>
|
|
88
|
+
<h3 style={{ margin: '0 0 12px', color: 'var(--tv-error-color, #ff4d4f)', fontWeight: 700 }}>
|
|
89
|
+
图表渲染失败
|
|
90
|
+
</h3>
|
|
91
|
+
<p style={{ margin: '0 0 16px', color: 'var(--tv-text-color-secondary, #666)', textAlign: 'center', maxWidth: '320px' }}>
|
|
87
92
|
图表在渲染过程中遇到错误,请检查数据配置是否正确
|
|
88
93
|
</p>
|
|
89
94
|
{showDetails && (
|
|
@@ -91,15 +96,15 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
91
96
|
style={{
|
|
92
97
|
width: '100%',
|
|
93
98
|
padding: '12px',
|
|
94
|
-
backgroundColor: '#f5f5f5',
|
|
95
|
-
borderRadius: '4px',
|
|
96
|
-
fontSize: '12px',
|
|
97
|
-
fontFamily: 'monospace',
|
|
99
|
+
backgroundColor: 'var(--tv-bg-color-secondary, #f5f5f5)',
|
|
100
|
+
borderRadius: 'var(--tv-border-radius-small, 4px)',
|
|
101
|
+
fontSize: 'var(--tv-font-size-small, 12px)',
|
|
102
|
+
fontFamily: 'var(--tv-font-family, monospace)',
|
|
98
103
|
overflow: 'auto',
|
|
99
104
|
maxHeight: '150px',
|
|
100
105
|
}}
|
|
101
106
|
>
|
|
102
|
-
<summary style={{ cursor: 'pointer', marginBottom: '8px' }}>错误详情</summary>
|
|
107
|
+
<summary style={{ cursor: 'pointer', marginBottom: '8px', fontWeight: 600 }}>错误详情</summary>
|
|
103
108
|
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
|
|
104
109
|
{error.message}
|
|
105
110
|
{'\n\n'}
|
|
@@ -112,12 +117,20 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
112
117
|
style={{
|
|
113
118
|
marginTop: '16px',
|
|
114
119
|
padding: '8px 24px',
|
|
115
|
-
backgroundColor: '#1890ff',
|
|
120
|
+
backgroundColor: 'var(--tv-primary-color, #1890ff)',
|
|
116
121
|
color: '#fff',
|
|
117
122
|
border: 'none',
|
|
118
|
-
borderRadius: '4px',
|
|
123
|
+
borderRadius: 'var(--tv-border-radius-small, 4px)',
|
|
119
124
|
cursor: 'pointer',
|
|
120
|
-
fontSize: '14px',
|
|
125
|
+
fontSize: 'var(--tv-font-size, 14px)',
|
|
126
|
+
fontWeight: 600,
|
|
127
|
+
transition: 'background-color var(--tv-transition-duration, 0.3s)',
|
|
128
|
+
}}
|
|
129
|
+
onMouseEnter={(e) => {
|
|
130
|
+
e.currentTarget.style.backgroundColor = 'var(--tv-primary-color-hover, #40a9ff)';
|
|
131
|
+
}}
|
|
132
|
+
onMouseLeave={(e) => {
|
|
133
|
+
e.currentTarget.style.backgroundColor = 'var(--tv-primary-color, #1890ff)';
|
|
121
134
|
}}
|
|
122
135
|
>
|
|
123
136
|
重试
|
|
@@ -18,7 +18,7 @@ const LazySunburstChart = lazy(() => import('../../charts/sunburst'));
|
|
|
18
18
|
const LazySankeyChart = lazy(() => import('../../charts/sankey'));
|
|
19
19
|
|
|
20
20
|
// 统一的图表类型到懒加载组件映射
|
|
21
|
-
const LAZY_CHART_MODULES: Record<string, () => Promise<{ default: ComponentType<
|
|
21
|
+
const LAZY_CHART_MODULES: Record<string, () => Promise<{ default: ComponentType<Record<string, unknown>> }>> = {
|
|
22
22
|
line: () => import('../../charts/line'),
|
|
23
23
|
bar: () => import('../../charts/bar'),
|
|
24
24
|
pie: () => import('../../charts/pie'),
|
|
@@ -35,10 +35,12 @@ const LAZY_CHART_MODULES: Record<string, () => Promise<{ default: ComponentType<
|
|
|
35
35
|
export const LAZY_CHART_TYPES = Object.keys(LAZY_CHART_MODULES);
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
38
|
+
* 默认加载状态组件(使用 CSS 变量,与 ThemeManager 对齐)
|
|
39
39
|
*/
|
|
40
40
|
const DefaultLoadingFallback: React.FC<{ text?: string }> = ({ text = '加载中...' }) => (
|
|
41
41
|
<div
|
|
42
|
+
role="status"
|
|
43
|
+
aria-label={text}
|
|
42
44
|
style={{
|
|
43
45
|
display: 'flex',
|
|
44
46
|
alignItems: 'center',
|
|
@@ -46,8 +48,8 @@ const DefaultLoadingFallback: React.FC<{ text?: string }> = ({ text = '加载中
|
|
|
46
48
|
width: '100%',
|
|
47
49
|
height: '100%',
|
|
48
50
|
minHeight: '200px',
|
|
49
|
-
backgroundColor: '#f5f5f5',
|
|
50
|
-
borderRadius: '8px',
|
|
51
|
+
backgroundColor: 'var(--tv-bg-color-secondary, #f5f5f5)',
|
|
52
|
+
borderRadius: 'var(--tv-border-radius, 8px)',
|
|
51
53
|
}}
|
|
52
54
|
>
|
|
53
55
|
<div style={{ textAlign: 'center' }}>
|
|
@@ -55,12 +57,13 @@ const DefaultLoadingFallback: React.FC<{ text?: string }> = ({ text = '加载中
|
|
|
55
57
|
style={{
|
|
56
58
|
width: '40px',
|
|
57
59
|
height: '40px',
|
|
58
|
-
border: '3px solid #1890ff',
|
|
60
|
+
border: '3px solid var(--tv-primary-color, #1890ff)',
|
|
59
61
|
borderTopColor: 'transparent',
|
|
60
62
|
borderRadius: '50%',
|
|
61
63
|
animation: 'taroviz-spin 1s linear infinite',
|
|
62
64
|
margin: '0 auto 12px',
|
|
63
65
|
}}
|
|
66
|
+
aria-hidden="true"
|
|
64
67
|
/>
|
|
65
68
|
<style>
|
|
66
69
|
{`
|
|
@@ -69,7 +72,9 @@ const DefaultLoadingFallback: React.FC<{ text?: string }> = ({ text = '加载中
|
|
|
69
72
|
}
|
|
70
73
|
`}
|
|
71
74
|
</style>
|
|
72
|
-
<span style={{ color: '#666', fontSize: '14px' }}>
|
|
75
|
+
<span style={{ color: 'var(--tv-text-color-secondary, #666)', fontSize: 'var(--tv-font-size, 14px)' }}>
|
|
76
|
+
{text}
|
|
77
|
+
</span>
|
|
73
78
|
</div>
|
|
74
79
|
</div>
|
|
75
80
|
);
|
|
@@ -136,8 +141,8 @@ export function preloadAllCharts(): Promise<void[]> {
|
|
|
136
141
|
* 创建懒加载图表映射
|
|
137
142
|
* 用于动态导入图表
|
|
138
143
|
*/
|
|
139
|
-
export function createLazyChart(chartType: string): ComponentType<
|
|
140
|
-
const lazyCharts: Record<string, ComponentType<
|
|
144
|
+
export function createLazyChart(chartType: string): ComponentType<Record<string, unknown>> | null {
|
|
145
|
+
const lazyCharts: Record<string, ComponentType<Record<string, unknown>>> = {
|
|
141
146
|
line: LazyLineChart,
|
|
142
147
|
bar: LazyBarChart,
|
|
143
148
|
pie: LazyPieChart,
|
|
@@ -159,7 +164,7 @@ export function createLazyChart(chartType: string): ComponentType<any> | null {
|
|
|
159
164
|
* 用于按名称动态获取懒加载图表组件
|
|
160
165
|
*/
|
|
161
166
|
export const LazyChartRegistry = {
|
|
162
|
-
get(chartType: string): ComponentType<
|
|
167
|
+
get(chartType: string): ComponentType<Record<string, unknown>> | null {
|
|
163
168
|
return createLazyChart(chartType);
|
|
164
169
|
},
|
|
165
170
|
|