@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.
- package/CHANGELOG.md +245 -0
- package/README.md +104 -302
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/vendors.js +1 -0
- package/dist/cjs/vendors~echarts.js +1 -0
- package/dist/esm/index.js +1 -58151
- package/dist/esm/vendors.js +1 -0
- package/dist/esm/vendors~echarts.js +1 -0
- package/package.json +19 -25
- package/src/adapters/MiniAppAdapter.ts +136 -0
- package/src/adapters/__tests__/index.test.ts +1 -1
- package/src/adapters/h5/__tests__/index.test.ts +4 -2
- package/src/adapters/h5/index.ts +63 -64
- package/src/adapters/harmony/index.ts +23 -245
- package/src/adapters/index.ts +49 -45
- package/src/adapters/swan/index.ts +6 -69
- package/src/adapters/tt/index.ts +7 -70
- package/src/adapters/types.ts +25 -58
- package/src/adapters/weapp/index.ts +6 -69
- package/src/charts/__tests__/testUtils.tsx +87 -0
- package/src/charts/boxplot/__tests__/index.test.tsx +49 -103
- package/src/charts/boxplot/index.tsx +2 -1
- package/src/charts/boxplot/types.ts +17 -16
- package/src/charts/common/BaseChartWrapper.tsx +90 -82
- package/src/charts/common/__mocks__/BaseChartWrapper.tsx +17 -0
- package/src/charts/createChartComponent.tsx +36 -0
- package/src/charts/createOptionChartComponent.tsx +32 -0
- package/src/charts/funnel/__tests__/index.test.tsx +99 -0
- package/src/charts/funnel/index.tsx +60 -10
- package/src/charts/funnel/types.ts +6 -0
- package/src/charts/graph/__tests__/index.test.tsx +102 -33
- package/src/charts/graph/index.tsx +66 -9
- package/src/charts/graph/types.ts +6 -0
- package/src/charts/heatmap/__tests__/index.test.tsx +139 -0
- package/src/charts/heatmap/index.tsx +103 -10
- package/src/charts/heatmap/types.ts +6 -0
- package/src/charts/index.ts +74 -26
- package/src/charts/liquid/__tests__/index.test.tsx +52 -0
- package/src/charts/liquid/index.tsx +239 -182
- package/src/charts/liquid/types.ts +11 -11
- package/src/charts/parallel/__tests__/index.test.tsx +40 -67
- package/src/charts/parallel/index.tsx +2 -1
- package/src/charts/parallel/types.ts +19 -18
- package/src/charts/radar/__tests__/index.test.tsx +210 -0
- package/src/charts/radar/index.tsx +143 -10
- package/src/charts/radar/types.ts +13 -0
- package/src/charts/sankey/__tests__/index.test.tsx +124 -0
- package/src/charts/sankey/index.tsx +62 -10
- package/src/charts/sankey/types.ts +6 -0
- package/src/charts/tree/__tests__/index.test.tsx +71 -0
- package/src/charts/tree/index.tsx +5 -2
- package/src/charts/tree/types.ts +9 -9
- package/src/charts/types.ts +208 -106
- package/src/charts/utils.ts +9 -7
- package/src/charts/wordcloud/__tests__/index.test.tsx +98 -31
- package/src/charts/wordcloud/index.tsx +75 -9
- package/src/charts/wordcloud/types.ts +6 -0
- package/src/components/DataFilter/index.tsx +32 -10
- package/src/core/animation/types.ts +6 -6
- package/src/core/components/Annotation.tsx +6 -7
- package/src/core/components/BaseChart.tsx +110 -168
- package/src/core/components/ErrorBoundary.tsx +17 -4
- package/src/core/components/LazyChart.tsx +54 -55
- package/src/core/components/hooks/index.ts +6 -2
- package/src/core/components/hooks/useChartInit.ts +6 -3
- package/src/core/components/hooks/usePerformance.ts +8 -2
- package/src/core/components/hooks/useVirtualScroll.ts +2 -1
- package/src/core/index.ts +1 -1
- package/src/core/themes/ThemeManager.ts +1 -1
- package/src/core/types/common.ts +2 -1
- package/src/core/types/index.ts +0 -12
- package/src/core/types/platform.ts +3 -5
- package/src/core/utils/__tests__/deepClone.test.ts +317 -0
- package/src/core/utils/__tests__/index.test.ts +2 -1
- package/src/core/utils/chartInstances.ts +13 -0
- package/src/core/utils/common.ts +20 -29
- package/src/core/utils/deepClone.ts +114 -0
- package/src/core/utils/download.ts +128 -0
- package/src/core/utils/drillDown.ts +34 -353
- package/src/core/utils/drillDownHelpers.ts +426 -0
- package/src/core/utils/events.ts +12 -0
- package/src/core/utils/export/ExportUtils.ts +36 -67
- package/src/core/utils/format.ts +44 -0
- package/src/core/utils/index.ts +21 -154
- package/src/core/utils/merge.ts +25 -0
- package/src/core/utils/performance/PerformanceAnalyzer.ts +38 -21
- package/src/core/utils/performance/hooks.ts +7 -0
- package/src/core/utils/performance/index.ts +2 -0
- package/src/{hooks → core/utils/performance}/useAnimation.ts +45 -41
- package/src/core/utils/performance/useDataZoom.ts +324 -0
- package/src/{hooks → core/utils/performance}/usePerformance.ts +49 -41
- package/src/core/utils/performance/usePerformanceHooks.ts +278 -0
- package/src/core/utils/performanceUtils.ts +310 -0
- package/src/core/utils/runtime.ts +190 -0
- package/src/core/utils/setOptionUtils.ts +59 -0
- package/src/core/version.ts +14 -0
- package/src/editor/EnhancedThemeEditor.tsx +362 -540
- package/src/editor/ThemeEditor.tsx +55 -321
- package/src/editor/components/ThemeBasicSettings.tsx +113 -0
- package/src/editor/components/ThemeColorEditor.tsx +105 -0
- package/src/editor/components/ThemeSelector.tsx +70 -0
- package/src/editor/hooks/useThemeEditorState.ts +201 -0
- package/src/editor/index.ts +10 -2
- package/src/hooks/__tests__/index.test.tsx +3 -1
- package/src/hooks/chartConnectHelpers.ts +341 -0
- package/src/hooks/index.ts +55 -660
- package/src/hooks/types.ts +189 -0
- package/src/hooks/useChartAutoResize.ts +73 -0
- package/src/hooks/useChartConnect.ts +92 -238
- package/src/hooks/useChartDownload.ts +25 -27
- package/src/hooks/useChartHistory.ts +34 -49
- package/src/hooks/useChartInit.ts +59 -0
- package/src/hooks/useChartOptions.ts +259 -0
- package/src/hooks/useChartPerformance.ts +109 -0
- package/src/hooks/useChartSelection.ts +52 -49
- package/src/hooks/useChartTheme.ts +51 -0
- package/src/hooks/useDataTransform.ts +19 -4
- package/src/hooks/utils/chartDownloadUtils.ts +40 -53
- package/src/hooks/utils/dataTransformUtils.ts +22 -0
- package/src/index.ts +48 -34
- package/src/main.tsx +4 -9
- package/src/react-dom.d.ts +3 -3
- package/src/themes/index.ts +30 -855
- package/src/themes/palettes/blue-green.ts +13 -0
- package/src/themes/palettes/chalk.ts +13 -0
- package/src/themes/palettes/cyber.ts +44 -0
- package/src/themes/palettes/dark.ts +52 -0
- package/src/themes/palettes/default.ts +52 -0
- package/src/themes/palettes/elegant.ts +34 -0
- package/src/themes/palettes/forest.ts +13 -0
- package/src/themes/palettes/glass.ts +49 -0
- package/src/themes/palettes/golden.ts +13 -0
- package/src/themes/palettes/neon.ts +43 -0
- package/src/themes/palettes/ocean.ts +39 -0
- package/src/themes/palettes/pastel.ts +37 -0
- package/src/themes/palettes/purple-passion.ts +13 -0
- package/src/themes/palettes/retro.ts +33 -0
- package/src/themes/palettes/sunset.ts +40 -0
- package/src/themes/palettes/walden.ts +13 -0
- package/src/themes/registry.ts +184 -0
- package/src/themes/types.ts +213 -0
- package/src/charts/bar/__tests__/index.test.tsx +0 -113
- package/src/charts/bar/index.tsx +0 -14
- package/src/charts/candlestick/__tests__/index.test.tsx +0 -40
- package/src/charts/candlestick/index.tsx +0 -13
- package/src/charts/gauge/index.tsx +0 -14
- package/src/charts/line/__tests__/index.test.tsx +0 -107
- package/src/charts/line/index.tsx +0 -15
- package/src/charts/pie/__tests__/index.test.tsx +0 -112
- package/src/charts/pie/index.tsx +0 -14
- package/src/charts/scatter/index.tsx +0 -14
- package/src/charts/sunburst/index.tsx +0 -18
- package/src/charts/treemap/index.tsx +0 -18
- package/src/core/utils/codeGenerator/CodeGenerator.ts +0 -669
- package/src/core/utils/codeGenerator/index.ts +0 -13
- package/src/core/utils/codeGenerator/types.ts +0 -198
- package/src/core/utils/configGenerator/ConfigGenerator.ts +0 -583
- package/src/core/utils/configGenerator/index.ts +0 -13
- package/src/core/utils/configGenerator/types.ts +0 -445
- package/src/core/utils/debug/DebugPanel.tsx +0 -637
- package/src/core/utils/debug/debugger.ts +0 -322
- package/src/core/utils/debug/index.ts +0 -21
- package/src/core/utils/debug/types.ts +0 -142
- 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;
|
|
40
|
-
const PAN_STEP = 10;
|
|
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();
|
|
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
|
|
68
|
-
const handleKeyDown = useCallback(
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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(([
|
|
168
|
-
(
|
|
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
|
-
|
|
193
|
+
adapterRef.dispose?.();
|
|
194
|
+
const instance = chartInstance.current;
|
|
195
|
+
if (instance) {
|
|
192
196
|
if (onEvents) {
|
|
193
|
-
Object.entries(onEvents).forEach(([
|
|
194
|
-
(
|
|
197
|
+
Object.entries(onEvents).forEach(([_eventName]) => {
|
|
198
|
+
(instance as unknown as { off: (_event: string) => void }).off(_eventName);
|
|
195
199
|
});
|
|
196
200
|
}
|
|
197
|
-
|
|
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">
|
|
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(
|
|
272
|
-
|
|
273
|
-
{
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
|
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
|
-
*
|
|
2
|
+
* TaroViz 漏斗图组件
|
|
3
|
+
*
|
|
4
|
+
* 基于 ECharts funnel 系列实现漏斗图可视化
|
|
3
5
|
*/
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
)
|
|
12
|
-
|
|
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;
|
|
@@ -1,47 +1,116 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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('
|
|
16
|
-
const { container } = render(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
expect(
|
|
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('
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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('
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
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
|
});
|