@agions/taroviz 1.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agions/taroviz",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "基于 Taro 和 ECharts 的多端图表组件库",
5
5
  "type": "module",
6
6
  "main": "dist/cjs/index.js",
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * 箱线图类型定义
3
3
  */
4
+ import type { EChartsType, ECElementEvent } from 'echarts';
5
+ import type { LoadingOptions } from '../types';
4
6
 
5
7
  export type BoxplotChartProps = {
6
8
  option?: BoxplotOption;
@@ -8,11 +10,11 @@ export type BoxplotChartProps = {
8
10
  height?: string | number;
9
11
  className?: string;
10
12
  style?: React.CSSProperties;
11
- onEvents?: Record<string, (params: any) => void>;
13
+ onEvents?: Record<string, (params: ECElementEvent) => void>;
12
14
  loading?: boolean;
13
- loadingOption?: any;
15
+ loadingOption?: LoadingOptions;
14
16
  theme?: string;
15
- onChartReady?: (chart: any) => void;
17
+ onChartReady?: (chart: EChartsType) => void;
16
18
  opts?: {
17
19
  devicePixelRatio?: number;
18
20
  renderer?: 'canvas' | 'svg';
@@ -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: any) => void>;
14
+ onEvents?: Record<string, (params: ECElementEvent) => void>;
12
15
  loading?: boolean;
13
- loadingOption?: any;
16
+ loadingOption?: LoadingOptions;
14
17
  theme?: string;
15
- onChartReady?: (chart: any) => void;
18
+ onChartReady?: (chart: EChartsType) => void;
16
19
  opts?: {
17
20
  devicePixelRatio?: number;
18
21
  renderer?: 'canvas' | 'svg';
@@ -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: any) => void;
169
+ onChartInit?: (chart: EChartsType) => void;
170
170
  /** 图表就绪回调 */
171
- onChartReady?: (chart: any) => void;
171
+ onChartReady?: (chart: EChartsType) => void;
172
172
  /** 事件回调 */
173
- onEvents?: Record<string, (params: any) => void>;
173
+ onEvents?: Record<string, (params: ECElementEvent) => void>;
174
174
  }
@@ -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?: string;
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: any) => string);
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
- } as any,
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: '平均值' }] as any,
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: '最大值' }] as any,
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: '最小值' }] as any,
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: '警戒线' }] as any,
295
+ data: [{ yAxis: value, name: '警戒线' }],
294
296
  lineStyle: { color, type: 'solid', width: 2 },
295
297
  label: { show: true, position: 'start', color },
296
298
  }),
@@ -95,8 +95,8 @@ export interface ChartProps {
95
95
  enableZoom?: boolean;
96
96
  onZoom?: (data: { start: number; end: number; dataZoomIndex: number }) => void;
97
97
  enableDataFiltering?: boolean;
98
- filters?: Record<string, any>;
99
- onDataFiltered?: (filteredData: any[], filters: Record<string, any>) => void;
98
+ filters?: Record<string, string | number | boolean | string[] | null>;
99
+ onDataFiltered?: (filteredData: unknown[], filters: Record<string, unknown>) => void;
100
100
  enableLegendInteraction?: boolean;
101
101
  legendInteractionMode?: 'single' | 'multiple' | 'all';
102
102
  onLegendSelect?: (params: { name: string; selected: Record<string, boolean> }) => void;
@@ -485,7 +485,7 @@ const BaseChart: React.FC<ChartProps> = (props) => {
485
485
  };
486
486
 
487
487
  const wrapperProps: BaseChartProps & { chartType: string } = {
488
- option: wrappedOption as any,
488
+ option: wrappedOption as unknown as Record<string, unknown>,
489
489
  width,
490
490
  height,
491
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: '20px',
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' }}>图表渲染失败</h3>
86
- <p style={{ margin: '0 0 16px', color: '#666', textAlign: 'center' }}>
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<any> }>> = {
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' }}>{text}</span>
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<any> | null {
140
- const lazyCharts: Record<string, ComponentType<any>> = {
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<any> | null {
167
+ get(chartType: string): ComponentType<Record<string, unknown>> | null {
163
168
  return createLazyChart(chartType);
164
169
  },
165
170
 
@@ -578,6 +578,39 @@ class ThemeManager {
578
578
  return this.currentTheme.isDark || this.currentTheme.type === 'dark';
579
579
  }
580
580
 
581
+ /**
582
+ * 检测系统 prefers-color-scheme 是否为暗色
583
+ */
584
+ public getSystemPrefersDark(): boolean {
585
+ if (typeof window === 'undefined') return false;
586
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
587
+ }
588
+
589
+ /**
590
+ * 订阅系统主题变化,自动切换匹配的主题
591
+ * @returns 取消订阅的函数
592
+ */
593
+ public watchSystemTheme(): () => void {
594
+ if (typeof window === 'undefined') return () => {};
595
+
596
+ const mq = window.matchMedia('(prefers-color-scheme: dark)');
597
+ const handler = (e: MediaQueryListEvent) => {
598
+ // 当系统主题切换时,自动应用对应主题
599
+ this.setTheme(e.matches ? 'dark' : 'default');
600
+ };
601
+ mq.addEventListener('change', handler);
602
+ return () => mq.removeEventListener('change', handler);
603
+ }
604
+
605
+ /**
606
+ * 应用初始主题(根据系统偏好自动选择 dark/default)
607
+ * 必须在 DOM 加载后调用
608
+ */
609
+ public applyInitialTheme(): void {
610
+ const prefersDark = this.getSystemPrefersDark();
611
+ this.setTheme(prefersDark ? 'dark' : 'default');
612
+ }
613
+
581
614
  /**
582
615
  * 注册主题变更监听器
583
616
  */
@@ -3,6 +3,10 @@
3
3
  */
4
4
  import type { EChartsOption } from 'echarts';
5
5
 
6
+ interface SeriesItem {
7
+ data?: unknown[] | Record<string, unknown>;
8
+ }
9
+
6
10
  /**
7
11
  * Normalize size value to CSS string
8
12
  */
@@ -19,7 +23,7 @@ export function calculateDataLength(option: { series?: unknown } | undefined): n
19
23
  let count = 0;
20
24
  if (option.series) {
21
25
  const series = Array.isArray(option.series) ? option.series : [option.series];
22
- for (const seriesItem of series as any[]) {
26
+ for (const seriesItem of series as SeriesItem[]) {
23
27
  if (seriesItem.data) {
24
28
  if (Array.isArray(seriesItem.data)) {
25
29
  count += seriesItem.data.length;
@@ -35,11 +39,12 @@ export function calculateDataLength(option: { series?: unknown } | undefined): n
35
39
  /**
36
40
  * Filter data by filter conditions
37
41
  */
38
- export function filterDataByKeys(data: any[], filters: Record<string, any>): any[] {
42
+ export function filterDataByKeys<T>(data: T[], filters: Record<string, unknown>): T[] {
39
43
  if (!filters || Object.keys(filters).length === 0) return data;
40
44
  return data.filter((item) => {
41
45
  for (const [key, value] of Object.entries(filters)) {
42
- if (item[key] !== value && !item[key]?.includes?.(value)) return false;
46
+ const itemVal = (item as Record<string, unknown>)[key];
47
+ if (itemVal !== value && !(Array.isArray(itemVal) && itemVal.includes(value))) return false;
43
48
  }
44
49
  return true;
45
50
  });
@@ -9,6 +9,8 @@ import type { EChartsOption } from 'echarts';
9
9
  import { useDataZoom } from './useDataZoom';
10
10
  import { useChartConnect } from './useChartConnect';
11
11
  import { useChartDownload } from './useChartDownload';
12
+ import { useChartHistory } from './useChartHistory';
13
+ import { useChartSelection } from './useChartSelection';
12
14
 
13
15
  // ============================================================================
14
16
  // 类型定义
@@ -651,6 +653,23 @@ export {
651
653
  type DownloadDataOptions,
652
654
  } from './useChartDownload';
653
655
 
656
+ // 图表历史记录 Hook (Undo/Redo)
657
+ export {
658
+ useChartHistory,
659
+ type UseChartHistoryOptions,
660
+ type UseChartHistoryReturn,
661
+ } from './useChartHistory';
662
+
663
+ // 图表选择 Hook
664
+ export {
665
+ useChartSelection,
666
+ type UseChartSelectionOptions,
667
+ type UseChartSelectionReturn,
668
+ type DataPointKey,
669
+ type SelectionMode,
670
+ type SelectionEvent,
671
+ } from './useChartSelection';
672
+
654
673
  // ============================================================================
655
674
  // 导出
656
675
  // ============================================================================
@@ -683,4 +702,6 @@ export default {
683
702
  useDataZoom,
684
703
  useChartConnect,
685
704
  useChartDownload,
705
+ useChartHistory,
706
+ useChartSelection,
686
707
  };