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 30, 2024
1 parent 0d5bc49 commit b93b079
Show file tree
Hide file tree
Showing 16 changed files with 918 additions and 9 deletions.
6 changes: 5 additions & 1 deletion packages/react-charts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
"types": "dist/esm/index.d.ts",
"typesVersions": {
"*": {
"echarts": [
"dist/esm/echarts/index.d.ts"
],
"victory": [
"dist/esm/victory/index.d.ts"
]
Expand Down Expand Up @@ -43,6 +46,7 @@
"tslib": "^2.7.0"
},
"peerDependencies": {
"echarts": "^5.5.1",
"react": "^17 || ^18",
"react-dom": "^17 || ^18",
"victory-area": "^37.1.1",
Expand All @@ -69,7 +73,7 @@
"subpaths": "node ../../scripts/exportSubpaths.mjs --config subpaths.config.json"
},
"devDependencies": {
"@types/lodash": "^4.17.9",
"@types/lodash": "^4.17.7",
"fs-extra": "^11.2.0"
}
}
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"]
}
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();
});
210 changes: 210 additions & 0 deletions packages/react-charts/src/echarts/components/Sankey/Sankey.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/* 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';

Check failure on line 6 in packages/react-charts/src/echarts/components/Sankey/Sankey.tsx

View workflow job for this annotation

GitHub Actions / Build, test & deploy

Cannot find module 'echarts' or its corresponding type declarations.

Check failure on line 6 in packages/react-charts/src/echarts/components/Sankey/Sankey.tsx

View workflow job for this annotation

GitHub Actions / Build

Cannot find module 'echarts' or its corresponding type declarations.
import { useCallback, useRef, useState } from 'react';
import defaultsDeep from 'lodash/defaultsDeep';
import { getMutationObserver } from '../utils/observe';
import { getComputedStyle } from '../utils/theme';

// 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 { getTheme } from './theme';
import { getClassName } from '../utils/misc';

/**
*/
export interface SankeyProps {
className?: string;
destinationLabel?: string;
height?: number;
id?: string;
legend?: {
symbolSize?: number; // Todo: move into tooltip?
};
lineStyle?: any;

/**
* This creates a Mutation Observer to watch the given DOM selector.
*
* When the pf-v6-theme-dark selector is added or removed, this component will be notified to update its computed
* theme styles. However, if the dark theme is not updated dynamically (e.g., via a toggle), there is no need to add
* this Mutation Observer.
*
* Note: Don't provide ".pf-v6-theme-dark" as the node selector as it won't exist in the page for light theme.
* The underlying querySelectorAll() function needs to find the element the dark theme selector will be added to.
*
* See https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Locating_DOM_elements_using_selectors
*
* @propType string
* @example <Sankey nodeSelector="html" />
* @example <Sankey nodeSelector="#main" />
* @example <Sankey nodeSelector=".chr-scope__default-layout" />
*/
nodeSelector?: string;
opts?: any;
series: any[];
sourceLabel?: string;
theme?: any;
title?: any;
tooltip?: any;
width?: number;
}

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

const getItemColor = useCallback(
(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 };
},
[series]
);

const getTooltip = useCallback(() => {
const symbolSize = `${legend.symbolSize}px`;
const defaults = {
backgroundColor: getComputedStyle(chart_voronoi_flyout_stroke_Fill),
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: getComputedStyle(chart_voronoi_labels_Fill)
},
trigger: 'item',
triggerOn: 'mousemove'
};
return defaultsDeep(tooltip, defaults);
}, [destinationLabel, getItemColor, legend.symbolSize, sourceLabel, tooltip]);

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

const newSeries = series.map((serie: any) => {
const defaults = {
data: serie.data.map((datum: any, index: number) => ({
itemStyle: {
color: chartTheme?.color[index % chartTheme?.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();
};
}, [chartTheme, containerRef, getTooltip, lineStyle, opts, series, title, tooltip]);

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

// Dark theme observer
React.useEffect(() => {
let observer = () => {};
observer = getMutationObserver(nodeSelector, () => {
setChartTheme(getTheme());
});
return () => {
observer();
};
}, [nodeSelector]);

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

return <div className={getClassName(className)} id={id} ref={containerRef} style={getSize()} />;
};
Sankey.displayName = 'Sankey';
Loading

0 comments on commit b93b079

Please sign in to comment.