@agions/taroviz 1.11.5 → 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 +31 -46
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/vendors.js +1 -1
- package/dist/cjs/vendors~echarts.js +1 -1
- package/dist/esm/index.js +1 -14270
- package/dist/esm/vendors.js +1 -16770
- package/dist/esm/vendors~echarts.js +1 -59417
- package/package.json +10 -15
- package/src/adapters/h5/index.ts +38 -38
- package/src/adapters/index.ts +32 -34
- package/src/adapters/types.ts +23 -55
- package/src/charts/boxplot/types.ts +2 -2
- package/src/charts/common/BaseChartWrapper.tsx +9 -7
- package/src/charts/createChartComponent.tsx +9 -21
- package/src/charts/createOptionChartComponent.tsx +32 -0
- package/src/charts/funnel/__tests__/index.test.tsx +99 -0
- package/src/charts/funnel/index.tsx +64 -0
- package/src/charts/funnel/types.ts +6 -0
- package/src/charts/graph/__tests__/index.test.tsx +116 -0
- package/src/charts/graph/index.tsx +70 -0
- package/src/charts/graph/types.ts +6 -0
- package/src/charts/heatmap/__tests__/index.test.tsx +139 -0
- package/src/charts/heatmap/index.tsx +107 -0
- package/src/charts/heatmap/types.ts +6 -0
- package/src/charts/index.ts +47 -57
- package/src/charts/liquid/__tests__/index.test.tsx +52 -0
- package/src/charts/liquid/index.tsx +7 -133
- package/src/charts/liquid/types.ts +6 -6
- package/src/charts/parallel/types.ts +3 -3
- package/src/charts/radar/__tests__/index.test.tsx +210 -0
- package/src/charts/radar/index.tsx +147 -0
- package/src/charts/radar/types.ts +13 -0
- package/src/charts/sankey/__tests__/index.test.tsx +124 -0
- package/src/charts/sankey/index.tsx +70 -0
- package/src/charts/sankey/types.ts +6 -0
- package/src/charts/tree/__tests__/index.test.tsx +71 -0
- package/src/charts/tree/index.tsx +1 -1
- package/src/charts/tree/types.ts +8 -8
- package/src/charts/types.ts +208 -106
- package/src/charts/wordcloud/__tests__/index.test.tsx +106 -0
- package/src/charts/wordcloud/index.tsx +79 -0
- package/src/charts/wordcloud/types.ts +6 -0
- package/src/components/DataFilter/index.tsx +7 -6
- package/src/core/animation/types.ts +6 -6
- package/src/core/components/Annotation.tsx +6 -6
- package/src/core/components/BaseChart.tsx +97 -133
- package/src/core/components/LazyChart.tsx +3 -8
- package/src/core/components/hooks/index.ts +6 -2
- package/src/core/components/hooks/usePerformance.ts +8 -2
- package/src/core/components/hooks/useVirtualScroll.ts +2 -1
- package/src/core/types/common.ts +2 -1
- package/src/core/types/platform.ts +1 -0
- 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 -36
- package/src/core/utils/deepClone.ts +114 -0
- package/src/core/utils/download.ts +22 -28
- package/src/core/utils/drillDown.ts +1 -0
- package/src/core/utils/events.ts +12 -0
- package/src/core/utils/export/ExportUtils.ts +2 -1
- package/src/core/utils/format.ts +44 -0
- package/src/core/utils/index.ts +18 -159
- package/src/core/utils/merge.ts +25 -0
- package/src/core/utils/performance/PerformanceAnalyzer.ts +3 -1
- 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 +6 -5
- package/src/{hooks → core/utils/performance}/useDataZoom.ts +7 -2
- package/src/{hooks → core/utils/performance}/usePerformance.ts +39 -39
- package/src/{hooks → core/utils/performance}/usePerformanceHooks.ts +39 -39
- package/src/core/utils/runtime.ts +190 -0
- package/src/editor/components/ThemeSelector.tsx +3 -3
- package/src/hooks/chartConnectHelpers.ts +6 -0
- package/src/hooks/index.ts +54 -626
- package/src/hooks/types.ts +27 -0
- package/src/hooks/useChartAutoResize.ts +73 -0
- package/src/hooks/useChartConnect.ts +5 -1
- package/src/hooks/useChartDownload.ts +1 -1
- package/src/hooks/useChartHistory.ts +1 -3
- 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 +23 -12
- package/src/hooks/useChartTheme.ts +51 -0
- package/src/hooks/useDataTransform.ts +19 -4
- package/src/index.ts +5 -10
- 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/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 -449
- package/src/core/utils/debug/DebugPanel.tsx +0 -640
- 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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaroViz 漏斗图组件
|
|
3
|
+
*
|
|
4
|
+
* 基于 ECharts funnel 系列实现漏斗图可视化
|
|
5
|
+
*/
|
|
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);
|
|
63
|
+
|
|
64
|
+
export default FunnelChart;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphChart 组件测试
|
|
3
|
+
*/
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { render, screen } from '@testing-library/react';
|
|
6
|
+
import '@testing-library/jest-dom';
|
|
7
|
+
import GraphChart from '../index';
|
|
8
|
+
|
|
9
|
+
// 使用正确的 mock 方式,参考 parallel 图表的测试
|
|
10
|
+
jest.mock('../../common/BaseChartWrapper');
|
|
11
|
+
jest.mock('echarts/charts', () => ({ GraphChart: jest.fn() }));
|
|
12
|
+
|
|
13
|
+
describe('GraphChart', () => {
|
|
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);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('当 nodes 为空时应该返回 null', () => {
|
|
47
|
+
const { container } = render(
|
|
48
|
+
<GraphChart nodes={[]} links={mockLinks} width={600} height={400} />
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(container.firstChild).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
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');
|
|
64
|
+
});
|
|
65
|
+
|
|
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);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaroViz 关系图组件
|
|
3
|
+
*
|
|
4
|
+
* 基于 ECharts graph 系列实现关系图/力导向图可视化
|
|
5
|
+
*/
|
|
6
|
+
import { createOptionChartComponent } from '@/charts/createOptionChartComponent';
|
|
7
|
+
import type { GraphChartProps } from './types';
|
|
8
|
+
// 类型 GraphNode、GraphLink 通过下方 export type 导出供外部使用
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 构建关系图 ECharts option
|
|
12
|
+
*/
|
|
13
|
+
function buildGraphOption(props: GraphChartProps) {
|
|
14
|
+
const { nodes, links, layout, force, draggable, optionMerge } = props;
|
|
15
|
+
|
|
16
|
+
// 验证数据
|
|
17
|
+
if (!nodes || nodes.length === 0) {
|
|
18
|
+
console.warn('[TaroViz] GraphChart: nodes is required');
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 构建关系图 series
|
|
23
|
+
const series: any = {
|
|
24
|
+
type: 'graph' as const,
|
|
25
|
+
layout: layout || 'force',
|
|
26
|
+
data: nodes,
|
|
27
|
+
links: links || [],
|
|
28
|
+
draggable: draggable !== false,
|
|
29
|
+
label: {
|
|
30
|
+
show: true,
|
|
31
|
+
position: 'right',
|
|
32
|
+
},
|
|
33
|
+
emphasis: {
|
|
34
|
+
focus: 'adjacency',
|
|
35
|
+
lineStyle: {
|
|
36
|
+
width: 4,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// 添加力导向布局配置
|
|
42
|
+
if (layout === 'force' && force) {
|
|
43
|
+
series.force = force;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const option: any = {
|
|
47
|
+
tooltip: {
|
|
48
|
+
trigger: 'item',
|
|
49
|
+
formatter: (params: any) => {
|
|
50
|
+
if (!params || !params.data) return '';
|
|
51
|
+
return `<b>${params.data.name}</b>`;
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
legend: {
|
|
55
|
+
data: nodes.map((n) => n.name),
|
|
56
|
+
},
|
|
57
|
+
series,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// 合并自定义配置
|
|
61
|
+
if (optionMerge) {
|
|
62
|
+
Object.assign(option, optionMerge);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return option;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const GraphChart = createOptionChartComponent<GraphChartProps>('GraphChart', buildGraphOption);
|
|
69
|
+
|
|
70
|
+
export default GraphChart;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HeatmapChart 组件测试
|
|
3
|
+
*/
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { render, screen } from '@testing-library/react';
|
|
6
|
+
import '@testing-library/jest-dom';
|
|
7
|
+
import HeatmapChart from '../index';
|
|
8
|
+
|
|
9
|
+
// 使用正确的 mock 方式,参考 parallel 图表的测试
|
|
10
|
+
jest.mock('../../common/BaseChartWrapper');
|
|
11
|
+
jest.mock('echarts/charts', () => ({ HeatmapChart: jest.fn() }));
|
|
12
|
+
|
|
13
|
+
describe('HeatmapChart', () => {
|
|
14
|
+
const mockXData = ['周一', '周二', '周三', '周四', '周五'];
|
|
15
|
+
const mockYData = ['A', 'B', 'C', 'D'];
|
|
16
|
+
const mockData = [
|
|
17
|
+
{ x: 0, y: 0, value: 10 },
|
|
18
|
+
{ x: 1, y: 0, value: 20 },
|
|
19
|
+
{ x: 2, y: 0, value: 30 },
|
|
20
|
+
{ x: 3, y: 0, value: 40 },
|
|
21
|
+
{ x: 4, y: 0, value: 50 },
|
|
22
|
+
{ x: 0, y: 1, value: 15 },
|
|
23
|
+
{ x: 1, y: 1, value: 25 },
|
|
24
|
+
{ x: 2, y: 1, value: 35 },
|
|
25
|
+
{ x: 3, y: 1, value: 45 },
|
|
26
|
+
{ x: 4, y: 1, value: 55 },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
it('应该渲染热力图组件', () => {
|
|
30
|
+
render(
|
|
31
|
+
<HeatmapChart xData={mockXData} yData={mockYData} data={mockData} width={600} height={400} />
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(screen.getByTestId('base-chart-wrapper')).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('应该传递正确的 option 到 BaseChart', () => {
|
|
38
|
+
render(
|
|
39
|
+
<HeatmapChart xData={mockXData} yData={mockYData} data={mockData} width={600} height={400} />
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const baseChartWrapper = screen.getByTestId('base-chart-wrapper');
|
|
43
|
+
const optionElement = baseChartWrapper.querySelector('[data-testid="chart-option"]');
|
|
44
|
+
const option = JSON.parse(optionElement?.textContent || '{}');
|
|
45
|
+
|
|
46
|
+
expect(option.xAxis).toBeDefined();
|
|
47
|
+
expect(option.xAxis.data).toEqual(mockXData);
|
|
48
|
+
expect(option.yAxis).toBeDefined();
|
|
49
|
+
expect(option.yAxis.data).toEqual(mockYData);
|
|
50
|
+
expect(option.series).toBeDefined();
|
|
51
|
+
expect(option.series.type).toBe('heatmap');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('当 xData 为空时应该返回 null', () => {
|
|
55
|
+
const { container } = render(
|
|
56
|
+
<HeatmapChart xData={[]} yData={mockYData} data={mockData} width={600} height={400} />
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(container.firstChild).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('当 yData 为空时应该返回 null', () => {
|
|
63
|
+
const { container } = render(
|
|
64
|
+
<HeatmapChart xData={mockXData} yData={[]} data={mockData} width={600} height={400} />
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(container.firstChild).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('当 data 为空时应该返回 null', () => {
|
|
71
|
+
const { container } = render(
|
|
72
|
+
<HeatmapChart xData={mockXData} yData={mockYData} data={[]} width={600} height={400} />
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(container.firstChild).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('应该支持自定义 visualMap', () => {
|
|
79
|
+
render(
|
|
80
|
+
<HeatmapChart
|
|
81
|
+
xData={mockXData}
|
|
82
|
+
yData={mockYData}
|
|
83
|
+
data={mockData}
|
|
84
|
+
visualMap={{
|
|
85
|
+
min: 5,
|
|
86
|
+
max: 60,
|
|
87
|
+
orient: 'vertical',
|
|
88
|
+
right: 10,
|
|
89
|
+
top: 'center',
|
|
90
|
+
}}
|
|
91
|
+
width={600}
|
|
92
|
+
height={400}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const baseChartWrapper = screen.getByTestId('base-chart-wrapper');
|
|
97
|
+
const optionElement = baseChartWrapper.querySelector('[data-testid="chart-option"]');
|
|
98
|
+
const option = JSON.parse(optionElement?.textContent || '{}');
|
|
99
|
+
|
|
100
|
+
expect(option.visualMap.min).toBe(5);
|
|
101
|
+
expect(option.visualMap.max).toBe(60);
|
|
102
|
+
expect(option.visualMap.orient).toBe('vertical');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('应该支持 optionMerge 自定义配置', () => {
|
|
106
|
+
const customTitle = { title: { text: '热力图标题', left: 'center' } };
|
|
107
|
+
|
|
108
|
+
render(
|
|
109
|
+
<HeatmapChart
|
|
110
|
+
xData={mockXData}
|
|
111
|
+
yData={mockYData}
|
|
112
|
+
data={mockData}
|
|
113
|
+
optionMerge={customTitle}
|
|
114
|
+
width={600}
|
|
115
|
+
height={400}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const baseChartWrapper = screen.getByTestId('base-chart-wrapper');
|
|
120
|
+
const optionElement = baseChartWrapper.querySelector('[data-testid="chart-option"]');
|
|
121
|
+
const option = JSON.parse(optionElement?.textContent || '{}');
|
|
122
|
+
|
|
123
|
+
expect(option.title).toEqual(customTitle.title);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('应该正确转换数据格式', () => {
|
|
127
|
+
render(
|
|
128
|
+
<HeatmapChart xData={mockXData} yData={mockYData} data={mockData} width={600} height={400} />
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const baseChartWrapper = screen.getByTestId('base-chart-wrapper');
|
|
132
|
+
const optionElement = baseChartWrapper.querySelector('[data-testid="chart-option"]');
|
|
133
|
+
const option = JSON.parse(optionElement?.textContent || '{}');
|
|
134
|
+
|
|
135
|
+
// 验证数据被正确转换为 [x, y, value] 格式
|
|
136
|
+
expect(option.series.data[0]).toEqual([0, 0, 10]);
|
|
137
|
+
expect(option.series.data[4]).toEqual([4, 0, 50]);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaroViz 热力图组件
|
|
3
|
+
*
|
|
4
|
+
* 基于 ECharts heatmap 系列实现二维数据密度可视化
|
|
5
|
+
*/
|
|
6
|
+
import { createOptionChartComponent } from '@/charts/createOptionChartComponent';
|
|
7
|
+
import type { HeatmapChartProps } from './types';
|
|
8
|
+
// 类型 HeatmapDataItem、HeatmapAxis 通过下方 export type 导出供外部使用
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 构建热力图 ECharts option
|
|
12
|
+
*/
|
|
13
|
+
function buildHeatmapOption(props: HeatmapChartProps) {
|
|
14
|
+
const { xData, yData, data, visualMap, optionMerge } = props;
|
|
15
|
+
|
|
16
|
+
// 验证数据
|
|
17
|
+
if (!xData || xData.length === 0) {
|
|
18
|
+
console.warn('[TaroViz] HeatmapChart: xData is required');
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!yData || yData.length === 0) {
|
|
23
|
+
console.warn('[TaroViz] HeatmapChart: yData is required');
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!data || data.length === 0) {
|
|
28
|
+
console.warn('[TaroViz] HeatmapChart: data is required');
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 构建热力图 series
|
|
33
|
+
const series = {
|
|
34
|
+
type: 'heatmap' as const,
|
|
35
|
+
data: data.map((item) => [item.x, item.y, item.value]),
|
|
36
|
+
label: {
|
|
37
|
+
show: true,
|
|
38
|
+
formatter: (params: any) => {
|
|
39
|
+
if (!params || params.value?.length !== 3) return '';
|
|
40
|
+
return params.value[2];
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
emphasis: {
|
|
44
|
+
itemStyle: {
|
|
45
|
+
shadowBlur: 10,
|
|
46
|
+
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// 构建坐标轴
|
|
52
|
+
const xAxis: any = {
|
|
53
|
+
type: 'category',
|
|
54
|
+
data: xData,
|
|
55
|
+
axisLabel: {
|
|
56
|
+
rotate: xData.length > 10 ? 45 : 0,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const yAxis: any = {
|
|
61
|
+
type: 'category',
|
|
62
|
+
data: yData,
|
|
63
|
+
axisLabel: {
|
|
64
|
+
rotate: yData.length > 10 ? 45 : 0,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// 构建 visualMap
|
|
69
|
+
const visualMapConfig = visualMap || {
|
|
70
|
+
min: 0,
|
|
71
|
+
max: Math.max(...data.map((d) => d.value)),
|
|
72
|
+
calculable: true,
|
|
73
|
+
orient: 'horizontal',
|
|
74
|
+
left: 'center',
|
|
75
|
+
top: 'bottom',
|
|
76
|
+
text: ['高', '低'],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const option: any = {
|
|
80
|
+
tooltip: {
|
|
81
|
+
position: 'top',
|
|
82
|
+
formatter: (params: any) => {
|
|
83
|
+
if (!params || !params.data) return '';
|
|
84
|
+
const [x, y, value] = params.data;
|
|
85
|
+
return `<b>${x} × ${y}</b><br/>值: ${value}`;
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
xAxis,
|
|
89
|
+
yAxis,
|
|
90
|
+
series,
|
|
91
|
+
visualMap: visualMapConfig,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// 合并自定义配置
|
|
95
|
+
if (optionMerge) {
|
|
96
|
+
Object.assign(option, optionMerge);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return option;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const HeatmapChart = createOptionChartComponent<HeatmapChartProps>(
|
|
103
|
+
'HeatmapChart',
|
|
104
|
+
buildHeatmapOption
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
export default HeatmapChart;
|