Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sankey chart #10960

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
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
Loading