@agions/taroviz 1.7.0 → 1.9.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.
@@ -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: 1000,
36
+ duration: 400,
25
37
  easing: 'cubicOut',
26
- appearDuration: 1200,
38
+ appearDuration: 450,
27
39
  appearEasing: 'cubicOut',
28
- updateDuration: 800,
40
+ updateDuration: 300,
29
41
  updateEasing: 'cubicOut',
30
- disappearDuration: 600,
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: 500,
43
- easing: 'linear',
44
- appearDuration: 600,
45
- appearEasing: 'linear',
46
- updateDuration: 400,
47
- updateEasing: 'linear',
48
- disappearDuration: 300,
49
- disappearEasing: 'linear',
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: 2000,
72
+ duration: 500,
61
73
  easing: 'cubicInOut',
62
- appearDuration: 2400,
74
+ appearDuration: 600,
63
75
  appearEasing: 'cubicInOut',
64
- updateDuration: 1600,
76
+ updateDuration: 400,
65
77
  updateEasing: 'cubicInOut',
66
- disappearDuration: 1200,
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: true,
78
- duration: 1500,
79
- easing: 'bounceOut',
80
- appearDuration: 1800,
81
- appearEasing: 'bounceOut',
82
- updateDuration: 1200,
83
- updateEasing: 'bounceOut',
84
- disappearDuration: 900,
85
- disappearEasing: 'bounceIn',
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: true,
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: true,
96
- duration: 1500,
97
- easing: 'elasticOut',
98
- appearDuration: 1800,
99
- appearEasing: 'elasticOut',
100
- updateDuration: 1200,
101
- updateEasing: 'elasticOut',
102
- disappearDuration: 900,
103
- disappearEasing: 'elasticIn',
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: true,
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;
@@ -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
- [key: string]: any;
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
  /** 图表导出选项 */
@@ -146,7 +166,7 @@ const BaseChart: React.FC<ChartProps> = (props) => {
146
166
  } = props;
147
167
 
148
168
  // Refs
149
- const chartInstanceRef = useRef<any>(null);
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<any>(null);
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: any) => {
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 && dataUpdateOptions?.enabled !== false) {
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
 
@@ -1,12 +1,31 @@
1
1
  /**
2
2
  * ECharts 类型定义
3
3
  */
4
- export type EChartsOption = any;
4
+ export type { EChartsOption } from 'echarts';
5
+
6
+ /**
7
+ * 图表事件参数类型
8
+ * 使用 ECharts 内的事件参数接口
9
+ */
10
+ export interface ChartEventParams {
11
+ componentType?: string;
12
+ seriesType?: string;
13
+ seriesIndex?: number;
14
+ seriesName?: string;
15
+ name?: string;
16
+ dataIndex?: number;
17
+ data?: unknown;
18
+ value?: unknown;
19
+ color?: string;
20
+ borderColor?: string;
21
+ borderWidth?: number;
22
+ target?: unknown;
23
+ }
5
24
 
6
25
  /**
7
26
  * 图表事件监听器
8
27
  */
9
- export type ChartEventListener = Record<string, (params: any) => void>;
28
+ export type ChartEventListener = Record<string, (params: ChartEventParams) => void>;
10
29
 
11
30
  /**
12
31
  * 图表渲染器类型
@@ -17,24 +36,9 @@ export type ChartRenderer = 'canvas' | 'svg';
17
36
  * DataURL选项
18
37
  */
19
38
  export interface DataURLOption {
20
- /**
21
- * 导出的图片类型
22
- */
23
39
  type?: 'png' | 'jpeg';
24
-
25
- /**
26
- * 设备像素比
27
- */
28
40
  pixelRatio?: number;
29
-
30
- /**
31
- * 背景色
32
- */
33
41
  backgroundColor?: string;
34
-
35
- /**
36
- * 排除的组件列表
37
- */
38
42
  excludeComponents?: string[];
39
43
  }
40
44
 
@@ -42,14 +46,7 @@ export interface DataURLOption {
42
46
  * 图表尺寸类型
43
47
  */
44
48
  export interface ChartSize {
45
- /**
46
- * 宽度
47
- */
48
49
  width?: number | string;
49
-
50
- /**
51
- * 高度
52
- */
53
50
  height?: number | string;
54
51
  }
55
52
 
@@ -57,29 +54,10 @@ export interface ChartSize {
57
54
  * 图表动画设置
58
55
  */
59
56
  export interface ChartAnimation {
60
- /**
61
- * 是否启用动画
62
- */
63
57
  enabled?: boolean;
64
-
65
- /**
66
- * 动画时长
67
- */
68
58
  duration?: number;
69
-
70
- /**
71
- * 动画缓动效果
72
- */
73
59
  easing?: string;
74
-
75
- /**
76
- * 初始动画时长
77
- */
78
60
  animationDuration?: number;
79
-
80
- /**
81
- * 更新动画时长
82
- */
83
61
  animationDurationUpdate?: number;
84
62
  }
85
63
 
@@ -87,29 +65,10 @@ export interface ChartAnimation {
87
65
  * 图表主题设置
88
66
  */
89
67
  export interface ChartTheme {
90
- /**
91
- * 主题名称或配置
92
- */
93
68
  theme?: string | object;
94
-
95
- /**
96
- * 背景色
97
- */
98
69
  backgroundColor?: string;
99
-
100
- /**
101
- * 文本颜色
102
- */
103
70
  textColor?: string;
104
-
105
- /**
106
- * 轴线颜色
107
- */
108
71
  axisLineColor?: string;
109
-
110
- /**
111
- * 分割线颜色
112
- */
113
72
  splitLineColor?: string;
114
73
  }
115
74
 
@@ -117,64 +76,17 @@ export interface ChartTheme {
117
76
  * 图表加载状态配置
118
77
  */
119
78
  export interface ChartLoadingOptions {
120
- /**
121
- * 加载提示文本
122
- */
123
79
  text?: string;
124
-
125
- /**
126
- * 加载动画颜色
127
- */
128
80
  color?: string;
129
-
130
- /**
131
- * 文本颜色
132
- */
133
81
  textColor?: string;
134
-
135
- /**
136
- * 遮罩颜色
137
- */
138
82
  maskColor?: string;
139
-
140
- /**
141
- * z层级
142
- */
143
83
  zlevel?: number;
144
-
145
- /**
146
- * 字体大小
147
- */
148
84
  fontSize?: number;
149
-
150
- /**
151
- * 是否显示旋转器
152
- */
153
85
  showSpinner?: boolean;
154
-
155
- /**
156
- * 旋转器半径
157
- */
158
86
  spinnerRadius?: number;
159
-
160
- /**
161
- * 线宽
162
- */
163
87
  lineWidth?: number;
164
-
165
- /**
166
- * 字体粗细
167
- */
168
88
  fontWeight?: number | string;
169
-
170
- /**
171
- * 字体样式
172
- */
173
89
  fontStyle?: string;
174
-
175
- /**
176
- * 字体族
177
- */
178
90
  fontFamily?: string;
179
91
  }
180
92
 
@@ -216,7 +128,6 @@ export enum ChartEventType {
216
128
  BRUSHSELECTED = 'brushselected',
217
129
  RENDERED = 'rendered',
218
130
  FINISHED = 'finished',
219
- // 自定义事件
220
131
  CHART_READY = 'chartReady',
221
132
  CHART_RESIZE = 'chartResize',
222
133
  CHART_ERROR = 'chartError',