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
4 changes: 4 additions & 0 deletions 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.8.1"
},
"peerDependencies": {
"echarts": "^5.5.1",
"react": "^17 || ^18",
"react-dom": "^17 || ^18",
"victory-area": "^37.3.2",
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"]
}
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();
});
251 changes: 251 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,251 @@
/* 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';
import { EChartsInitOpts } from 'echarts/types/dist/echarts';

/**
* Sankey diagram is a specific type of streamgraph (can also be seen as a directed acyclic graph) in which the width
* of each branch is shown proportionally to the flow quantity. These graphs are typically used to visualize energy or
* material or cost transfers between processes. They can also visualize the energy accounts, material flow accounts
* on a regional or national level, and also the breakdown of cost of item or services.
*/
export interface SankeyProps {
/**
* The className prop specifies a class name that will be applied to outermost element
*/
className?: string;
/**
* Specify height explicitly, in pixels
*/
height?: number;
/**
* The id prop specifies an ID that will be applied to outermost element.
*/
id?: string;
/**
* 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;
/**
* Optional chart configuration
*
* See https://echarts.apache.org/en/api.html#echarts.init
*/
opts?: EChartsInitOpts;
/**
* Series component properties
*
* See https://echarts.apache.org/en/option.html#series-sankey
*/
series: any[];
/**
* The theme prop specifies a theme to use for determining styles and layout properties for a component. Any styles or
* props defined in theme may be overwritten by props specified on the component instance.
*
* See https://echarts.apache.org/handbook/en/concepts/style/#theme
*/
theme?: any; // Todo: Add theme type
/**
* Title component properties
*
* See https://echarts.apache.org/en/option.html#title
*/
title?: any;
/**
* Tooltip component properties
*
* See https://echarts.apache.org/en/option.html#tooltip
*/
tooltip?: any;
/**
* This is the destination label shown in the tooltip
*/
tooltipDestinationLabel?: string;
/**
* The source label shown in the tooltip
*/
tooltipSourceLabel?: string;
/**
* Specify width explicitly, in pixels
*/
width?: number;
}

export const Sankey: React.FunctionComponent<SankeyProps> = ({
className,
height,
id,
nodeSelector,
opts,
series,

theme,
title,
tooltip = {
valueFormatter: (value: number | string) => value
},
tooltipDestinationLabel = 'Destination',
tooltipSourceLabel = 'Source',
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 getSize = () => ({
...(height && { height: `${height}px` }),
...(width && { width: `${width}px` })
});

const getTooltip = useCallback(() => {
const symbolSize = '10px';
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>${tooltipSourceLabel}</p>
<div style="display: inline-block; background-color: ${sourceColor}; height: ${symbolSize}; width: ${symbolSize};"></div>
${params.data.source}
<p style="padding-top: 10px;">${tooltipDestinationLabel}</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);
}, [getItemColor, tooltipDestinationLabel, tooltipSourceLabel, tooltip]);

React.useEffect(() => {
echarts.registerTheme('pf-sankey', chartTheme);
echart.current = echarts.init(containerRef.current, 'pf-v5-sankey', defaultsDeep(opts, { 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: {
color: 'source',
opacity: 0.6
},
type: 'sankey'
};
return defaultsDeep(serie, defaults);
});

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

return () => {
echart.current?.dispose();
};
}, [chartTheme, containerRef, getTooltip, 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]);

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