@agions/taroviz 1.11.1 → 2.0.3

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.
Files changed (164) hide show
  1. package/CHANGELOG.md +245 -0
  2. package/README.md +104 -302
  3. package/dist/cjs/index.js +1 -1
  4. package/dist/cjs/vendors.js +1 -0
  5. package/dist/cjs/vendors~echarts.js +1 -0
  6. package/dist/esm/index.js +1 -58151
  7. package/dist/esm/vendors.js +1 -0
  8. package/dist/esm/vendors~echarts.js +1 -0
  9. package/package.json +19 -25
  10. package/src/adapters/MiniAppAdapter.ts +136 -0
  11. package/src/adapters/__tests__/index.test.ts +1 -1
  12. package/src/adapters/h5/__tests__/index.test.ts +4 -2
  13. package/src/adapters/h5/index.ts +63 -64
  14. package/src/adapters/harmony/index.ts +23 -245
  15. package/src/adapters/index.ts +49 -45
  16. package/src/adapters/swan/index.ts +6 -69
  17. package/src/adapters/tt/index.ts +7 -70
  18. package/src/adapters/types.ts +25 -58
  19. package/src/adapters/weapp/index.ts +6 -69
  20. package/src/charts/__tests__/testUtils.tsx +87 -0
  21. package/src/charts/boxplot/__tests__/index.test.tsx +49 -103
  22. package/src/charts/boxplot/index.tsx +2 -1
  23. package/src/charts/boxplot/types.ts +17 -16
  24. package/src/charts/common/BaseChartWrapper.tsx +90 -82
  25. package/src/charts/common/__mocks__/BaseChartWrapper.tsx +17 -0
  26. package/src/charts/createChartComponent.tsx +36 -0
  27. package/src/charts/createOptionChartComponent.tsx +32 -0
  28. package/src/charts/funnel/__tests__/index.test.tsx +99 -0
  29. package/src/charts/funnel/index.tsx +60 -10
  30. package/src/charts/funnel/types.ts +6 -0
  31. package/src/charts/graph/__tests__/index.test.tsx +102 -33
  32. package/src/charts/graph/index.tsx +66 -9
  33. package/src/charts/graph/types.ts +6 -0
  34. package/src/charts/heatmap/__tests__/index.test.tsx +139 -0
  35. package/src/charts/heatmap/index.tsx +103 -10
  36. package/src/charts/heatmap/types.ts +6 -0
  37. package/src/charts/index.ts +74 -26
  38. package/src/charts/liquid/__tests__/index.test.tsx +52 -0
  39. package/src/charts/liquid/index.tsx +239 -182
  40. package/src/charts/liquid/types.ts +11 -11
  41. package/src/charts/parallel/__tests__/index.test.tsx +40 -67
  42. package/src/charts/parallel/index.tsx +2 -1
  43. package/src/charts/parallel/types.ts +19 -18
  44. package/src/charts/radar/__tests__/index.test.tsx +210 -0
  45. package/src/charts/radar/index.tsx +143 -10
  46. package/src/charts/radar/types.ts +13 -0
  47. package/src/charts/sankey/__tests__/index.test.tsx +124 -0
  48. package/src/charts/sankey/index.tsx +62 -10
  49. package/src/charts/sankey/types.ts +6 -0
  50. package/src/charts/tree/__tests__/index.test.tsx +71 -0
  51. package/src/charts/tree/index.tsx +5 -2
  52. package/src/charts/tree/types.ts +9 -9
  53. package/src/charts/types.ts +208 -106
  54. package/src/charts/utils.ts +9 -7
  55. package/src/charts/wordcloud/__tests__/index.test.tsx +98 -31
  56. package/src/charts/wordcloud/index.tsx +75 -9
  57. package/src/charts/wordcloud/types.ts +6 -0
  58. package/src/components/DataFilter/index.tsx +32 -10
  59. package/src/core/animation/types.ts +6 -6
  60. package/src/core/components/Annotation.tsx +6 -7
  61. package/src/core/components/BaseChart.tsx +110 -168
  62. package/src/core/components/ErrorBoundary.tsx +17 -4
  63. package/src/core/components/LazyChart.tsx +54 -55
  64. package/src/core/components/hooks/index.ts +6 -2
  65. package/src/core/components/hooks/useChartInit.ts +6 -3
  66. package/src/core/components/hooks/usePerformance.ts +8 -2
  67. package/src/core/components/hooks/useVirtualScroll.ts +2 -1
  68. package/src/core/index.ts +1 -1
  69. package/src/core/themes/ThemeManager.ts +1 -1
  70. package/src/core/types/common.ts +2 -1
  71. package/src/core/types/index.ts +0 -12
  72. package/src/core/types/platform.ts +3 -5
  73. package/src/core/utils/__tests__/deepClone.test.ts +317 -0
  74. package/src/core/utils/__tests__/index.test.ts +2 -1
  75. package/src/core/utils/chartInstances.ts +13 -0
  76. package/src/core/utils/common.ts +20 -29
  77. package/src/core/utils/deepClone.ts +114 -0
  78. package/src/core/utils/download.ts +128 -0
  79. package/src/core/utils/drillDown.ts +34 -353
  80. package/src/core/utils/drillDownHelpers.ts +426 -0
  81. package/src/core/utils/events.ts +12 -0
  82. package/src/core/utils/export/ExportUtils.ts +36 -67
  83. package/src/core/utils/format.ts +44 -0
  84. package/src/core/utils/index.ts +21 -154
  85. package/src/core/utils/merge.ts +25 -0
  86. package/src/core/utils/performance/PerformanceAnalyzer.ts +38 -21
  87. package/src/core/utils/performance/hooks.ts +7 -0
  88. package/src/core/utils/performance/index.ts +2 -0
  89. package/src/{hooks → core/utils/performance}/useAnimation.ts +45 -41
  90. package/src/core/utils/performance/useDataZoom.ts +324 -0
  91. package/src/{hooks → core/utils/performance}/usePerformance.ts +49 -41
  92. package/src/core/utils/performance/usePerformanceHooks.ts +278 -0
  93. package/src/core/utils/performanceUtils.ts +310 -0
  94. package/src/core/utils/runtime.ts +190 -0
  95. package/src/core/utils/setOptionUtils.ts +59 -0
  96. package/src/core/version.ts +14 -0
  97. package/src/editor/EnhancedThemeEditor.tsx +362 -540
  98. package/src/editor/ThemeEditor.tsx +55 -321
  99. package/src/editor/components/ThemeBasicSettings.tsx +113 -0
  100. package/src/editor/components/ThemeColorEditor.tsx +105 -0
  101. package/src/editor/components/ThemeSelector.tsx +70 -0
  102. package/src/editor/hooks/useThemeEditorState.ts +201 -0
  103. package/src/editor/index.ts +10 -2
  104. package/src/hooks/__tests__/index.test.tsx +3 -1
  105. package/src/hooks/chartConnectHelpers.ts +341 -0
  106. package/src/hooks/index.ts +55 -660
  107. package/src/hooks/types.ts +189 -0
  108. package/src/hooks/useChartAutoResize.ts +73 -0
  109. package/src/hooks/useChartConnect.ts +92 -238
  110. package/src/hooks/useChartDownload.ts +25 -27
  111. package/src/hooks/useChartHistory.ts +34 -49
  112. package/src/hooks/useChartInit.ts +59 -0
  113. package/src/hooks/useChartOptions.ts +259 -0
  114. package/src/hooks/useChartPerformance.ts +109 -0
  115. package/src/hooks/useChartSelection.ts +52 -49
  116. package/src/hooks/useChartTheme.ts +51 -0
  117. package/src/hooks/useDataTransform.ts +19 -4
  118. package/src/hooks/utils/chartDownloadUtils.ts +40 -53
  119. package/src/hooks/utils/dataTransformUtils.ts +22 -0
  120. package/src/index.ts +48 -34
  121. package/src/main.tsx +4 -9
  122. package/src/react-dom.d.ts +3 -3
  123. package/src/themes/index.ts +30 -855
  124. package/src/themes/palettes/blue-green.ts +13 -0
  125. package/src/themes/palettes/chalk.ts +13 -0
  126. package/src/themes/palettes/cyber.ts +44 -0
  127. package/src/themes/palettes/dark.ts +52 -0
  128. package/src/themes/palettes/default.ts +52 -0
  129. package/src/themes/palettes/elegant.ts +34 -0
  130. package/src/themes/palettes/forest.ts +13 -0
  131. package/src/themes/palettes/glass.ts +49 -0
  132. package/src/themes/palettes/golden.ts +13 -0
  133. package/src/themes/palettes/neon.ts +43 -0
  134. package/src/themes/palettes/ocean.ts +39 -0
  135. package/src/themes/palettes/pastel.ts +37 -0
  136. package/src/themes/palettes/purple-passion.ts +13 -0
  137. package/src/themes/palettes/retro.ts +33 -0
  138. package/src/themes/palettes/sunset.ts +40 -0
  139. package/src/themes/palettes/walden.ts +13 -0
  140. package/src/themes/registry.ts +184 -0
  141. package/src/themes/types.ts +213 -0
  142. package/src/charts/bar/__tests__/index.test.tsx +0 -113
  143. package/src/charts/bar/index.tsx +0 -14
  144. package/src/charts/candlestick/__tests__/index.test.tsx +0 -40
  145. package/src/charts/candlestick/index.tsx +0 -13
  146. package/src/charts/gauge/index.tsx +0 -14
  147. package/src/charts/line/__tests__/index.test.tsx +0 -107
  148. package/src/charts/line/index.tsx +0 -15
  149. package/src/charts/pie/__tests__/index.test.tsx +0 -112
  150. package/src/charts/pie/index.tsx +0 -14
  151. package/src/charts/scatter/index.tsx +0 -14
  152. package/src/charts/sunburst/index.tsx +0 -18
  153. package/src/charts/treemap/index.tsx +0 -18
  154. package/src/core/utils/codeGenerator/CodeGenerator.ts +0 -669
  155. package/src/core/utils/codeGenerator/index.ts +0 -13
  156. package/src/core/utils/codeGenerator/types.ts +0 -198
  157. package/src/core/utils/configGenerator/ConfigGenerator.ts +0 -583
  158. package/src/core/utils/configGenerator/index.ts +0 -13
  159. package/src/core/utils/configGenerator/types.ts +0 -445
  160. package/src/core/utils/debug/DebugPanel.tsx +0 -637
  161. package/src/core/utils/debug/debugger.ts +0 -322
  162. package/src/core/utils/debug/index.ts +0 -21
  163. package/src/core/utils/debug/types.ts +0 -142
  164. package/src/hooks/useDataZoom.ts +0 -323
@@ -36,8 +36,8 @@ function buildAriaLabel(chartType: string, option: unknown): string {
36
36
  }
37
37
 
38
38
  // ─── Keyboard navigation step sizes ───────────────────────────────────────
39
- const ZOOM_STEP = 5; // % per key press
40
- const PAN_STEP = 10; // % pan per arrow key
39
+ const ZOOM_STEP = 5; // % per key press
40
+ const PAN_STEP = 10; // % pan per arrow key
41
41
 
42
42
  const BaseChartWrapper: React.FC<BaseChartProps & { chartType: string }> = ({
43
43
  option,
@@ -60,80 +60,77 @@ const BaseChartWrapper: React.FC<BaseChartProps & { chartType: string }> = ({
60
60
  const containerRef = useRef<HTMLDivElement>(null);
61
61
  const isMountedRef = useRef(true);
62
62
  const cleanupRef = useRef<(() => void) | null>(null);
63
- const tableId = useId(); // unique id for aria-describedby
63
+ const tableId = useId(); // unique id for aria-describedby
64
64
  const seriesData = useMemo(() => extractSeriesData(option), [option]);
65
65
  const ariaLabel = useMemo(() => buildAriaLabel(chartType, option), [chartType, option]);
66
66
 
67
- // Keyboard handler for zoom/pan — attached to the chart container
68
- const handleKeyDown = useCallback(
69
- (e: React.KeyboardEvent<HTMLDivElement>) => {
70
- const instance = chartInstance.current;
71
- if (!instance) return;
67
+ // Keyboard _handler for zoom/pan — attached to the chart container
68
+ const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
69
+ const instance = chartInstance.current;
70
+ if (!instance) return;
72
71
 
73
- // ECharts dataZoom dispatch — works for any chart with dataZoom axis
74
- const dispatchZoom = (startDelta: number, endDelta: number) => {
75
- instance.dispatchAction({ type: 'dataZoom', startDelta, endDelta });
76
- };
72
+ // ECharts dataZoom dispatch — works for any chart with dataZoom axis
73
+ const dispatchZoom = (startDelta: number, endDelta: number) => {
74
+ instance.dispatchAction({ type: 'dataZoom', startDelta, endDelta });
75
+ };
77
76
 
78
- // Home = reset zoom to full range
79
- if (e.key === 'Home') {
80
- e.preventDefault();
81
- instance.dispatchAction({ type: 'dataZoom', start: 0, end: 100 });
82
- return;
83
- }
77
+ // Home = reset zoom to full range
78
+ if (e.key === 'Home') {
79
+ e.preventDefault();
80
+ instance.dispatchAction({ type: 'dataZoom', start: 0, end: 100 });
81
+ return;
82
+ }
84
83
 
85
- switch (e.key) {
86
- case '+':
87
- case '=': {
88
- e.preventDefault();
89
- // Zoom in (narrow range) — decrease end by ZOOM_STEP
90
- const end = instance.getOption() as { dataZoom?: Array<{ start?: number; end?: number }> };
91
- const dz = end?.dataZoom?.[0];
92
- if (dz) {
93
- const newEnd = Math.max(0, (dz.end ?? 100) - ZOOM_STEP);
94
- const newStart = Math.max(0, (dz.start ?? 0) - ZOOM_STEP);
95
- instance.dispatchAction({ type: 'dataZoom', start: newStart, end: newEnd });
96
- }
97
- break;
98
- }
99
- case '-':
100
- case '_': {
101
- e.preventDefault();
102
- // Zoom out (expand range) — increase end by ZOOM_STEP
103
- const end = instance.getOption() as { dataZoom?: Array<{ start?: number; end?: number }> };
104
- const dz = end?.dataZoom?.[0];
105
- if (dz) {
106
- const newEnd = Math.min(100, (dz.end ?? 100) + ZOOM_STEP);
107
- const newStart = Math.min((dz.start ?? 0) + ZOOM_STEP, newEnd);
108
- instance.dispatchAction({ type: 'dataZoom', start: newStart, end: newEnd });
109
- }
110
- break;
111
- }
112
- case 'ArrowLeft': {
113
- e.preventDefault();
114
- dispatchZoom(-PAN_STEP, 0);
115
- break;
116
- }
117
- case 'ArrowRight': {
118
- e.preventDefault();
119
- dispatchZoom(PAN_STEP, 0);
120
- break;
121
- }
122
- case 'ArrowUp': {
123
- e.preventDefault();
124
- dispatchZoom(0, -PAN_STEP);
125
- break;
84
+ switch (e.key) {
85
+ case '+':
86
+ case '=': {
87
+ e.preventDefault();
88
+ // Zoom in (narrow range) — decrease end by ZOOM_STEP
89
+ const end = instance.getOption() as { dataZoom?: Array<{ start?: number; end?: number }> };
90
+ const dz = end?.dataZoom?.[0];
91
+ if (dz) {
92
+ const newEnd = Math.max(0, (dz.end ?? 100) - ZOOM_STEP);
93
+ const newStart = Math.max(0, (dz.start ?? 0) - ZOOM_STEP);
94
+ instance.dispatchAction({ type: 'dataZoom', start: newStart, end: newEnd });
126
95
  }
127
- case 'ArrowDown': {
128
- e.preventDefault();
129
- dispatchZoom(0, PAN_STEP);
130
- break;
96
+ break;
97
+ }
98
+ case '-':
99
+ case '_': {
100
+ e.preventDefault();
101
+ // Zoom out (expand range) — increase end by ZOOM_STEP
102
+ const end = instance.getOption() as { dataZoom?: Array<{ start?: number; end?: number }> };
103
+ const dz = end?.dataZoom?.[0];
104
+ if (dz) {
105
+ const newEnd = Math.min(100, (dz.end ?? 100) + ZOOM_STEP);
106
+ const newStart = Math.min((dz.start ?? 0) + ZOOM_STEP, newEnd);
107
+ instance.dispatchAction({ type: 'dataZoom', start: newStart, end: newEnd });
131
108
  }
132
- // No default — let other keys pass through for accessibility tools
109
+ break;
133
110
  }
134
- },
135
- []
136
- );
111
+ case 'ArrowLeft': {
112
+ e.preventDefault();
113
+ dispatchZoom(-PAN_STEP, 0);
114
+ break;
115
+ }
116
+ case 'ArrowRight': {
117
+ e.preventDefault();
118
+ dispatchZoom(PAN_STEP, 0);
119
+ break;
120
+ }
121
+ case 'ArrowUp': {
122
+ e.preventDefault();
123
+ dispatchZoom(0, -PAN_STEP);
124
+ break;
125
+ }
126
+ case 'ArrowDown': {
127
+ e.preventDefault();
128
+ dispatchZoom(0, PAN_STEP);
129
+ break;
130
+ }
131
+ // No default — let other keys pass through for accessibility tools
132
+ }
133
+ }, []);
137
134
 
138
135
  // Use memo to cache adapter config
139
136
  const adapterConfig = useMemo(() => {
@@ -164,8 +161,12 @@ const BaseChartWrapper: React.FC<BaseChartProps & { chartType: string }> = ({
164
161
  chartInstance.current = instance;
165
162
 
166
163
  if (onEvents) {
167
- Object.entries(onEvents).forEach(([eventName, handler]) => {
168
- (instance as unknown as { on: Function }).on(eventName, handler);
164
+ Object.entries(onEvents).forEach(([_eventName, _handler]) => {
165
+ (
166
+ instance as unknown as {
167
+ on: (_event: string, _handler: (..._args: unknown[]) => void) => void;
168
+ }
169
+ ).on(_eventName, _handler);
169
170
  });
170
171
  }
171
172
 
@@ -180,6 +181,7 @@ const BaseChartWrapper: React.FC<BaseChartProps & { chartType: string }> = ({
180
181
  });
181
182
 
182
183
  const adapter = await getAdapter(initConfig);
184
+ const adapterRef = adapter; // store for cleanup
183
185
 
184
186
  if (!isMountedRef.current) {
185
187
  return;
@@ -188,13 +190,15 @@ const BaseChartWrapper: React.FC<BaseChartProps & { chartType: string }> = ({
188
190
  adapter.init();
189
191
 
190
192
  cleanupRef.current = () => {
191
- if (chartInstance.current) {
193
+ adapterRef.dispose?.();
194
+ const instance = chartInstance.current;
195
+ if (instance) {
192
196
  if (onEvents) {
193
- Object.entries(onEvents).forEach(([eventName]) => {
194
- (chartInstance.current as unknown as { off: Function }).off(eventName);
197
+ Object.entries(onEvents).forEach(([_eventName]) => {
198
+ (instance as unknown as { off: (_event: string) => void }).off(_eventName);
195
199
  });
196
200
  }
197
- chartInstance.current.dispose();
201
+ instance.dispose();
198
202
  chartInstance.current = null;
199
203
  }
200
204
  };
@@ -262,25 +266,29 @@ const BaseChartWrapper: React.FC<BaseChartProps & { chartType: string }> = ({
262
266
  <thead>
263
267
  <tr>
264
268
  {seriesData.map((s, i) => (
265
- <th key={i} scope="col">{s.name}</th>
269
+ <th key={i} scope="col">
270
+ {s.name}
271
+ </th>
266
272
  ))}
267
273
  </tr>
268
274
  </thead>
269
275
  <tbody>
270
276
  {/* Render up to 20 rows to avoid overwhelming screen readers */}
271
- {Array.from({ length: Math.min(20, seriesData[0]?.data.length ?? 0) }).map((_, rowIdx) => (
272
- <tr key={rowIdx}>
273
- {seriesData.map((s, colIdx) => (
274
- <td key={colIdx}>{String(s.data[rowIdx] ?? '')}</td>
275
- ))}
276
- </tr>
277
- ))}
277
+ {Array.from({ length: Math.min(20, seriesData[0]?.data.length ?? 0) }).map(
278
+ (_, rowIdx) => (
279
+ <tr key={rowIdx}>
280
+ {seriesData.map((s, colIdx) => (
281
+ <td key={colIdx}>{String(s.data[rowIdx] ?? '')}</td>
282
+ ))}
283
+ </tr>
284
+ )
285
+ )}
278
286
  </tbody>
279
287
  </table>
280
288
 
281
289
  {/*
282
290
  Chart container with role="application" + keyboard navigation.
283
- role="application" tells assistive tech to pass through keyboard events.
291
+ role="application" tells assistive tech to pass through keyboard _events.
284
292
  aria-describedby links to the hidden data table above.
285
293
  */}
286
294
  <div
@@ -0,0 +1,17 @@
1
+ /**
2
+ * BaseChartWrapper 自动 mock
3
+ * 放在 __mocks__ 目录中,测试文件只需写 jest.mock('../common/BaseChartWrapper') 即可自动加载本 mock
4
+ */
5
+ import React from 'react';
6
+
7
+ const MockBaseChartWrapper = (props: Record<string, unknown>) => (
8
+ <div
9
+ data-testid="base-chart-wrapper"
10
+ className={`taroviz-${props.chartType as string} ${(props.className as string) || ''}`}
11
+ style={{ width: props.width as string, height: props.height as string }}
12
+ >
13
+ <div data-testid="chart-option">{JSON.stringify(props.option)}</div>
14
+ </div>
15
+ );
16
+
17
+ export default MockBaseChartWrapper;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 图表组件创建辅助函数
3
+ * 为每个图表类型生成带正确 Props 类型的组件
4
+ */
5
+
6
+ import React, { memo } from 'react';
7
+ import type { EChartsOption } from 'echarts';
8
+ import BaseChartWrapper from './common/BaseChartWrapper';
9
+ import type { BaseChartProps } from './types';
10
+
11
+ /**
12
+ * 创建图表组件
13
+ * @param displayName 组件名称
14
+ * @param chartType ECharts 图表类型
15
+ * @param needOptionCast 是否需要 option 类型转换(用于 boxplot、parallel 等)
16
+ * @returns 带 displayName 的 memo 组件
17
+ */
18
+ export function createChartComponent<P extends BaseChartProps = BaseChartProps>(
19
+ displayName: string,
20
+ chartType: string,
21
+ needOptionCast = false
22
+ ): React.FC<P> {
23
+ const Chart: React.FC<P> = memo((props) => (
24
+ <BaseChartWrapper
25
+ {...(props as unknown as BaseChartProps)}
26
+ option={
27
+ needOptionCast
28
+ ? ((props as { option?: EChartsOption }).option as EChartsOption)
29
+ : (props as unknown as BaseChartProps).option
30
+ }
31
+ chartType={chartType}
32
+ />
33
+ ));
34
+ Chart.displayName = displayName;
35
+ return Chart;
36
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Factory for charts that follow the buildXxxOption + render pattern.
3
+ * Eliminates the repeated null-check + BaseChart wrapper code.
4
+ */
5
+ import React from 'react';
6
+ import BaseChart from '@/core/components/BaseChart';
7
+ import type { BaseChartProps } from './types';
8
+
9
+ interface OptionBuilderProps {
10
+ optionMerge?: Record<string, unknown>;
11
+ }
12
+
13
+ /**
14
+ * Creates a chart component from a buildOption function.
15
+ * @param displayName Component display name
16
+ * @param buildOption Function that builds ECharts option from props, returns null if invalid
17
+ */
18
+ export function createOptionChartComponent<Props extends OptionBuilderProps>(
19
+ displayName: string,
20
+ buildOption: (props: Props) => Record<string, unknown> | null
21
+ ): React.FC<Props & Omit<BaseChartProps, 'option' | 'data'>> {
22
+ const Component: React.FC<Props & Omit<BaseChartProps, 'option' | 'data'>> = (props) => {
23
+ const { optionMerge, ...rest } = props;
24
+ const option = buildOption(props as Props);
25
+ if (!option) {
26
+ return null;
27
+ }
28
+ return <BaseChart option={option as any} {...(rest as BaseChartProps)} />;
29
+ };
30
+ Component.displayName = displayName;
31
+ return Component;
32
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * FunnelChart 组件测试
3
+ */
4
+ import React from 'react';
5
+ import { render, screen } from '@testing-library/react';
6
+ import '@testing-library/jest-dom';
7
+ import FunnelChart from '../index';
8
+
9
+ // 使用正确的 mock 方式,参考 parallel 图表的测试
10
+ jest.mock('../../common/BaseChartWrapper');
11
+ jest.mock('echarts/charts', () => ({ FunnelChart: jest.fn() }));
12
+
13
+ describe('FunnelChart', () => {
14
+ const mockData = [
15
+ { value: 100, name: '访问' },
16
+ { value: 80, name: '咨询' },
17
+ { value: 60, name: '订单' },
18
+ { value: 40, name: '成交' },
19
+ { value: 20, name: '退款' },
20
+ ];
21
+
22
+ it('应该渲染漏斗图组件', () => {
23
+ render(<FunnelChart data={mockData} width={600} height={400} />);
24
+
25
+ expect(screen.getByTestId('base-chart-wrapper')).toBeInTheDocument();
26
+ });
27
+
28
+ it('应该传递正确的 option 到 BaseChart', () => {
29
+ render(<FunnelChart data={mockData} width={600} height={400} />);
30
+
31
+ const baseChartWrapper = screen.getByTestId('base-chart-wrapper');
32
+ const optionElement = baseChartWrapper.querySelector('[data-testid="chart-option"]');
33
+ const option = JSON.parse(optionElement?.textContent || '{}');
34
+
35
+ expect(option.series).toBeDefined();
36
+ expect(option.series.type).toBe('funnel');
37
+ expect(option.series.data).toEqual(mockData);
38
+ expect(option.series.sort).toBe('descending');
39
+ });
40
+
41
+ it('当 data 为空时应该返回 null', () => {
42
+ const { container } = render(<FunnelChart data={[]} width={600} height={400} />);
43
+
44
+ expect(container.firstChild).toBeNull();
45
+ });
46
+
47
+ it('应该支持自定义 sort 排序', () => {
48
+ render(<FunnelChart data={mockData} sort="ascending" width={600} height={400} />);
49
+
50
+ const baseChartWrapper = screen.getByTestId('base-chart-wrapper');
51
+ const optionElement = baseChartWrapper.querySelector('[data-testid="chart-option"]');
52
+ const option = JSON.parse(optionElement?.textContent || '{}');
53
+
54
+ expect(option.series.sort).toBe('ascending');
55
+ });
56
+
57
+ it('应该支持自定义 align 对齐', () => {
58
+ render(<FunnelChart data={mockData} align="center" width={600} height={400} />);
59
+
60
+ const baseChartWrapper = screen.getByTestId('base-chart-wrapper');
61
+ const optionElement = baseChartWrapper.querySelector('[data-testid="chart-option"]');
62
+ const option = JSON.parse(optionElement?.textContent || '{}');
63
+
64
+ expect(option.series.align).toBe('center');
65
+ });
66
+
67
+ it('应该支持自定义 gap 间隙', () => {
68
+ render(<FunnelChart data={mockData} gap={10} width={600} height={400} />);
69
+
70
+ const baseChartWrapper = screen.getByTestId('base-chart-wrapper');
71
+ const optionElement = baseChartWrapper.querySelector('[data-testid="chart-option"]');
72
+ const option = JSON.parse(optionElement?.textContent || '{}');
73
+
74
+ expect(option.series.gap).toBe(10);
75
+ });
76
+
77
+ it('应该支持自定义 min/max 高度', () => {
78
+ render(<FunnelChart data={mockData} min={10} max={120} width={600} height={400} />);
79
+
80
+ const baseChartWrapper = screen.getByTestId('base-chart-wrapper');
81
+ const optionElement = baseChartWrapper.querySelector('[data-testid="chart-option"]');
82
+ const option = JSON.parse(optionElement?.textContent || '{}');
83
+
84
+ expect(option.series.min).toBe(10);
85
+ expect(option.series.max).toBe(120);
86
+ });
87
+
88
+ it('应该支持 optionMerge 自定义配置', () => {
89
+ const customTitle = { title: { text: '漏斗图标题', left: 'center' } };
90
+
91
+ render(<FunnelChart data={mockData} optionMerge={customTitle} width={600} height={400} />);
92
+
93
+ const baseChartWrapper = screen.getByTestId('base-chart-wrapper');
94
+ const optionElement = baseChartWrapper.querySelector('[data-testid="chart-option"]');
95
+ const option = JSON.parse(optionElement?.textContent || '{}');
96
+
97
+ expect(option.title).toEqual(customTitle.title);
98
+ });
99
+ });
@@ -1,14 +1,64 @@
1
1
  /**
2
- * FunnelChart组件
2
+ * TaroViz 漏斗图组件
3
+ *
4
+ * 基于 ECharts funnel 系列实现漏斗图可视化
3
5
  */
4
- import React, { memo } from 'react';
5
- import BaseChartWrapper from '../common/BaseChartWrapper';
6
- import { FunnelChartProps } from '../types';
7
- import '@/core/echarts';
8
-
9
- const FunnelChart: React.FC<FunnelChartProps> = memo((props) => (
10
- <BaseChartWrapper {...props} chartType="funnel-chart" />
11
- ));
12
- FunnelChart.displayName = 'FunnelChart';
6
+ import { createOptionChartComponent } from '@/charts/createOptionChartComponent';
7
+ import type { FunnelChartProps } from './types';
8
+ // 类型 FunnelDataItem 通过下方 export type 导出供外部使用
9
+
10
+ /**
11
+ * 构建漏斗图 ECharts option
12
+ */
13
+ function buildFunnelOption(props: FunnelChartProps) {
14
+ const { data, sort, align, gap, min, max, optionMerge } = props;
15
+
16
+ // 验证数据
17
+ if (!data || data.length === 0) {
18
+ console.warn('[TaroViz] FunnelChart: data is required');
19
+ return null;
20
+ }
21
+
22
+ // 构建漏斗图 series
23
+ const series = {
24
+ type: 'funnel' as const,
25
+ data: data,
26
+ sort: sort || 'descending',
27
+ align: align || 'left',
28
+ gap: gap || 2,
29
+ min: min || 0,
30
+ max: max || 100,
31
+ label: {
32
+ show: true,
33
+ position: 'inside',
34
+ },
35
+ emphasis: {
36
+ itemStyle: {
37
+ shadowBlur: 10,
38
+ shadowColor: 'rgba(0, 0, 0, 0.5)',
39
+ },
40
+ },
41
+ };
42
+
43
+ const option: any = {
44
+ tooltip: {
45
+ trigger: 'item',
46
+ formatter: (params: any) => {
47
+ if (!params || !params.data) return '';
48
+ return `<b>${params.data.name}</b><br/>值: ${params.data.value}`;
49
+ },
50
+ },
51
+ series,
52
+ };
53
+
54
+ // 合并自定义配置
55
+ if (optionMerge) {
56
+ Object.assign(option, optionMerge);
57
+ }
58
+
59
+ return option;
60
+ }
61
+
62
+ const FunnelChart = createOptionChartComponent<FunnelChartProps>('FunnelChart', buildFunnelOption);
13
63
 
14
64
  export default FunnelChart;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * TaroViz 漏斗图类型定义
3
+ *
4
+ * 从主 types 文件导入基础类型,只导出组件特有的类型
5
+ */
6
+ export type { FunnelChartProps, FunnelDataItem, FunnelLabel } from '../types';
@@ -1,47 +1,116 @@
1
1
  /**
2
- * @version v1.5.0
2
+ * GraphChart 组件测试
3
3
  */
4
-
5
4
  import React from 'react';
6
- import { render } from '@testing-library/react';
5
+ import { render, screen } from '@testing-library/react';
6
+ import '@testing-library/jest-dom';
7
7
  import GraphChart from '../index';
8
8
 
9
+ // 使用正确的 mock 方式,参考 parallel 图表的测试
10
+ jest.mock('../../common/BaseChartWrapper');
11
+ jest.mock('echarts/charts', () => ({ GraphChart: jest.fn() }));
12
+
9
13
  describe('GraphChart', () => {
10
- it('renders without crashing', () => {
11
- const { container } = render(<GraphChart />);
12
- expect(container).toBeTruthy();
14
+ const mockNodes = [
15
+ { id: '1', name: '节点 A', value: 10 },
16
+ { id: '2', name: '节点 B', value: 20 },
17
+ { id: '3', name: '节点 C', value: 30 },
18
+ { id: '4', name: '节点 D', value: 40 },
19
+ ];
20
+
21
+ const mockLinks = [
22
+ { source: '1', target: '2', value: 5 },
23
+ { source: '2', target: '3', value: 8 },
24
+ { source: '3', target: '4', value: 12 },
25
+ ];
26
+
27
+ it('应该渲染关系图组件', () => {
28
+ render(<GraphChart nodes={mockNodes} links={mockLinks} width={600} height={400} />);
29
+
30
+ expect(screen.getByTestId('base-chart-wrapper')).toBeInTheDocument();
31
+ });
32
+
33
+ it('应该传递正确的 option 到 BaseChart', () => {
34
+ render(<GraphChart nodes={mockNodes} links={mockLinks} width={600} height={400} />);
35
+
36
+ const baseChartWrapper = screen.getByTestId('base-chart-wrapper');
37
+ const optionElement = baseChartWrapper.querySelector('[data-testid="chart-option"]');
38
+ const option = JSON.parse(optionElement?.textContent || '{}');
39
+
40
+ expect(option.series).toBeDefined();
41
+ expect(option.series.type).toBe('graph');
42
+ expect(option.series.data).toEqual(mockNodes);
43
+ expect(option.series.links).toEqual(mockLinks);
13
44
  });
14
45
 
15
- it('renders with custom className', () => {
16
- const { container } = render(<GraphChart className="test-graph" />);
17
- // BaseChartWrapper renders a fragment with a hidden accessibility table (first child)
18
- // and the actual chart div (last child), so we query the last child for className
19
- const chartDiv = container.lastChild;
20
- expect(chartDiv).toHaveClass('test-graph');
46
+ it(' nodes 为空时应该返回 null', () => {
47
+ const { container } = render(
48
+ <GraphChart nodes={[]} links={mockLinks} width={600} height={400} />
49
+ );
50
+
51
+ expect(container.firstChild).toBeNull();
21
52
  });
22
53
 
23
- it('renders with custom width and height', () => {
24
- const { container } = render(<GraphChart width={500} height={400} />);
25
- // BaseChartWrapper renders a fragment with a hidden accessibility table (first child)
26
- // and the actual chart div (last child), so we query the last child for styles
27
- const chartDiv = container.lastChild;
28
- expect(chartDiv).toHaveStyle({ width: '500px', height: '400px' });
54
+ it('应该支持自定义 layout', () => {
55
+ render(
56
+ <GraphChart nodes={mockNodes} links={mockLinks} layout="circular" width={600} height={400} />
57
+ );
58
+
59
+ const baseChartWrapper = screen.getByTestId('base-chart-wrapper');
60
+ const optionElement = baseChartWrapper.querySelector('[data-testid="chart-option"]');
61
+ const option = JSON.parse(optionElement?.textContent || '{}');
62
+
63
+ expect(option.series.layout).toBe('circular');
29
64
  });
30
65
 
31
- it('renders with basic option', () => {
32
- const option = {
33
- series: [
34
- {
35
- type: 'graph' as const,
36
- nodes: [
37
- { id: '1', name: 'Node 1' },
38
- { id: '2', name: 'Node 2' },
39
- ],
40
- links: [{ source: '1', target: '2' }],
41
- },
42
- ],
43
- };
44
- const { container } = render(<GraphChart option={option} />);
45
- expect(container).toBeTruthy();
66
+ it('应该支持自定义 force 配置', () => {
67
+ render(
68
+ <GraphChart
69
+ nodes={mockNodes}
70
+ links={mockLinks}
71
+ layout="force"
72
+ force={{ repulsion: 200, edgeLength: 100 }}
73
+ width={600}
74
+ height={400}
75
+ />
76
+ );
77
+
78
+ const baseChartWrapper = screen.getByTestId('base-chart-wrapper');
79
+ const optionElement = baseChartWrapper.querySelector('[data-testid="chart-option"]');
80
+ const option = JSON.parse(optionElement?.textContent || '{}');
81
+
82
+ expect(option.series.force).toEqual({ repulsion: 200, edgeLength: 100 });
83
+ });
84
+
85
+ it('应该支持 draggable 配置', () => {
86
+ render(
87
+ <GraphChart nodes={mockNodes} links={mockLinks} draggable={true} width={600} height={400} />
88
+ );
89
+
90
+ const baseChartWrapper = screen.getByTestId('base-chart-wrapper');
91
+ const optionElement = baseChartWrapper.querySelector('[data-testid="chart-option"]');
92
+ const option = JSON.parse(optionElement?.textContent || '{}');
93
+
94
+ expect(option.series.draggable).toBe(true);
95
+ });
96
+
97
+ it('应该支持 optionMerge 自定义配置', () => {
98
+ const customTitle = { title: { text: '关系图标题', left: 'center' } };
99
+
100
+ render(
101
+ <GraphChart
102
+ nodes={mockNodes}
103
+ links={mockLinks}
104
+ optionMerge={customTitle}
105
+ width={600}
106
+ height={400}
107
+ />
108
+ );
109
+
110
+ const baseChartWrapper = screen.getByTestId('base-chart-wrapper');
111
+ const optionElement = baseChartWrapper.querySelector('[data-testid="chart-option"]');
112
+ const option = JSON.parse(optionElement?.textContent || '{}');
113
+
114
+ expect(option.title).toEqual(customTitle.title);
46
115
  });
47
116
  });