Skip to content

Commit 647c82e

Browse files
committed
feat(echarts): sankey
1 parent e7ba60e commit 647c82e

File tree

16 files changed

+850
-21
lines changed

16 files changed

+850
-21
lines changed

packages/react-charts/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/next
2+
/components

packages/react-charts/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
"main": "dist/js/index.js",
66
"module": "dist/esm/index.js",
77
"types": "dist/esm/index.d.ts",
8+
"typesVersions": {
9+
"*": {
10+
"next": [
11+
"dist/esm/next/index.d.ts"
12+
]
13+
}
14+
},
815
"patternfly:src": "src/",
916
"sideEffects": [
1017
"*.css",
@@ -53,6 +60,7 @@
5360
"victory-zoom-container": "^37.0.2"
5461
},
5562
"peerDependencies": {
63+
"echarts": "^5.5.1",
5664
"react": "^17 || ^18",
5765
"react-dom": "^17 || ^18"
5866
},
@@ -61,6 +69,7 @@
6169
"build:single:packages": "node ../../scripts/build-single-packages.mjs --config single-packages.config.json"
6270
},
6371
"devDependencies": {
72+
"@types/echarts": "^4.9.22",
6473
"@types/lodash": "^4.17.5",
6574
"fs-extra": "^11.2.0"
6675
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"packageName": "@patternfly/react-charts",
3-
"exclude": ["dist/esm/deprecated/index.js", "dist/esm/next/index.js"]
3+
"exclude": ["dist/esm/deprecated/index.js"]
44
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import * as React from 'react';
2+
// import * as echarts from 'echarts';
3+
import { render } from '@testing-library/react';
4+
import { Sankey } from './Sankey';
5+
6+
const data = [
7+
{
8+
name: 'a'
9+
},
10+
{
11+
name: 'b'
12+
},
13+
{
14+
name: 'a1'
15+
},
16+
{
17+
name: 'a2'
18+
},
19+
{
20+
name: 'b1'
21+
},
22+
{
23+
name: 'c'
24+
}
25+
];
26+
27+
const links = [
28+
{
29+
source: 'a',
30+
target: 'a1',
31+
value: 5
32+
},
33+
{
34+
source: 'a',
35+
target: 'a2',
36+
value: 3
37+
},
38+
{
39+
source: 'b',
40+
target: 'b1',
41+
value: 8
42+
},
43+
{
44+
source: 'a',
45+
target: 'b1',
46+
value: 3
47+
},
48+
{
49+
source: 'b1',
50+
target: 'a1',
51+
value: 1
52+
},
53+
{
54+
source: 'b1',
55+
target: 'c',
56+
value: 2
57+
}
58+
];
59+
60+
let spy: any;
61+
62+
// beforeAll(() => {
63+
// console.log(`*** TEST 1`);
64+
// spy = jest.spyOn(echarts, 'getInstanceByDom').mockImplementation(
65+
// () =>
66+
// ({
67+
// hideLoading: jest.fn(),
68+
// setOption: jest.fn(),
69+
// showLoading: jest.fn()
70+
// }) as any
71+
// );
72+
// });
73+
//
74+
// afterAll(() => {
75+
// console.log(`*** TEST 2`);
76+
// spy.mockRestore();
77+
// });
78+
79+
// See https://stackoverflow.com/questions/54921743/testing-echarts-react-component-with-jest-echartelement-is-null
80+
xtest('renders component data', () => {
81+
const { asFragment } = render(<Sankey opts={{ renderer: 'svg' }} series={[{ data, links }]} />);
82+
expect(asFragment()).toMatchSnapshot();
83+
});
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/* eslint-disable camelcase */
2+
import chart_voronoi_flyout_stroke_Fill from '@patternfly/react-tokens/dist/esm/chart_voronoi_flyout_stroke_Fill';
3+
import chart_voronoi_labels_Fill from '@patternfly/react-tokens/dist/esm/chart_voronoi_labels_Fill';
4+
5+
import * as React from 'react';
6+
import * as echarts from 'echarts';
7+
import { useRef } from 'react';
8+
import defaultsDeep from 'lodash/defaultsDeep';
9+
10+
// import { BarChart, SankeyChart } from 'echarts/charts';
11+
// import { CanvasRenderer } from 'echarts/renderers';
12+
13+
// import {
14+
// TitleComponent,
15+
// TooltipComponent,
16+
// GridComponent,
17+
// DatasetComponent,
18+
// TransformComponent
19+
// } from 'echarts/components';
20+
21+
// Register the required components
22+
// echarts.use([
23+
// BarChart,
24+
// SankeyChart,
25+
// TitleComponent,
26+
// TooltipComponent,
27+
// GridComponent,
28+
// DatasetComponent,
29+
// TransformComponent,
30+
// LabelLayout,
31+
// UniversalTransition,
32+
// CanvasRenderer
33+
// ]);
34+
35+
import { theme as chartTheme } from './theme';
36+
37+
/**
38+
*/
39+
export interface SankeyProps {
40+
destinationLabel?: string;
41+
height?: number;
42+
id?: string;
43+
legend?: {
44+
symbolSize?: number; // Todo: move into tooltip?
45+
};
46+
lineStyle?: any;
47+
opts?: any;
48+
series: any[];
49+
sourceLabel?: string;
50+
theme?: any;
51+
title?: any;
52+
tooltip?: any;
53+
width?: number;
54+
}
55+
56+
export const Sankey: React.FunctionComponent<SankeyProps> = ({
57+
destinationLabel = 'Destination',
58+
height,
59+
id,
60+
legend = {
61+
symbolSize: 10
62+
},
63+
lineStyle = {
64+
color: 'source',
65+
opacity: 0.6
66+
},
67+
opts,
68+
series,
69+
sourceLabel = 'Source',
70+
theme = chartTheme,
71+
title,
72+
tooltip = {
73+
valueFormatter: (value: number | string) => value
74+
},
75+
width
76+
}: SankeyProps) => {
77+
const containerRef = useRef<HTMLDivElement>();
78+
const echart = useRef<echarts.ECharts>();
79+
80+
const getItemColor = (params: any) => {
81+
const serie = series[params.seriesIndex];
82+
const sourceData = serie?.data.find((datum: any) => datum.name === params.data?.source);
83+
const targetData = serie?.data.find((datum: any) => datum.name === params.data?.target);
84+
const sourceColor = sourceData?.itemStyle?.color;
85+
const targetColor = targetData?.itemStyle?.color;
86+
return { sourceColor, targetColor };
87+
};
88+
89+
const getTooltip = () => {
90+
const symbolSize = `${legend.symbolSize}px`;
91+
const defaults = {
92+
backgroundColor: chart_voronoi_flyout_stroke_Fill.value,
93+
confine: true,
94+
formatter: (params: any) => {
95+
const result = `
96+
<div style="display: inline-block; background-color: ${params.color}; height: ${symbolSize}; width: ${symbolSize};"></div>
97+
${params.name} ${params.value}
98+
`;
99+
if (params.data.source && params.data.target) {
100+
const { sourceColor, targetColor } = getItemColor(params);
101+
return `
102+
<p>${sourceLabel}</p>
103+
<div style="display: inline-block; background-color: ${sourceColor}; height: ${symbolSize}; width: ${symbolSize};"></div>
104+
${params.data.source}
105+
<p style="padding-top: 10px;">${destinationLabel}</p>
106+
<p style="text-align:left;">
107+
<div style="display: inline-block; background-color: ${targetColor}; height: ${symbolSize}; width: ${symbolSize};"></div>
108+
${params.data.target}
109+
<strong style="float:right;">
110+
${tooltip.valueFormatter(params.value, params.dataIndex)}
111+
</strong>
112+
</p>
113+
`;
114+
}
115+
return result.replace(/\s\s+/g, ' ');
116+
},
117+
textStyle: {
118+
color: chart_voronoi_labels_Fill.value
119+
},
120+
trigger: 'item',
121+
triggerOn: 'mousemove'
122+
};
123+
return defaultsDeep(tooltip, defaults);
124+
};
125+
126+
React.useEffect(() => {
127+
echarts.registerTheme('pf-v5-sankey', theme);
128+
echart.current = echarts.init(containerRef.current, 'pf-v5-sankey', opts); // renderer: 'svg'
129+
130+
const newSeries = series.map((serie: any) => {
131+
const defaults = {
132+
data: serie.data.map((datum: any, index: number) => ({
133+
itemStyle: {
134+
color: theme?.color[index % theme?.color.length]
135+
}
136+
})),
137+
emphasis: {
138+
focus: 'adjacency'
139+
},
140+
layout: 'none',
141+
lineStyle,
142+
type: 'sankey'
143+
};
144+
return defaultsDeep(serie, defaults);
145+
});
146+
147+
echart.current?.setOption({
148+
series: newSeries,
149+
title,
150+
tooltip: getTooltip()
151+
});
152+
153+
return () => {
154+
echart.current?.dispose();
155+
};
156+
}, [containerRef, lineStyle, opts, series, title, tooltip]);
157+
158+
React.useEffect(() => {
159+
echart.current?.resize();
160+
}, [height, width]);
161+
162+
const getSize = () => ({
163+
...(height && { height: `${height}px` }),
164+
...(width && { width: `${width}px` })
165+
});
166+
167+
return <div id={id} ref={containerRef} style={getSize()} />;
168+
};
169+
Sankey.displayName = 'Sankey';
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React from 'react';
2+
import { Sankey } from '@patternfly/react-charts/next';
3+
import { getResizeObserver } from '@patternfly/react-core';
4+
5+
export const FormBasic: React.FunctionComponent = () => {
6+
const data = [
7+
{
8+
name: 'a'
9+
},
10+
{
11+
name: 'b'
12+
},
13+
{
14+
name: 'a1'
15+
},
16+
{
17+
name: 'a2'
18+
},
19+
{
20+
name: 'b1'
21+
},
22+
{
23+
name: 'c'
24+
}
25+
];
26+
27+
const links = [
28+
{
29+
source: 'a',
30+
target: 'a1',
31+
value: 5
32+
},
33+
{
34+
source: 'a',
35+
target: 'a2',
36+
value: 3
37+
},
38+
{
39+
source: 'b',
40+
target: 'b1',
41+
value: 8
42+
},
43+
{
44+
source: 'a',
45+
target: 'b1',
46+
value: 3
47+
},
48+
{
49+
source: 'b1',
50+
target: 'a1',
51+
value: 1
52+
},
53+
{
54+
source: 'b1',
55+
target: 'c',
56+
value: 2
57+
}
58+
];
59+
60+
// let observer = () => {};
61+
const containerRef = React.useRef<HTMLDivElement>();
62+
const [width, setWidth] = React.useState(0);
63+
64+
React.useEffect(() => {
65+
const handleResize = () => {
66+
if (containerRef.current && containerRef.current.clientWidth) {
67+
setWidth(containerRef.current.clientWidth);
68+
}
69+
};
70+
let observer = () => {};
71+
observer = getResizeObserver(containerRef.current, handleResize);
72+
73+
return () => {
74+
observer();
75+
};
76+
}, [containerRef, width]);
77+
78+
return (
79+
<div ref={containerRef}>
80+
<Sankey
81+
height={400}
82+
series={[{ data, links }]}
83+
title={{
84+
subtext: 'This is a Sankey chart',
85+
left: 'center'
86+
}}
87+
tooltip={{
88+
valueFormatter: (value) => `${value} GiB`
89+
}}
90+
width={width}
91+
/>
92+
</div>
93+
);
94+
};

0 commit comments

Comments
 (0)