Skip to content

Commit

Permalink
Charting:Add New Sankey chart to charting package (microsoft#13982)
Browse files Browse the repository at this point in the history
* Add  Sankey chart to charting package

* Expose pathColor prop

* Change files

* Add d3-sankey dependency

* Fix tslint

* Update yarn.lock file

* Fix eslint errors

* Fix eslint errors

* Update JSX.Element to  React.Reactnode

Co-authored-by: Rajesh Goriga <v-gorraj@microsoft.com>
  • Loading branch information
RajeshGoriga and Rajesh Goriga committed Jul 16, 2020
1 parent 06b7832 commit a92d7dc
Show file tree
Hide file tree
Showing 14 changed files with 526 additions and 0 deletions.
@@ -0,0 +1,8 @@
{
"type": "minor",
"comment": "Charting:Add New Sankey chart to charting package",
"packageName": "@uifabric/charting",
"email": "v-gorraj@microsoft.com",
"dependentChangeType": "patch",
"date": "2020-07-10T06:24:33.752Z"
}
2 changes: 2 additions & 0 deletions packages/charting/package.json
Expand Up @@ -52,6 +52,7 @@
"@types/d3-array": "1.2.1",
"@types/d3-axis": "1.0.10",
"@types/d3-format": "^1.3.1",
"@types/d3-sankey": "^0.11.0",
"@types/d3-scale": "2.0.0",
"@types/d3-selection": "1.4.1",
"@types/d3-shape": "^1.2.3",
Expand All @@ -61,6 +62,7 @@
"d3-array": "1.2.1",
"d3-axis": "1.0.8",
"d3-format": "^1.4.4",
"d3-sankey": "^0.12.3",
"d3-scale": "2.0.0",
"d3-selection": "1.3.0",
"d3-shape": "^1.2.0",
Expand Down
1 change: 1 addition & 0 deletions packages/charting/src/SankeyChart.ts
@@ -0,0 +1 @@
export * from './components/SankeyChart/index';
156 changes: 156 additions & 0 deletions packages/charting/src/components/SankeyChart/SankeyChart.base.tsx
@@ -0,0 +1,156 @@
import * as React from 'react';
import { classNamesFunction, getId } from 'office-ui-fabric-react/lib/Utilities';
import { ISankeyChartProps, ISankeyChartStyleProps, ISankeyChartStyles } from './SankeyChart.types';
import { IProcessedStyleSet } from 'office-ui-fabric-react/lib/Styling';
import * as d3Sankey from 'd3-sankey';
const getClassNames = classNamesFunction<ISankeyChartStyleProps, ISankeyChartStyles>();

export class SankeyChartBase extends React.Component<
ISankeyChartProps,
{
containerWidth: number;
containerHeight: number;
}
> {
private _classNames: IProcessedStyleSet<ISankeyChartStyles>;
private chartContainer: HTMLDivElement;
private _reqID: number;
constructor(props: ISankeyChartProps) {
super(props);
this.state = {
containerHeight: 0,
containerWidth: 0,
};
}
public componentDidMount(): void {
this._fitParentContainer();
}

public componentDidUpdate(prevProps: ISankeyChartProps): void {
if (prevProps.shouldResize !== this.props.shouldResize) {
this._fitParentContainer();
}
}
public componentWillUnmount(): void {
cancelAnimationFrame(this._reqID);
}
public render(): React.ReactNode {
const { theme, className, styles, pathColor } = this.props;
this._classNames = getClassNames(styles!, {
theme: theme!,
width: this.state.containerWidth,
height: this.state.containerHeight,
pathColor: pathColor,
className,
});
const margin = { top: 10, right: 0, bottom: 10, left: 0 };
const width = this.state.containerWidth - margin.left - margin.right;
const height =
this.state.containerHeight - margin.top - margin.bottom > 0
? this.state.containerHeight - margin.top - margin.bottom
: 0;

const sankey = d3Sankey
.sankey()
.nodeWidth(5)
.nodePadding(6)
.extent([
[1, 1],
[width - 1, height - 6],
]);

sankey(this.props.data.SankeyChartData!);
const nodeData = this._createNodes(width);
const linkData = this._createLinks();
return (
<div
className={this._classNames.root}
role={'presentation'}
// eslint-disable-next-line react/jsx-no-bind
ref={(rootElem: HTMLDivElement) => (this.chartContainer = rootElem)}
>
<svg width={width} height={height} id={getId('sankeyChart')}>
<g className={this._classNames.nodes}>{nodeData}</g>
<g className={this._classNames.links} strokeOpacity={0.2}>
{linkData}
</g>
</svg>
</div>
);
}

private _createLinks(): React.ReactNode[] | undefined {
const links: React.ReactNode[] = [];
if (this.props.data.SankeyChartData) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.props.data.SankeyChartData.links.forEach((singleLink: any, index: number) => {
const path = d3Sankey.sankeyLinkHorizontal();
const pathValue = path(singleLink);
const link = (
<path
key={index}
d={pathValue ? pathValue : undefined}
strokeWidth={Math.max(1, singleLink.width)}
id={getId('link')}
>
<title>
<text>{singleLink.source.name + ' → ' + singleLink.target.name + '\n' + singleLink.value}</text>
</title>
</path>
);
links.push(link);
});
}
return links;
}

private _createNodes(width: number): React.ReactNode[] | undefined {
const nodes: React.ReactNode[] = [];
if (this.props.data.SankeyChartData) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.props.data.SankeyChartData.nodes.forEach((singleNode: any, index: number) => {
const height = singleNode.y1 - singleNode.y0 > 0 ? singleNode.y1 - singleNode.y0 : 0;
const node = (
<g id={getId('nodeGElement')} key={index}>
<rect
x={singleNode.x0}
y={singleNode.y0}
height={height}
width={singleNode.x1 - singleNode.x0}
fill={singleNode.color}
id={getId('nodeBar')}
/>
<text
x={singleNode.x0 < width / 2 ? singleNode.x1 + 6 : singleNode.x0 - 6}
y={(singleNode.y1 + singleNode.y0) / 2}
dy={'0.35em'}
textAnchor={singleNode.x0 < width / 2 ? 'start' : 'end'}
>
{singleNode.name}
</text>
<title>
<text>{singleNode.name + '\n' + singleNode.value}</text>
</title>
</g>
);
nodes.push(node);
});
return nodes;
}
}
private _fitParentContainer(): void {
const { containerWidth, containerHeight } = this.state;
this._reqID = requestAnimationFrame(() => {
const container = this.props.parentRef ? this.props.parentRef : this.chartContainer;
const currentContainerWidth = container.getBoundingClientRect().width;
const currentContainerHeight = container.getBoundingClientRect().height;
const shouldResize = containerWidth !== currentContainerWidth || containerHeight !== currentContainerHeight;
if (shouldResize) {
this.setState({
containerWidth: currentContainerWidth,
containerHeight: currentContainerHeight,
});
}
});
}
}
22 changes: 22 additions & 0 deletions packages/charting/src/components/SankeyChart/SankeyChart.styles.ts
@@ -0,0 +1,22 @@
import { ISankeyChartStyleProps, ISankeyChartStyles } from './SankeyChart.types';

export const getStyles = (props: ISankeyChartStyleProps): ISankeyChartStyles => {
const { className, theme, pathColor } = props;
return {
root: [
theme.fonts.medium,
{
display: 'flex',
width: '100%',
height: '100%',
flexDirection: 'column',
overflow: 'hidden',
},
className,
],
links: {
stroke: pathColor ? pathColor : theme.palette.blue,
fill: 'none',
},
};
};
11 changes: 11 additions & 0 deletions packages/charting/src/components/SankeyChart/SankeyChart.tsx
@@ -0,0 +1,11 @@
import { styled } from 'office-ui-fabric-react/lib/Utilities';
import { ISankeyChartProps, ISankeyChartStyleProps, ISankeyChartStyles } from './SankeyChart.types';
import { SankeyChartBase } from './SankeyChart.base';
import { getStyles } from './SankeyChart.styles';

// Create a SankeyChart variant which uses these default styles and this styled subcomponent.
export const SankeyChart: React.FunctionComponent<ISankeyChartProps> = styled<
ISankeyChartProps,
ISankeyChartStyleProps,
ISankeyChartStyles
>(SankeyChartBase, getStyles);
77 changes: 77 additions & 0 deletions packages/charting/src/components/SankeyChart/SankeyChart.types.ts
@@ -0,0 +1,77 @@
import { ITheme, IStyle } from 'office-ui-fabric-react/lib/Styling';
import { IStyleFunctionOrObject } from 'office-ui-fabric-react/lib/Utilities';
import { IChartProps } from '../../types/IDataPoint';

export { IChartProps, IDataPoint, ISankeyChartData } from '../../types/IDataPoint';

export interface ISankeyChartProps {
/**
* Data to render in the chart.
*/
data: IChartProps;

/**
* Width of the chart.
*/
width?: number;

/**
* Height of the chart.
*/
height?: number;

/**
* Additional CSS class(es) to apply to the SankeyChart.
*/
className?: string;

/**
* Theme (provided through customization.)
*/
theme?: ITheme;

/**
* Call to provide customized styling that will layer on top of the variant rules.
*/
styles?: IStyleFunctionOrObject<ISankeyChartStyleProps, ISankeyChartStyles>;

/**
* this prop takes its parent as a HTML element to define the width and height of the Sankey chart
*/
parentRef?: HTMLElement | null;

/**
* should chart resize when parent resize.
*/
shouldResize?: number;

/**
* Color for path
*/
pathColor?: string;
}

export interface ISankeyChartStyleProps {
theme: ITheme;
className?: string;
width: number;
height: number;
pathColor?: string;
}

export interface ISankeyChartStyles {
/**
* Style for the root element.
*/
root?: IStyle;

/**
* Style for the nodes.
*/
nodes?: IStyle;

/**
* Style for the links.
*/
links?: IStyle;
}
53 changes: 53 additions & 0 deletions packages/charting/src/components/SankeyChart/SankeyChartPage.tsx
@@ -0,0 +1,53 @@
import * as React from 'react';

import { ComponentPage, ExampleCard, IComponentDemoPageProps, PropertiesTableSet } from '@uifabric/example-app-base';

import { SankeyChartBasicExample } from './examples/SankeyChart.Basic.Example';

const SankeyChartBasicExampleCode = require('!raw-loader!@uifabric/charting/src/components/SankeyChart/examples/SankeyChart.Basic.Example.tsx') as string;

export class SankeyChartPage extends React.Component<IComponentDemoPageProps, {}> {
public render(): JSX.Element {
return (
<ComponentPage
title="SankeyChart"
componentName="SankeyChartExample"
exampleCards={
<div>
<ExampleCard title="SankeyChart basic" code={SankeyChartBasicExampleCode}>
<SankeyChartBasicExample />
</ExampleCard>
</div>
}
propertiesTables={
<PropertiesTableSet
sources={[
require<string>('!raw-loader!@uifabric/charting/src/components/SankeyChart/SankeyChart.types.ts'),
]}
/>
}
overview={
<div>
<p>SankeyChart description</p>
</div>
}
bestPractices={<div />}
dos={
<div>
<ul>
<li />
</ul>
</div>
}
donts={
<div>
<ul>
<li />
</ul>
</div>
}
isHeaderVisible={this.props.isHeaderVisible}
/>
);
}
}

0 comments on commit a92d7dc

Please sign in to comment.