Skip to content

Commit

Permalink
feat(charts): sankey
Browse files Browse the repository at this point in the history
  • Loading branch information
dlabrecq committed Sep 11, 2024
1 parent 4a815fb commit 2735536
Show file tree
Hide file tree
Showing 16 changed files with 852 additions and 7 deletions.
2 changes: 2 additions & 0 deletions packages/react-charts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/next
/components
14 changes: 12 additions & 2 deletions packages/react-charts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
"main": "dist/js/index.js",
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"typesVersions": {
"*": {
"next": [
"dist/esm/next/index.d.ts"
]
}
},
"patternfly:src": "src/",
"sideEffects": [
"*.css",
Expand Down Expand Up @@ -53,14 +60,17 @@
"victory-zoom-container": "^37.1.1"
},
"peerDependencies": {
"echarts": "^5.5.1",
"react": "^17 || ^18",
"react-dom": "^17 || ^18"
},
"scripts": {
"clean": "rimraf dist",
"build:single:packages": "node ../../scripts/build-single-packages.mjs --config single-packages.config.json"
"clean": "rimraf dist components next",
"build:single:packages": "node ../../scripts/build-single-packages.mjs --config single-packages.config.json",
"subpaths": "node ../../scripts/exportSubpaths.mjs --config subpaths.config.json"
},
"devDependencies": {
"@types/echarts": "^4.9.22",
"@types/lodash": "^4.17.7",
"fs-extra": "^11.2.0"
}
Expand Down
2 changes: 1 addition & 1 deletion packages/react-charts/single-packages.config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"packageName": "@patternfly/react-charts",
"exclude": ["dist/esm/deprecated/index.js", "dist/esm/next/index.js"]
"exclude": ["dist/esm/deprecated/index.js"]
}
83 changes: 83 additions & 0 deletions packages/react-charts/src/next/components/Sankey/Sankey.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as React from 'react';
// import * as echarts from 'echarts';
import { render } from '@testing-library/react';
import { Sankey } from './Sankey';

const data = [
{
name: 'a'
},
{
name: 'b'
},
{
name: 'a1'
},
{
name: 'a2'
},
{
name: 'b1'
},
{
name: 'c'
}
];

const links = [
{
source: 'a',
target: 'a1',
value: 5
},
{
source: 'a',
target: 'a2',
value: 3
},
{
source: 'b',
target: 'b1',
value: 8
},
{
source: 'a',
target: 'b1',
value: 3
},
{
source: 'b1',
target: 'a1',
value: 1
},
{
source: 'b1',
target: 'c',
value: 2
}
];

let spy: any;

// beforeAll(() => {
// console.log(`*** TEST 1`);
// spy = jest.spyOn(echarts, 'getInstanceByDom').mockImplementation(
// () =>
// ({
// hideLoading: jest.fn(),
// setOption: jest.fn(),
// showLoading: jest.fn()
// }) as any
// );
// });
//
// afterAll(() => {
// console.log(`*** TEST 2`);
// spy.mockRestore();
// });

// See https://stackoverflow.com/questions/54921743/testing-echarts-react-component-with-jest-echartelement-is-null
xtest('renders component data', () => {
const { asFragment } = render(<Sankey opts={{ renderer: 'svg' }} series={[{ data, links }]} />);
expect(asFragment()).toMatchSnapshot();
});
169 changes: 169 additions & 0 deletions packages/react-charts/src/next/components/Sankey/Sankey.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/* eslint-disable camelcase */
import chart_voronoi_flyout_stroke_Fill from '@patternfly/react-tokens/dist/esm/chart_voronoi_flyout_stroke_Fill';
import chart_voronoi_labels_Fill from '@patternfly/react-tokens/dist/esm/chart_voronoi_labels_Fill';

import * as React from 'react';
import * as echarts from 'echarts';
import { useRef } from 'react';
import defaultsDeep from 'lodash/defaultsDeep';

// import { BarChart, SankeyChart } from 'echarts/charts';
// import { CanvasRenderer } from 'echarts/renderers';

// import {
// TitleComponent,
// TooltipComponent,
// GridComponent,
// DatasetComponent,
// TransformComponent
// } from 'echarts/components';

// Register the required components
// echarts.use([
// BarChart,
// SankeyChart,
// TitleComponent,
// TooltipComponent,
// GridComponent,
// DatasetComponent,
// TransformComponent,
// LabelLayout,
// UniversalTransition,
// CanvasRenderer
// ]);

import { theme as chartTheme } from './theme';

/**
*/
export interface SankeyProps {
destinationLabel?: string;
height?: number;
id?: string;
legend?: {
symbolSize?: number; // Todo: move into tooltip?
};
lineStyle?: any;
opts?: any;
series: any[];
sourceLabel?: string;
theme?: any;
title?: any;
tooltip?: any;
width?: number;
}

export const Sankey: React.FunctionComponent<SankeyProps> = ({
destinationLabel = 'Destination',
height,
id,
legend = {
symbolSize: 10
},
lineStyle = {
color: 'source',
opacity: 0.6
},
opts,
series,
sourceLabel = 'Source',
theme = chartTheme,
title,
tooltip = {
valueFormatter: (value: number | string) => value
},
width
}: SankeyProps) => {
const containerRef = useRef<HTMLDivElement>();
const echart = useRef<echarts.ECharts>();

const getItemColor = (params: any) => {
const serie = series[params.seriesIndex];
const sourceData = serie?.data.find((datum: any) => datum.name === params.data?.source);
const targetData = serie?.data.find((datum: any) => datum.name === params.data?.target);
const sourceColor = sourceData?.itemStyle?.color;
const targetColor = targetData?.itemStyle?.color;
return { sourceColor, targetColor };
};

const getTooltip = () => {
const symbolSize = `${legend.symbolSize}px`;
const defaults = {
backgroundColor: chart_voronoi_flyout_stroke_Fill.value,
confine: true,
formatter: (params: any) => {
const result = `
<div style="display: inline-block; background-color: ${params.color}; height: ${symbolSize}; width: ${symbolSize};"></div>
${params.name} ${params.value}
`;
if (params.data.source && params.data.target) {
const { sourceColor, targetColor } = getItemColor(params);
return `
<p>${sourceLabel}</p>
<div style="display: inline-block; background-color: ${sourceColor}; height: ${symbolSize}; width: ${symbolSize};"></div>
${params.data.source}
<p style="padding-top: 10px;">${destinationLabel}</p>
<p style="text-align:left;">
<div style="display: inline-block; background-color: ${targetColor}; height: ${symbolSize}; width: ${symbolSize};"></div>
${params.data.target}
<strong style="float:right;">
${tooltip.valueFormatter(params.value, params.dataIndex)}
</strong>
</p>
`;
}
return result.replace(/\s\s+/g, ' ');
},
textStyle: {
color: chart_voronoi_labels_Fill.value
},
trigger: 'item',
triggerOn: 'mousemove'
};
return defaultsDeep(tooltip, defaults);
};

React.useEffect(() => {
echarts.registerTheme('pf-v5-sankey', theme);
echart.current = echarts.init(containerRef.current, 'pf-v5-sankey', opts); // renderer: 'svg'

const newSeries = series.map((serie: any) => {
const defaults = {
data: serie.data.map((datum: any, index: number) => ({
itemStyle: {
color: theme?.color[index % theme?.color.length]
}
})),
emphasis: {
focus: 'adjacency'
},
layout: 'none',
lineStyle,
type: 'sankey'
};
return defaultsDeep(serie, defaults);
});

echart.current?.setOption({
series: newSeries,
title,
tooltip: getTooltip()
});

return () => {
echart.current?.dispose();
};
}, [containerRef, lineStyle, opts, series, title, tooltip]);

Check warning on line 156 in packages/react-charts/src/next/components/Sankey/Sankey.tsx

View workflow job for this annotation

GitHub Actions / Lint

React Hook React.useEffect has missing dependencies: 'getTooltip' and 'theme'. Either include them or remove the dependency array

React.useEffect(() => {
echart.current?.resize();
}, [height, width]);

const getSize = () => ({
...(height && { height: `${height}px` }),
...(width && { width: `${width}px` })
});

return <div id={id} ref={containerRef} style={getSize()} />;
};
Sankey.displayName = 'Sankey';
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React from 'react';
import { Sankey } from '@patternfly/react-charts/next';
import { getResizeObserver } from '@patternfly/react-core';

export const FormBasic: React.FunctionComponent = () => {
const data = [
{
name: 'a'
},
{
name: 'b'
},
{
name: 'a1'
},
{
name: 'a2'
},
{
name: 'b1'
},
{
name: 'c'
}
];

const links = [
{
source: 'a',
target: 'a1',
value: 5
},
{
source: 'a',
target: 'a2',
value: 3
},
{
source: 'b',
target: 'b1',
value: 8
},
{
source: 'a',
target: 'b1',
value: 3
},
{
source: 'b1',
target: 'a1',
value: 1
},
{
source: 'b1',
target: 'c',
value: 2
}
];

// let observer = () => {};
const containerRef = React.useRef<HTMLDivElement>();
const [width, setWidth] = React.useState(0);

React.useEffect(() => {
const handleResize = () => {
if (containerRef.current && containerRef.current.clientWidth) {
setWidth(containerRef.current.clientWidth);
}
};
let observer = () => {};
observer = getResizeObserver(containerRef.current, handleResize);

return () => {
observer();
};
}, [containerRef, width]);

return (
<div ref={containerRef}>
<Sankey
height={400}
series={[{ data, links }]}
title={{
subtext: 'This is a Sankey chart',
left: 'center'
}}
tooltip={{
valueFormatter: (value) => `${value} GiB`
}}
width={width}
/>
</div>
);
};
Loading

0 comments on commit 2735536

Please sign in to comment.