Skip to content

Commit f2479b1

Browse files
authored
feat(charts): Sankey and Line charts, based on Apache ECharts (#11616)
1 parent 239a4c5 commit f2479b1

File tree

75 files changed

+4193
-168
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+4193
-168
lines changed

jest.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ const config: Config = {
1919
'^.+\\.m?[jt]sx?$': 'babel-jest',
2020
'^.+\\.svg$': 'jest-transform-stub'
2121
},
22-
setupFilesAfterEnv: ['<rootDir>/packages/testSetup.ts'],
22+
setupFilesAfterEnv: ['<rootDir>/packages/testSetup.ts', 'jest-canvas-mock'],
2323
testPathIgnorePatterns: ['<rootDir>/packages/react-integration/'],
24-
transformIgnorePatterns: ['node_modules/victory-*/', '/node_modules/(?!(case-anything)/)'],
24+
transformIgnorePatterns: ['/node_modules/victory-*/', '/node_modules/(?!(case-anything|echarts|zrender)/)'],
2525
coveragePathIgnorePatterns: ['/dist/'],
2626
moduleNameMapper: {
2727
'\\.(css|less)$': '<rootDir>/packages/react-styles/__mocks__/styleMock.js'

packages/react-charts/package.json

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
"types": "dist/esm/index.d.ts",
88
"typesVersions": {
99
"*": {
10+
"echarts": [
11+
"dist/esm/echarts/index.d.ts"
12+
],
1013
"victory": [
1114
"dist/esm/victory/index.d.ts"
1215
]
@@ -42,7 +45,13 @@
4245
"lodash": "^4.17.21",
4346
"tslib": "^2.8.1"
4447
},
48+
"devDependencies": {
49+
"@types/lodash": "^4.17.16",
50+
"fs-extra": "^11.3.0",
51+
"jest-canvas-mock": "^2.5.2"
52+
},
4553
"peerDependencies": {
54+
"echarts": "^5.6.0",
4655
"react": "^17 || ^18",
4756
"react-dom": "^17 || ^18",
4857
"victory-area": "^37.3.6",
@@ -64,6 +73,9 @@
6473
"victory-zoom-container": "^37.3.6"
6574
},
6675
"peerDependenciesMeta": {
76+
"echarts": {
77+
"optional": true
78+
},
6779
"victory-area": {
6880
"optional": true
6981
},
@@ -120,9 +132,5 @@
120132
"clean": "rimraf dist echarts victory",
121133
"build:single:packages": "node ../../scripts/build-single-packages.mjs --config single-packages.config.json",
122134
"subpaths": "node ../../scripts/exportSubpaths.mjs --config subpaths.config.json"
123-
},
124-
"devDependencies": {
125-
"@types/lodash": "^4.17.16",
126-
"fs-extra": "^11.3.0"
127135
}
128136
}
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/echarts/deprecated/index.js", "dist/esm/echarts/next/index.js", "dist/esm/victory/deprecated/index.js", "dist/esm/victory/next/index.js"]
44
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const echarts: any = jest.createMockFromModule('echarts');
2+
echarts.init = jest.fn(() => ({
3+
setOption: jest.fn(),
4+
dispose: jest.fn()
5+
}));
6+
module.exports = echarts;
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import { FunctionComponent, useEffect } from 'react';
2+
import { useCallback, useReducer, useRef } from 'react';
3+
import cloneDeep from 'lodash/cloneDeep';
4+
import defaultsDeep from 'lodash/defaultsDeep';
5+
6+
import * as echarts from 'echarts/core';
7+
import { EChartsInitOpts } from 'echarts/types/dist/echarts';
8+
import { EChartsOption } from 'echarts/types/dist/option';
9+
import { TooltipOption } from 'echarts/types/dist/shared';
10+
11+
import { getLineSeries } from '../Line';
12+
import { getSankeySeries } from '../Sankey';
13+
14+
import { ThemeDefinition } from '../themes/Theme';
15+
import { getMutationObserver } from '../utils/observe';
16+
import { getClassName } from '../utils/styles';
17+
import { getTheme } from '../utils/theme';
18+
import { getLegendTooltip, getSankeyTooltip } from '../utils/tooltip';
19+
import { ThemeColor } from '../themes/ThemeColor';
20+
21+
/**
22+
* See https://echarts.apache.org/en/option.html#tooltip
23+
*
24+
* @public
25+
* @beta
26+
*/
27+
export interface TooltipOptionProps extends TooltipOption {
28+
/**
29+
* The destination label shown in the tooltip -- for Sankey only
30+
*/
31+
destinationLabel?: string;
32+
/**
33+
* The source label shown in the tooltip -- for Sankey only
34+
*/
35+
sourceLabel?: string;
36+
}
37+
38+
/**
39+
* See https://echarts.apache.org/en/option.html
40+
*
41+
* @public
42+
* @beta
43+
*/
44+
export interface ChartsOptionProps extends EChartsOption {
45+
/**
46+
* Tooltip component -- see https://echarts.apache.org/en/option.html#tooltip
47+
*/
48+
tooltip?: TooltipOptionProps | TooltipOptionProps[];
49+
}
50+
51+
/**
52+
* This component is based on the Apache ECharts chart library. It provides additional functionality, custom
53+
* components, and theming for PatternFly. This provides a collection of React based components you can use to build
54+
* PatternFly patterns with consistent markup, styling, and behavior.
55+
*
56+
* See https://echarts.apache.org/en/api.html#echarts
57+
*
58+
* @public
59+
* @beta
60+
*/
61+
export interface ChartsProps {
62+
/**
63+
* The className prop specifies a class name that will be applied to outermost element
64+
*/
65+
className?: string;
66+
/**
67+
* Specify height explicitly, in pixels
68+
*/
69+
height?: number;
70+
/**
71+
* The id prop specifies an ID that will be applied to outermost element.
72+
*/
73+
id?: string;
74+
/**
75+
* Flag indicating to use the legend tooltip (default). This may be overridden by the `option.tooltip` property.
76+
*/
77+
isLegendTooltip?: boolean;
78+
/**
79+
* Flag indicating to use the SVG renderer (default). This may be overridden by the `opts.renderer` property.
80+
*/
81+
isSvgRenderer?: boolean;
82+
/**
83+
* This creates a Mutation Observer to watch the given DOM selector.
84+
*
85+
* When the pf-v6-theme-dark selector is added or removed, this component will be notified to update its computed
86+
* theme styles. However, if the dark theme is not updated dynamically (e.g., via a toggle), there is no need to add
87+
* this Mutation Observer.
88+
*
89+
* Note: Don't provide ".pf-v6-theme-dark" as the node selector as it won't exist in the page for light theme.
90+
* The underlying querySelectorAll() function needs to find the element the dark theme selector will be added to.
91+
*
92+
* See https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Locating_DOM_elements_using_selectors
93+
*
94+
* @propType string
95+
* @example <Charts nodeSelector="html" />
96+
* @example <Charts nodeSelector="#main" />
97+
* @example <Charts nodeSelector=".chr-scope__default-layout" />
98+
*/
99+
nodeSelector?: string;
100+
/**
101+
* ECharts uses this object to configure its properties; for example, series, title, and tooltip
102+
*
103+
* See https://echarts.apache.org/en/option.html
104+
*/
105+
option?: ChartsOptionProps;
106+
/**
107+
* Optional chart configuration
108+
*
109+
* See https://echarts.apache.org/en/api.html#echarts.init
110+
*/
111+
opts?: EChartsInitOpts;
112+
/**
113+
* The theme prop specifies a theme to use for determining styles and layout properties for a component. Any styles or
114+
* props defined in theme may be overwritten by props specified on the component instance.
115+
*
116+
* See https://echarts.apache.org/handbook/en/concepts/style/#theme
117+
*/
118+
theme?: ThemeDefinition;
119+
/**
120+
* Specifies the theme color. Valid values are 'blue', 'green', 'multi', etc.
121+
*
122+
* Note: Not compatible with theme prop
123+
*
124+
* @example themeColor={ChartThemeColor.blue}
125+
*/
126+
themeColor?: string;
127+
/**
128+
* Specify width explicitly, in pixels
129+
*/
130+
width?: number;
131+
}
132+
export const Charts: FunctionComponent<ChartsProps> = ({
133+
className,
134+
height,
135+
id,
136+
isLegendTooltip = true,
137+
isSvgRenderer = true,
138+
nodeSelector,
139+
option,
140+
opts,
141+
theme,
142+
themeColor,
143+
width,
144+
...rest
145+
}: ChartsProps) => {
146+
const containerRef = useRef<HTMLDivElement>();
147+
const echart = useRef<echarts.ECharts>();
148+
const [update, forceUpdate] = useReducer((x) => x + 1, 0);
149+
150+
const getSize = () => ({
151+
...(height && { height: `${height}px` }),
152+
...(width && { width: `${width}px` })
153+
});
154+
155+
const getTooltip = useCallback(
156+
(series: any[], tooltipType: string, isSkeleton: boolean, echart) => {
157+
// Skeleton should not have any interactions
158+
if (isSkeleton) {
159+
return undefined;
160+
} else if (tooltipType === 'sankey') {
161+
return getSankeyTooltip(series, option);
162+
} else if (tooltipType === 'legend') {
163+
return getLegendTooltip(series, option, echart);
164+
}
165+
return option.tooltip;
166+
},
167+
[option]
168+
);
169+
170+
const getSeries = useCallback(
171+
(chartTheme: ThemeDefinition, isSkeleton: boolean) => {
172+
let tooltipType;
173+
const series: any = cloneDeep(option?.series);
174+
const newSeries = [];
175+
176+
series.map((serie: any) => {
177+
switch (serie.type) {
178+
case 'sankey':
179+
tooltipType = 'sankey'; // Overrides legend tooltip
180+
newSeries.push(getSankeySeries(serie, chartTheme, isSkeleton));
181+
break;
182+
case 'line':
183+
if (!tooltipType) {
184+
tooltipType = 'legend';
185+
}
186+
newSeries.push(getLineSeries(serie, chartTheme, isSkeleton));
187+
break;
188+
default:
189+
newSeries.push(serie);
190+
break;
191+
}
192+
});
193+
return { series, tooltipType };
194+
},
195+
[option?.series]
196+
);
197+
198+
useEffect(() => {
199+
const isSkeleton = themeColor === ThemeColor.skeleton;
200+
const chartTheme = theme ? theme : getTheme(themeColor);
201+
const renderer = isSvgRenderer ? 'svg' : 'canvas';
202+
203+
echart.current = echarts.init(
204+
containerRef.current,
205+
chartTheme,
206+
defaultsDeep(opts, { height, renderer, width }) // height and width are necessary here for unit tests
207+
);
208+
209+
const { series, tooltipType } = getSeries(chartTheme, isSkeleton);
210+
echart.current?.setOption({
211+
...option,
212+
...(isLegendTooltip && { tooltip: getTooltip(series, tooltipType, isSkeleton, echart.current) }),
213+
series
214+
});
215+
216+
return () => {
217+
echart.current?.dispose();
218+
};
219+
}, [
220+
containerRef,
221+
getSeries,
222+
getTooltip,
223+
height,
224+
isLegendTooltip,
225+
isSvgRenderer,
226+
option,
227+
opts,
228+
theme,
229+
themeColor,
230+
update,
231+
width
232+
]);
233+
234+
// Resize observer
235+
useEffect(() => {
236+
echart.current?.resize();
237+
}, [height, width]);
238+
239+
// Dark theme observer
240+
useEffect(() => {
241+
let observer = () => {};
242+
observer = getMutationObserver(nodeSelector, () => {
243+
forceUpdate();
244+
});
245+
return () => {
246+
observer();
247+
};
248+
}, [nodeSelector]);
249+
250+
return <div className={getClassName(className)} id={id} ref={containerRef} style={getSize()} {...rest} />;
251+
};
252+
Charts.displayName = 'Charts';

0 commit comments

Comments
 (0)