diff --git a/change/@fluentui-react-charting-cfa801e0-6573-413b-bb65-4c94f5e8ddbe.json b/change/@fluentui-react-charting-cfa801e0-6573-413b-bb65-4c94f5e8ddbe.json new file mode 100644 index 0000000000000..fee99acec2231 --- /dev/null +++ b/change/@fluentui-react-charting-cfa801e0-6573-413b-bb65-4c94f5e8ddbe.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Sankey chart changes for the new design", + "packageName": "@fluentui/react-charting", + "email": "ankityadav@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-charting/package.json b/packages/react-charting/package.json index 21c6e15dd3b98..4a3dbd8c5a265 100644 --- a/packages/react-charting/package.json +++ b/packages/react-charting/package.json @@ -43,7 +43,7 @@ "@types/d3-sankey": "^0.11.0", "@types/d3-scale": "^4.0.0", "@types/d3-selection": "1.4.1", - "@types/d3-shape": "^1.2.3", + "@types/d3-shape": "2.1.0", "@types/d3-time-format": "^2.1.0", "@types/d3-time": "^1.1.0", "@fluentui/set-version": "^8.2.2", @@ -54,7 +54,7 @@ "d3-sankey": "^0.12.3", "d3-scale": "^4.0.0", "d3-selection": "1.3.0", - "d3-shape": "^1.2.0", + "d3-shape": "2.1.0", "d3-time-format": "^2.1.3", "d3-time": "^1.1.0", "tslib": "^2.1.0" diff --git a/packages/react-charting/src/components/SankeyChart/SankeyChart.base.tsx b/packages/react-charting/src/components/SankeyChart/SankeyChart.base.tsx index 1ca4bf921b294..9602c4264f226 100644 --- a/packages/react-charting/src/components/SankeyChart/SankeyChart.base.tsx +++ b/packages/react-charting/src/components/SankeyChart/SankeyChart.base.tsx @@ -1,26 +1,87 @@ import * as React from 'react'; -import { classNamesFunction, getId } from '@fluentui/react/lib/Utilities'; -import { ISankeyChartProps, ISankeyChartStyleProps, ISankeyChartStyles } from './SankeyChart.types'; +import { classNamesFunction, getId, getRTL, memoizeFunction } from '@fluentui/react/lib/Utilities'; +import { ISankeyChartData, ISankeyChartProps, ISankeyChartStyleProps, ISankeyChartStyles } from './SankeyChart.types'; import { IProcessedStyleSet } from '@fluentui/react/lib/Styling'; import * as d3Sankey from 'd3-sankey'; +import { area as d3Area, curveBumpX as d3CurveBasis } from 'd3-shape'; +import { sum as d3Sum } from 'd3-array'; +import { ChartHoverCard, IBasestate, IChartHoverCardProps, SLink, SNode } from '../../index'; +import { Callout, DirectionalHint } from '@fluentui/react/lib/Callout'; +import { select, selectAll } from 'd3-selection'; +import { FocusZone, FocusZoneDirection, FocusZoneTabbableElements } from '@fluentui/react-focus'; +import { IMargins } from '../../utilities/utilities'; + const getClassNames = classNamesFunction(); +const PADDING_PERCENTAGE = 0.3; -export class SankeyChartBase extends React.Component< - ISankeyChartProps, - { - containerWidth: number; - containerHeight: number; - } -> { +type NodesInColumns = { [key: number]: SNode[] }; +type NodeColors = { fillColor: string; borderColor: string }; + +export interface ISankeyChartState extends IBasestate, IChartHoverCardProps { + containerWidth: number; + containerHeight: number; + selectedState: boolean; + selectedLinks: Set; + selectedNodes: Set; + selectedNode?: SNode; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + refSelected?: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + selectedLink?: SLink; + shouldOverflow: boolean; +} + +const NON_SELECTED_NODE_AND_STREAM_COLOR: string = '#757575'; +const DEFAULT_NODE_COLORS: NodeColors[] = [ + { fillColor: '#00758F', borderColor: '#002E39' }, + { fillColor: '#77004D', borderColor: '#43002C' }, + { fillColor: '#4F6BED', borderColor: '#3B52B4' }, + { fillColor: '#937600', borderColor: '#6D5700' }, + { fillColor: '#286EA8', borderColor: '#00457E' }, + { fillColor: '#A43FB1', borderColor: '#7C158A' }, + { fillColor: '#CC3595', borderColor: '#7F215D' }, + { fillColor: '#0E7878', borderColor: '#004E4E' }, + { fillColor: '#8764B8', borderColor: '#4B3867' }, + { fillColor: '#9C663F', borderColor: '#6D4123' }, +]; + +const MIN_HEIGHT_FOR_DOUBLINE_TYPE = 36; +const MIN_HEIGHT_FOR_TYPE = 24; +const REST_STREAM_OPACITY: number = 1; +const NON_SELECTED_OPACITY: number = 1; +const SELECTED_STREAM_OPACITY: number = 0.3; +const NON_SELECTED_STREAM_BORDER_OPACITY: number = 0.5; +const DEFAULT_TEXT_COLOR: string = '#323130'; +const NON_SELECTED_TEXT_COLOR: string = '#FFFFFF'; + +export class SankeyChartBase extends React.Component { private _classNames: IProcessedStyleSet; private chartContainer: HTMLDivElement; private _reqID: number; + private _calloutId: string; + private _linkId: string; + private _nodesInColumn: NodesInColumns; + private _sankey: d3Sankey.SankeyLayout, {}, {}>; + private _margins: IMargins; + private _isRtl: boolean = getRTL(); + private _normalizeData: (data: ISankeyChartData) => void; + constructor(props: ISankeyChartProps) { super(props); this.state = { - containerHeight: 0, - containerWidth: 0, + containerHeight: 468, + containerWidth: 912, + selectedState: false, + selectedLinks: new Set(), + selectedNodes: new Set(), + shouldOverflow: false, + isCalloutVisible: false, }; + this._calloutId = getId('callout'); + this._linkId = getId('link'); + this._margins = { top: 36, right: 48, bottom: 32, left: 48 }; + this._preRenderLayout(); + this._normalizeData = memoizeFunction((data: ISankeyChartData) => this._normalizeSankeyData(data)); } public componentDidMount(): void { this._fitParentContainer(); @@ -40,62 +101,257 @@ export class SankeyChartBase extends React.Component< theme: theme!, width: this.state.containerWidth, height: this.state.containerHeight, - pathColor: 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; + // We are using the this._margins.left and this._margins.top in sankey extent while constructing the layout + const { height, width } = this._preRenderLayout(); + this._normalizeData(this.props.data.SankeyChartData!); + let nodePadding = 8; + nodePadding = this._adjustPadding(this._sankey, height - 6, this._nodesInColumn); - const sankey = d3Sankey - .sankey() - .nodeWidth(5) - .nodePadding(6) - .extent([ - [1, 1], - [width - 1, height - 6], - ]); - - sankey(this.props.data.SankeyChartData!); + this._sankey.nodePadding(nodePadding); + this._sankey(this.props.data.SankeyChartData!); + this._populateNodeActualValue(this.props.data.SankeyChartData!); + this._assignNodeColors(); const nodeData = this._createNodes(width); const linkData = this._createLinks(); + const calloutProps = { + isCalloutVisible: this.state.isCalloutVisible, + directionalHint: DirectionalHint.topAutoEdge, + id: `toolTip${this._calloutId}`, + target: this.state.refSelected, + color: this.state.color, + XValue: this.state.xCalloutValue, + YValue: this.state.yCalloutValue ? this.state.yCalloutValue : this.state.dataForHoverCard, + descriptionMessage: this.state.descriptionMessage, + isBeakVisible: false, + gapSpace: 15, + onDismiss: this._onCloseCallout, + className: this._classNames.calloutContentRoot, + preventDismissOnLostFocus: true, + }; return (
(this.chartContainer = rootElem)} + onMouseLeave={this._onCloseCallout} > - - {nodeData} - - {linkData} - - + + + + {linkData} + + {nodeData} + {calloutProps.isCalloutVisible && ( + + + + )} + +
); } + private _preRenderLayout(): { height: number; width: number } { + const width = this.state.containerWidth - this._margins.right!; + const height = + this.state.containerHeight - this._margins.bottom! > 0 ? this.state.containerHeight - this._margins.bottom! : 0; + + this._sankey = d3Sankey + .sankey() + .nodeWidth(124) + .extent([ + [this._margins.left!, this._margins.top!], + [width - 1, height - 6], + ]) + .nodeAlign(this._isRtl ? d3Sankey.sankeyRight : d3Sankey.sankeyJustify); + + return { height, width }; + } + + /** + * This is used for calculating the node non normalized value based on link non normalized value. + * + */ + private _populateNodeActualValue(data: ISankeyChartData) { + data.links.forEach((link: SLink) => { + if (!link.unnormalizedValue) { + link.unnormalizedValue = link.value; + } + }); + data.nodes.forEach((node: SNode) => { + node.actualValue = Math.max( + d3Sum(node.sourceLinks!, (link: SLink) => link.unnormalizedValue), + d3Sum(node.targetLinks!, (link: SLink) => link.unnormalizedValue), + ); + }); + } + + private _normalizeSankeyData(data: ISankeyChartData): void { + this._nodesInColumn = this._populateNodeInColumns(data, this._sankey); + this._adjustOnePercentHeightNodes(this._nodesInColumn); + } + /** + * + * This is used to group nodes by column index. + */ + private _populateNodeInColumns( + graph: ISankeyChartData, + sankey: d3Sankey.SankeyLayout, {}, {}>, + ) { + sankey(graph); + const nodesInColumn: NodesInColumns = {}; + graph.nodes.forEach((node: SNode) => { + const columnId = node.layer!; + if (nodesInColumn[columnId]) { + nodesInColumn[columnId].push(node); + } else { + nodesInColumn[columnId] = [node]; + } + }); + return nodesInColumn; + } + + /** + * This is used to normalize the nodes value whose value is less than 1% of the total column value. + * + */ + private _adjustOnePercentHeightNodes(nodesInColumn: NodesInColumns) { + const totalColumnValue = Object.values(nodesInColumn).map((column: SNode[]) => { + return d3Sum(column, (node: SNode) => node.value); + }); + totalColumnValue.forEach((columnValue: number, index: number) => { + let totalPercentage = 0; + nodesInColumn[index].forEach((node: SNode) => { + const nodePercentage = (node.value! / columnValue) * 100; + node.actualValue = node.value; + //if the value is less than 1% then we are making it as 1% of total . + if (nodePercentage < 1) { + node.value = 0.01 * columnValue; + totalPercentage = totalPercentage + 1; + } else { + totalPercentage = totalPercentage + nodePercentage; + } + }); + //since we have adjusted the value to be 1% but we need to keep the sum of the percentage value under 100. + const scalingRatio = totalPercentage !== 0 ? totalPercentage / 100 : 1; + if (scalingRatio > 1) { + nodesInColumn[index].forEach((node: SNode) => { + node.value = node.value! / scalingRatio; + this._changeColumnValue(node, node.actualValue!, node.value); + }); + } + }); + } + + /** + * + * This is used for normalizing each links value for reflecting the normalized node value. + */ + private _changeColumnValue(node: SNode, originalValue: number, normalizedValue: number) { + node.sourceLinks!.forEach((link: SLink) => { + link.unnormalizedValue = link.value; + const linkRatio = link.value / originalValue; + link.value = normalizedValue * linkRatio; + }); + node.targetLinks!.forEach((link: SLink) => { + link.unnormalizedValue = link.value; + const linkRatio = link.value / originalValue; + link.value = normalizedValue * linkRatio; + }); + } + + /** + * + * This is used to introduce dynamic padding for cases where the number of nodes in a column is huge + * so that we maintain a node to space ratio for such columns as if we fail to do so the + * chart is devoid of nodes and only shows links. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _adjustPadding(sankey: any, height: number, nodesInColumn: NodesInColumns) { + let padding = this._sankey.nodePadding(); + Object.values(nodesInColumn).forEach((column: SNode[]) => { + const minPadding = PADDING_PERCENTAGE * height; + const toatlPaddingInColumn = height - d3Sum(column, (node: SNode) => node.y1! - node.y0!); + if (minPadding < toatlPaddingInColumn) { + //Here we are calculating the min of default and calculated padding, we will not increase the padding + //in any scenario. + padding = Math.min(padding, minPadding / (column.length - 1)); + } + }); + return padding; + } + 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); + this.props.data.SankeyChartData.links.forEach((singleLink: SLink, index: number) => { + const onMouseOut = () => { + this._onStreamLeave(singleLink); + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data = (d: any) => { + return [ + { x: d.source.x1, y0: d.y0 + d.width / 2, y1: d.y0 - d.width / 2 }, + { x: d.target.x0, y0: d.y1 + d.width / 2, y1: d.y1 - d.width / 2 }, + ]; + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const dataPoints: Array = data(singleLink); + const linkArea = d3Area() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .x((p: any) => p.x) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .y0((p: any) => p.y0) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .y1((p: any) => p.y1) + .curve(d3CurveBasis); + const gradientUrl = `url(#gradient-${this._linkId}-${index})`; const link = ( - - - <text>{singleLink.source.name + ' → ' + singleLink.target.name + '\n' + singleLink.value}</text> - - + + + + + + + + + ); links.push(link); }); @@ -105,31 +361,118 @@ export class SankeyChartBase extends React.Component< 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; + this.props.data.SankeyChartData.nodes.forEach((singleNode: SNode, index: number) => { + const onMouseOut = () => { + this._onLeave(singleNode); + }; + const height = singleNode.y1! - singleNode.y0! > 0 ? singleNode.y1! - singleNode.y0! : 0; + let padding = 8; + let textLengthForNodeWeight = 0; + + // If the nodeWeight is in the same line as node description an extra padding + // of 6 px is required between node description and node weight. + if (height < MIN_HEIGHT_FOR_DOUBLINE_TYPE) { + padding = padding + 6; + const tspan = select('.nodeName').append('text').attr('class', 'tempText').append('tspan').text(null); + tspan.text(singleNode.actualValue!); + if (tspan.node() !== null) { + textLengthForNodeWeight = tspan.node()!.getComputedTextLength(); + padding = padding + textLengthForNodeWeight; + } + tspan.text(null); + selectAll('.tempText').remove(); + } + // Since the total width of the node is 124 and we are giving margin of 8px from the left . + //So the actual value on which it will be truncated is 124-8=116. + const truncatedname: string = this._truncateText(singleNode.name, 116, padding); + const isTruncated: boolean = truncatedname.slice(-3) === '...'; + const id = getId('tooltip'); + const div = select('body') + .append('div') + .attr('id', id) + .attr('class', this._classNames.toolTip!) + .style('opacity', 0); + const nodeId = getId('nodeBar'); const node = ( - + - - {singleNode.name} - - - <text>{singleNode.name + '\n' + singleNode.value}</text> - + {singleNode.y1! - singleNode.y0! > MIN_HEIGHT_FOR_TYPE && ( + + + + + + MIN_HEIGHT_FOR_DOUBLINE_TYPE ? singleNode.x0 : singleNode.x1! - textLengthForNodeWeight - 8 + } + y={singleNode.y0} + dy={height > MIN_HEIGHT_FOR_DOUBLINE_TYPE ? '2em' : '1em'} + dx={height > MIN_HEIGHT_FOR_DOUBLINE_TYPE ? '0.4em' : '0em'} + textAnchor={this._isRtl ? 'end' : 'start'} + fontWeight="bold" + aria-hidden="true" + fill={ + !( + !this.state.selectedState || + (this.state.selectedState && + this.state.selectedNodes.has(singleNode.index!) && + this.state.selectedNode) || + (this.state.selectedState && !this.state.selectedNode) + ) + ? DEFAULT_TEXT_COLOR + : NON_SELECTED_TEXT_COLOR + } + fontSize={14} + > + {singleNode.actualValue} + + + )} ); nodes.push(node); @@ -137,6 +480,292 @@ export class SankeyChartBase extends React.Component< return nodes; } } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _onLeave(singleNode: SNode) { + if (this.state.selectedState) { + this.setState({ + selectedState: false, + selectedNodes: new Set(), + selectedLinks: new Set(), + selectedNode: undefined, + }); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _onHover(singleNode: SNode, mouseEvent: React.MouseEvent) { + mouseEvent.persist(); + this._onCloseCallout(); + if (!this.state.selectedState) { + const selectedLinks = this._getSelectedLinks(singleNode); + const selectedNodes = this._getSelectedNodes(selectedLinks); + selectedNodes.push(singleNode); + this.setState({ + selectedState: true, + selectedNodes: new Set(Array.from(selectedNodes).map(node => node.index)), + selectedLinks: new Set(Array.from(selectedLinks).map(link => link.index!)), + selectedNode: singleNode, + refSelected: mouseEvent, //this._refArray.get(index), + isCalloutVisible: singleNode.y1! - singleNode.y0! < MIN_HEIGHT_FOR_TYPE, + color: singleNode.color, + xCalloutValue: singleNode.name, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + yCalloutValue: (singleNode.actualValue! as any) as string, + }); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _onStreamHover(singleLink: SLink, mouseEvent: React.MouseEvent) { + mouseEvent.persist(); + this._onCloseCallout(); + if (!this.state.selectedState) { + const { selectedLinks, selectedNodes } = this._getSelectedLinksforStreamHover(singleLink); + this.setState({ + selectedState: true, + selectedNodes: new Set(Array.from(selectedNodes).map(node => node.index!)), + selectedLinks: new Set(Array.from(selectedLinks).map(link => link.index!)), + refSelected: mouseEvent, + selectedLink: singleLink, + isCalloutVisible: true, + color: (singleLink.source as SNode).color, + xCalloutValue: (singleLink.target as SNode).name, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + yCalloutValue: ((singleLink.source as SNode).actualValue! as any) as string, + descriptionMessage: 'from ' + (singleLink.source as SNode).name, + }); + } + } + + private _onStreamLeave(singleLink: SLink) { + if (this.state.selectedState) { + this.setState({ + selectedState: false, + selectedNodes: new Set(), + selectedLinks: new Set(), + selectedLink: undefined, + }); + } + } + + private _onFocusLink(singleLink: SLink, element: React.FocusEvent): void { + element.persist(); + this._onCloseCallout(); + this.setState({ + refSelected: element.currentTarget, + selectedLink: singleLink, + isCalloutVisible: true, + color: (singleLink.source as SNode).color, + xCalloutValue: (singleLink.target as SNode).name, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + yCalloutValue: ((singleLink.source as SNode).actualValue! as any) as string, + descriptionMessage: 'from ' + (singleLink.source as SNode).name, + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _onCloseCallout = () => { + this.setState({ + isCalloutVisible: false, + refSelected: undefined, + descriptionMessage: '', + }); + }; + + private _onBlur = (): void => { + /**/ + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _fillNodeColors = (singleNode: SNode): string | undefined => { + if (!this.state.selectedState) { + return singleNode.color; + } else if (this.state.selectedState && this.state.selectedNodes.has(singleNode.index!) && this.state.selectedNode) { + return this.state.selectedNode.color; + } else if (this.state.selectedState && !this.state.selectedNode) { + return singleNode.color; + } + }; + + /** + * This is used to assign node fillcolors and borderColor cyclically when the user doesnt + * provide color to individual node. + */ + private _assignNodeColors() { + let colors: string[]; + let borders: string[]; + if (this.props.colorsForNodes && this.props.borderColorsForNodes) { + colors = this.props.colorsForNodes; + borders = this.props.borderColorsForNodes; + } else { + colors = DEFAULT_NODE_COLORS.map(color => color.fillColor); + borders = DEFAULT_NODE_COLORS.map(color => color.borderColor); + } + let currentIndex = 0; + this.props.data.SankeyChartData!.nodes.forEach((node: SNode) => { + if (!node.color && !node.borderColor) { + node.color = colors[currentIndex]; + node.borderColor = borders[currentIndex]; + } else if (node.color && !node.borderColor) { + node.borderColor = '#757575'; + } else if (node.borderColor && !node.color) { + node.color = '#F5F5F5'; + } + currentIndex = (currentIndex + 1) % colors.length; + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _fillStreamColors(singleLink: SLink, gradientUrl: string): string | undefined { + if (this.state.selectedState && this.state.selectedLinks.has(singleLink.index!) && this.state.selectedNode) { + return this.state.selectedNode.color; + } else if (this.state.selectedState && this.state.selectedLinks.has(singleLink.index!)) { + return gradientUrl; + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _fillStreamBorder(singleLink: SLink, gradientUrl: string): string { + if (!this.state.selectedState) { + return NON_SELECTED_NODE_AND_STREAM_COLOR; + } else if (this.state.selectedState && this.state.selectedLinks.has(singleLink.index!) && this.state.selectedNode) { + return this.state.selectedNode.borderColor!; + } else if (this.state.selectedState && this.state.selectedLinks.has(singleLink.index!)) { + return gradientUrl; + } + return NON_SELECTED_NODE_AND_STREAM_COLOR; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _fillNodeBorder = (singleNode: SNode): string => { + if (!this.state.selectedState) { + return singleNode.borderColor!; + } else if (this.state.selectedState && this.state.selectedNodes.has(singleNode.index!) && this.state.selectedNode) { + return this.state.selectedNode.borderColor!; + } else if (this.state.selectedState && this.state.selectedNodes.has(singleNode.index!)) { + return singleNode.borderColor!; + } + return singleNode.borderColor!; + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _getSelectedNodes(selectedLinks: Set): any[] { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const nodes: SNode[] = []; + selectedLinks.forEach(link => { + nodes.push(link.target as SNode); + + if (nodes.indexOf(link.source as SNode) === -1) { + nodes.push(link.source as SNode); + } + }); + return nodes; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _getSelectedLinks(singleNode: SNode): Set { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-array-constructor + const q: any = new Array(); + const finalLinks: Set = new Set(); + + singleNode.sourceLinks!.forEach((link: SLink) => { + q.push(link); + finalLinks.add(link); + }); + + while (q.length > 0) { + const poppedLink: SLink = q.shift(); + const node: SNode = poppedLink.target as SNode; + if (node && node.sourceLinks) { + node.sourceLinks.forEach((link: SLink) => { + finalLinks.add(link); + q.push(link); + }); + } + } + + if (singleNode.targetLinks) { + singleNode.targetLinks.forEach((link: SLink) => { + q.push(link); + finalLinks.add(link); + }); + } + + while (q.length > 0) { + const poppedLink: SLink = q.shift(); + const node: SNode = poppedLink.source as SNode; + if (node && node.targetLinks) { + node.targetLinks.forEach((link: SLink) => { + finalLinks.add(link); + q.push(link); + }); + } + } + + return finalLinks; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _getSelectedLinksforStreamHover(singleLink: SLink): { selectedLinks: Set; selectedNodes: Set } { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-array-constructor + const q: any = new Array(); + const finalLinks: Set = new Set(); + const finalNodes: Set = new Set(); + + q.push(singleLink.source); + finalLinks.add(singleLink); + while (q.length > 0) { + const poppedNode: SNode = q.shift(); + finalNodes.add(poppedNode); + if (poppedNode.targetLinks && poppedNode.targetLinks.length > 0) { + poppedNode.targetLinks.forEach((link: SLink) => { + q.push(link.source); + finalLinks.add(link); + }); + } + } + + q.push(singleLink.target); + + while (q.length > 0) { + const poppedNode: SNode = q.shift(); + finalNodes.add(poppedNode); + if (poppedNode.sourceLinks && poppedNode.sourceLinks.length > 0) { + poppedNode.sourceLinks.forEach((link: SLink) => { + q.push(link.target); + finalLinks.add(link); + }); + } + } + + return { + selectedLinks: finalLinks, + selectedNodes: finalNodes, + }; + } + + private _getOpacityStream(singleLink: SLink): number { + if (this.state.selectedState) { + if (!this.state.selectedLinks.has(singleLink.index!)) { + return NON_SELECTED_OPACITY; + } else if ( + this.state.selectedState && + this.state.selectedLinks.has(singleLink.index!) && + !this.state.selectedNode + ) { + return SELECTED_STREAM_OPACITY; + } + } + return REST_STREAM_OPACITY; + } + + private _getOpacityStreamBorder(singleLink: SLink): number { + if (this.state.selectedState && !this.state.selectedLinks.has(singleLink.index!) && !this.state.selectedNode) { + return NON_SELECTED_STREAM_BORDER_OPACITY; + } + + return NON_SELECTED_OPACITY; + } + private _fitParentContainer(): void { const { containerWidth, containerHeight } = this.state; this._reqID = requestAnimationFrame(() => { @@ -152,4 +781,65 @@ export class SankeyChartBase extends React.Component< } }); } + /** + * + * @param text is the text which we are trying to truncate + * @param rectangleWidth is the width of the rectangle which will contain the text + * @param padding is the space we need to leave between the rect lines and other text + * @param nodeWeight is the text if present needs to be accomodate in the same line as text. + * @returns the truncated text , if truncated given the above parameters. + */ + private _truncateText(text: string, rectangleWidth: number, padding: number) { + const textLengthForNodeName = rectangleWidth - padding; + let elipsisLength = 0; + const tspan = select('.nodeName') + .append('text') + .attr('class', 'tempText') + .attr('font-size', '10') + .append('tspan') + .text(null); + tspan.text(text); + if (tspan.node() !== null && tspan.node()!.getComputedTextLength() <= textLengthForNodeName) { + return text; + } + tspan.text(null); + tspan.text('...'); + if (tspan.node() !== null) { + elipsisLength = tspan.node()!.getComputedTextLength(); + } + tspan.text(null); + let line: string = ''; + for (let i = 0; i < text.length; i++) { + line += text[i]; + tspan.text(line); + if (tspan.node() !== null) { + const w = tspan.node()!.getComputedTextLength(); + if (w >= textLengthForNodeName - elipsisLength) { + line = line.slice(0, -1); + line += '...'; + break; + } + } + } + tspan.text(null); + selectAll('.tempText').remove(); + return line; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _showTooltip(text: string, checkTrcuncated: boolean, div: any, evt: any) { + if (checkTrcuncated) { + //Fixing tooltip position by attaching it to the element rather than page + div.style('opacity', 0.9); + div + .html(text) + .style('left', evt.pageX + 'px') + .style('top', evt.pageY - 28 + 'px'); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _hideTooltip(div: any) { + div.style('opacity', 0); + } } diff --git a/packages/react-charting/src/components/SankeyChart/SankeyChart.styles.ts b/packages/react-charting/src/components/SankeyChart/SankeyChart.styles.ts index 327bd2a670ca0..da22f0337477d 100644 --- a/packages/react-charting/src/components/SankeyChart/SankeyChart.styles.ts +++ b/packages/react-charting/src/components/SankeyChart/SankeyChart.styles.ts @@ -1,7 +1,9 @@ import { ISankeyChartStyleProps, ISankeyChartStyles } from './SankeyChart.types'; +import { HighContrastSelectorBlack } from '@fluentui/react/lib/Styling'; export const getStyles = (props: ISankeyChartStyleProps): ISankeyChartStyles => { const { className, theme, pathColor } = props; + const { effects } = theme; return { root: [ theme.fonts.medium, @@ -15,8 +17,53 @@ export const getStyles = (props: ISankeyChartStyleProps): ISankeyChartStyles => className, ], links: { - stroke: pathColor ? pathColor : theme.palette.blue, - fill: 'none', + stroke: pathColor ? pathColor : theme.palette.black, + fill: theme ? theme.semanticColors.bodyBackground : '#F5F5F5', + strokeWidth: 3, + selectors: { + [HighContrastSelectorBlack]: { + fill: '#000000', + }, + }, + }, + nodes: { + fill: '#F5F5F5', + selectors: { + [HighContrastSelectorBlack]: { + fill: '#000000', + }, + }, + }, + toolTip: { + ...props.theme!.fonts.medium, + display: 'flex', + flexDirection: 'column', + padding: '8px', + position: 'absolute', + textAlign: 'center', + top: '0px', + background: props.theme!.semanticColors.bodyBackground, + borderRadius: '2px', + pointerEvents: 'none', + }, + nodeTextContainer: { + selectors: { + text: { + selectors: { + [HighContrastSelectorBlack]: { + fill: 'rgb(179, 179, 179)', + }, + }, + }, + }, + + marginTop: '4px', + marginLeft: '8px', + marginBottom: '4px', + marginRight: '8px', + }, + calloutContentRoot: { + boxShadow: effects.elevation4, }, }; }; diff --git a/packages/react-charting/src/components/SankeyChart/SankeyChart.test.tsx b/packages/react-charting/src/components/SankeyChart/SankeyChart.test.tsx new file mode 100644 index 0000000000000..13226931a3587 --- /dev/null +++ b/packages/react-charting/src/components/SankeyChart/SankeyChart.test.tsx @@ -0,0 +1,463 @@ +jest.mock('react-dom'); +import * as React from 'react'; +import { resetIds } from '../../Utilities'; +import * as renderer from 'react-test-renderer'; +import { mount, ReactWrapper } from 'enzyme'; +import { ISankeyChartProps, SankeyChart } from './index'; +import { SankeyChartBase, ISankeyChartState } from './SankeyChart.base'; +import { IChartProps } from '../../index'; +import toJson from 'enzyme-to-json'; + +// Wrapper of the SankeyChart to be tested. +let wrapper: ReactWrapper | undefined; + +function sharedBeforeEach() { + resetIds(); +} + +function sharedAfterEach() { + if (wrapper) { + wrapper.unmount(); + wrapper = undefined; + } + + // Do this after unmounting the wrapper to make sure if any timers cleaned up on unmount are + // cleaned up in fake timers world + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((global.setTimeout as any).mock) { + jest.useRealTimers(); + } +} + +const data: IChartProps = { + chartTitle: 'Sankey Chart', + SankeyChartData: { + nodes: [ + { + nodeId: 0, + name: '192.168.42.72', + color: '#8764B8', + borderColor: '#4B3867', + }, + { + nodeId: 1, + name: '172.152.48.13', + color: '#8764B8', + borderColor: '#4B3867', + }, + { + nodeId: 2, + name: '124.360.55.1', + color: '#8764B8', + borderColor: '#4B3867', + }, + { + nodeId: 3, + name: '192.564.10.2', + color: '#8764B8', + borderColor: '#4B3867', + }, + { + nodeId: 4, + name: '124.124.50.1', + color: '#8764B8', + borderColor: '#4B3867', + }, + { + nodeId: 5, + name: '172.630.89.4', + color: '#8764B8', + borderColor: '#4B3867', + }, + { + nodeId: 6, + name: 'inbox', + color: '#0E7878', + borderColor: '#004E4E', + }, + { + nodeId: 7, + name: 'Junk Folder', + color: '#0E7878', + borderColor: '#004E4E', + }, + { + nodeId: 8, + name: 'Deleted Folder', + color: '#0E7878', + borderColor: '#004E4E', + }, + { + nodeId: 9, + name: 'Clicked', + color: '#4F6BED', + borderColor: '#3B52B4', + }, + { + nodeId: 10, + name: 'Opened', + color: '#4F6BED', + borderColor: '#3B52B4', + }, + { + nodeId: 11, + name: ' No further action required', + color: '#4F6BED', + borderColor: '#3B52B4', + }, + ], + links: [ + { + source: 0, + target: 6, + value: 80, + }, + { + source: 1, + target: 6, + value: 50, + }, + { + source: 1, + target: 7, + value: 28, + }, + { + source: 2, + target: 7, + value: 14, + }, + { + source: 3, + target: 7, + value: 7, + }, + { + source: 3, + target: 8, + value: 20, + }, + { + source: 4, + target: 7, + value: 10, + }, + { + source: 5, + target: 7, + value: 10, + }, + + { + source: 6, + target: 9, + value: 30, + }, + { + source: 6, + target: 10, + value: 55, + }, + { + source: 7, + target: 11, + value: 60, + }, + { + source: 8, + target: 11, + value: 2, + }, + ], + }, +}; + +const dataWithoutColors: IChartProps = { + chartTitle: 'Sankey Chart', + SankeyChartData: { + nodes: [ + { + nodeId: 0, + name: '192.168.42.72', + }, + { + nodeId: 1, + name: '172.152.48.13', + }, + { + nodeId: 2, + name: '124.360.55.1', + }, + { + nodeId: 3, + name: '192.564.10.2', + }, + { + nodeId: 4, + name: '124.124.50.1', + }, + { + nodeId: 5, + name: '172.630.89.4', + }, + { + nodeId: 6, + name: 'inbox', + }, + { + nodeId: 7, + name: 'Junk Folder', + }, + { + nodeId: 8, + name: 'Deleted Folder', + }, + { + nodeId: 9, + name: 'Clicked', + }, + { + nodeId: 10, + name: 'Opened', + }, + { + nodeId: 11, + name: ' No further action required', + }, + ], + links: [ + { + source: 0, + target: 6, + value: 80, + }, + { + source: 1, + target: 6, + value: 50, + }, + { + source: 1, + target: 7, + value: 28, + }, + { + source: 2, + target: 7, + value: 14, + }, + { + source: 3, + target: 7, + value: 7, + }, + { + source: 3, + target: 8, + value: 20, + }, + { + source: 4, + target: 7, + value: 10, + }, + { + source: 5, + target: 7, + value: 10, + }, + + { + source: 6, + target: 9, + value: 30, + }, + { + source: 6, + target: 10, + value: 55, + }, + { + source: 7, + target: 11, + value: 60, + }, + { + source: 8, + target: 11, + value: 2, + }, + ], + }, +}; + +describe('Sankey Chart snapShot testing', () => { + it('renders Sankey correctly', () => { + const component = renderer.create(); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders Sankey correctly on providing nodecolors and border colors ', () => { + const nodeColors = ['#E3008C', '#00A2AD', '#022F22', '#00188F']; + const borderColors = ['#002E39', '#43002C', '#3B52B4']; + + const component = renderer.create( + , + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); + +describe('Render calling with respective to props', () => { + it('No prop changes', () => { + const renderMock = jest.spyOn(SankeyChartBase.prototype, 'render'); + const props = { + data, + height: 500, + width: 800, + }; + mount(); + expect(renderMock).toHaveBeenCalledTimes(1); + renderMock.mockRestore(); + }); + + it('prop changes', () => { + const renderMock = jest.spyOn(SankeyChartBase.prototype, 'render'); + const props = { + data, + height: 700, + width: 1100, + }; + const component = mount(); + component.setProps({ ...props, height: 1000 }); + expect(renderMock).toHaveBeenCalledTimes(2); + renderMock.mockRestore(); + }); +}); + +describe('SankeyChart - mouse events', () => { + beforeEach(sharedBeforeEach); + afterEach(sharedAfterEach); + + it('Should render correctly on node mouseover', () => { + wrapper = mount(); + wrapper.find('rect').at(1).simulate('mouseover'); + const tree = toJson(wrapper, { mode: 'deep' }); + expect(tree).toMatchSnapshot(); + }); + + it('Should render correctly on link mouseover', () => { + wrapper = mount(); + wrapper.find('path').at(1).simulate('mouseover'); + const tree = toJson(wrapper, { mode: 'deep' }); + expect(tree).toMatchSnapshot(); + }); + it('Should render callout correctly on mouseover when height of node is less than 24px', () => { + wrapper = mount(); + wrapper.find('rect[aria-label="node124.360.55.1with weight14"]').at(0).simulate('mouseover'); + const tree = toJson(wrapper, { mode: 'deep' }); + expect(tree).toMatchSnapshot(); + }); + + it('Should render tooltip correctly on mouseover when node description is large', () => { + wrapper = mount(); + wrapper.find('text[x=739]').at(0).simulate('mouseover'); + const tree = toJson(wrapper, { mode: 'deep' }); + expect(tree).toMatchSnapshot(); + }); +}); + +describe('SankeyChart - Min Height of Node Test', () => { + it('renders Sankey correctly on providing height less than onepercent of total height', () => { + const onepercentheightdata: IChartProps = { + chartTitle: 'Sankey Chart', + SankeyChartData: { + nodes: [ + { + nodeId: 0, + name: 'node0', + color: '#0078D4', + }, + { + nodeId: 1, + name: 'node1', + color: '#0078D4', + }, + { + nodeId: 2, + name: 'node2', + color: '#0078D4', + }, + { + nodeId: 3, + name: 'node3', + color: '#0078D4', + }, + { + nodeId: 4, + name: 'node4', + color: '#0078D4', + }, + { + nodeId: 5, + name: 'node5', + color: '#0078D4', + }, + { + nodeId: 6, + name: 'node6', + color: '#E3008C', + }, + { + nodeId: 7, + name: 'node7', + color: '#E3008C', + }, + ], + links: [ + { + source: 0, + target: 6, + value: 5, + }, + { + source: 1, + target: 6, + value: 5, + }, + { + source: 2, + target: 6, + value: 5, + }, + { + source: 3, + target: 6, + value: 5, + }, + { + source: 4, + target: 7, + value: 900, + }, + { + source: 5, + target: 7, + value: 80, + }, + ], + }, + }; + const component = renderer.create(); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/packages/react-charting/src/components/SankeyChart/SankeyChart.types.ts b/packages/react-charting/src/components/SankeyChart/SankeyChart.types.ts index 100834f6f2b4d..33af2b8acc187 100644 --- a/packages/react-charting/src/components/SankeyChart/SankeyChart.types.ts +++ b/packages/react-charting/src/components/SankeyChart/SankeyChart.types.ts @@ -49,6 +49,16 @@ export interface ISankeyChartProps { * Color for path */ pathColor?: string; + + /** + * Colors for nodes + */ + colorsForNodes?: string[]; + + /** + * Colors for nodes border + */ + borderColorsForNodes?: string[]; } export interface ISankeyChartStyleProps { @@ -74,4 +84,20 @@ export interface ISankeyChartStyles { * Style for the links. */ links?: IStyle; + + /** + * Style for the text inside node. + */ + nodeTextContainer?: IStyle; + + /** + * Style for the tooltip ,when user hover over the truncated node detail. + */ + toolTip?: IStyle; + + /** + * Style for the tooltip ,when user hover over the truncated node detail. + */ + + calloutContentRoot?: IStyle; } diff --git a/packages/react-charting/src/components/SankeyChart/__snapshots__/SankeyChart.test.tsx.snap b/packages/react-charting/src/components/SankeyChart/__snapshots__/SankeyChart.test.tsx.snap new file mode 100644 index 0000000000000..6b86c8faf1be7 --- /dev/null +++ b/packages/react-charting/src/components/SankeyChart/__snapshots__/SankeyChart.test.tsx.snap @@ -0,0 +1,7687 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Sankey Chart snapShot testing renders Sankey correctly 1`] = ` +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+`; + +exports[`Sankey Chart snapShot testing renders Sankey correctly on providing nodecolors and border colors 1`] = ` +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+`; + +exports[`SankeyChart - Min Height of Node Test renders Sankey correctly on providing height less than onepercent of total height 1`] = ` +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+`; + +exports[`SankeyChart - mouse events Should render callout correctly on mouseover when height of node is less than 24px 1`] = ` +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+`; + +exports[`SankeyChart - mouse events Should render correctly on link mouseover 1`] = ` +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+`; + +exports[`SankeyChart - mouse events Should render correctly on node mouseover 1`] = ` +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+`; + +exports[`SankeyChart - mouse events Should render tooltip correctly on mouseover when node description is large 1`] = ` +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+`; diff --git a/packages/react-charting/src/types/IDataPoint.ts b/packages/react-charting/src/types/IDataPoint.ts index 34a7cb60e3710..67ccf9066feb4 100644 --- a/packages/react-charting/src/types/IDataPoint.ts +++ b/packages/react-charting/src/types/IDataPoint.ts @@ -388,13 +388,17 @@ export interface ISankeyChartData { interface ISNodeExtra { nodeId: number | string; name: string; - color: string; + color?: string; + borderColor?: string; + actualValue?: number; + layer?: number; } interface ISLinkExtra { source: number; target: number; value: number; + unnormalizedValue?: number; } export type SNode = d3Sankey.SankeyNode; diff --git a/packages/react-examples/src/react-charting/SankeyChart/SankeyChart.Basic.Example.tsx b/packages/react-examples/src/react-charting/SankeyChart/SankeyChart.Basic.Example.tsx index d2f07d7ecca6c..91704be547f54 100644 --- a/packages/react-examples/src/react-charting/SankeyChart/SankeyChart.Basic.Example.tsx +++ b/packages/react-examples/src/react-charting/SankeyChart/SankeyChart.Basic.Example.tsx @@ -10,8 +10,8 @@ export class SankeyChartBasicExample extends React.Component<{}, ISankeyChartBas constructor(props: ISankeyChartProps) { super(props); this.state = { - width: 700, - height: 300, + width: 912, + height: 412, }; } @@ -34,32 +34,38 @@ export class SankeyChartBasicExample extends React.Component<{}, ISankeyChartBas { nodeId: 0, name: 'node0', - color: '#0078D4', + color: '#00758F', + borderColor: '#002E39', }, { nodeId: 1, name: 'node1', - color: '#EF6950', + color: '#77004D', + borderColor: '#43002C', }, { nodeId: 2, name: 'node2', - color: '#00188F', + color: '#4F6BED', + borderColor: '#3B52B4', }, { nodeId: 3, name: 'node3', - color: '#022F22', + color: '#937600', + borderColor: '#6D5700', }, { nodeId: 4, name: 'node4', - color: '#00A2AD', + color: '#286EA8', + borderColor: '#00457E', }, { nodeId: 5, name: 'node5', - color: '#E3008C', + color: '#A43FB1', + borderColor: '#7C158A', }, ], links: [ @@ -120,7 +126,7 @@ export class SankeyChartBasicExample extends React.Component<{}, ISankeyChartBas { + constructor(props: ISankeyChartProps) { + super(props); + this.state = { + width: 912, + height: 400, + }; + } + + public render(): JSX.Element { + return
{this._inboxExample()}
; + } + + private _onWidthChange = (e: React.ChangeEvent) => { + this.setState({ width: parseInt(e.target.value, 10) }); + }; + private _onHeightChange = (e: React.ChangeEvent) => { + this.setState({ height: parseInt(e.target.value, 10) }); + }; + + private _inboxExample(): JSX.Element { + const data: IChartProps = { + chartTitle: 'Sankey Chart', + SankeyChartData: { + nodes: [ + { + nodeId: 0, + name: '192.168.42.72', + color: '#8764B8', + borderColor: '#4B3867', + }, + { + nodeId: 1, + name: '172.152.48.13', + color: '#8764B8', + borderColor: '#4B3867', + }, + { + nodeId: 2, + name: '124.360.55.1', + color: '#8764B8', + borderColor: '#4B3867', + }, + { + nodeId: 3, + name: '192.564.10.2', + color: '#8764B8', + borderColor: '#4B3867', + }, + { + nodeId: 4, + name: '124.124.50.1', + color: '#8764B8', + borderColor: '#4B3867', + }, + { + nodeId: 5, + name: '172.630.89.4', + color: '#8764B8', + borderColor: '#4B3867', + }, + { + nodeId: 6, + name: 'inbox', + color: '#0E7878', + borderColor: '#004E4E', + }, + { + nodeId: 7, + name: 'Junk Folder', + color: '#0E7878', + borderColor: '#004E4E', + }, + { + nodeId: 8, + name: 'Deleted Folder', + color: '#0E7878', + borderColor: '#004E4E', + }, + { + nodeId: 9, + name: 'Clicked', + color: '#4F6BED', + borderColor: '#3B52B4', + }, + { + nodeId: 10, + name: 'Opened', + color: '#4F6BED', + borderColor: '#3B52B4', + }, + { + nodeId: 11, + name: ' No further action required', + color: '#4F6BED', + borderColor: '#3B52B4', + }, + ], + links: [ + { + source: 0, + target: 6, + value: 80, + }, + { + source: 1, + target: 6, + value: 50, + }, + { + source: 1, + target: 7, + value: 28, + }, + { + source: 2, + target: 7, + value: 14, + }, + { + source: 3, + target: 7, + value: 7, + }, + { + source: 3, + target: 8, + value: 20, + }, + { + source: 4, + target: 7, + value: 10, + }, + { + source: 5, + target: 7, + value: 10, + }, + + { + source: 6, + target: 9, + value: 30, + }, + { + source: 6, + target: 10, + value: 55, + }, + { + source: 7, + target: 11, + value: 60, + }, + { + source: 8, + target: 11, + value: 2, + }, + ], + }, + }; + + const rootStyle = { width: `${this.state.width}px`, height: `${this.state.height}px` }; + + return ( + <> + + + + +
+ +
+ + ); + } +} diff --git a/packages/react-examples/src/react-charting/SankeyChart/SankeyChartPage.tsx b/packages/react-examples/src/react-charting/SankeyChart/SankeyChartPage.tsx index bab038a357ba0..0cb3addebeda2 100644 --- a/packages/react-examples/src/react-charting/SankeyChart/SankeyChartPage.tsx +++ b/packages/react-examples/src/react-charting/SankeyChart/SankeyChartPage.tsx @@ -8,8 +8,10 @@ import { } from '@fluentui/react-docsite-components'; import { SankeyChartBasicExample } from './SankeyChart.Basic.Example'; +import { SankeyChartInboxExample } from './SankeyChart.Inbox.Example'; const SankeyChartBasicExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/SankeyChart/SankeyChart.Basic.Example.tsx') as string; +const SankeyChartInboxExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/SankeyChart/SankeyChart.Inbox.Example.tsx') as string; export class SankeyChartPage extends React.Component { public render(): JSX.Element { @@ -22,6 +24,9 @@ export class SankeyChartPage extends React.Component + + + } propertiesTables={ diff --git a/yarn.lock b/yarn.lock index 1da940e42cd48..3121a84e218a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5266,7 +5266,14 @@ resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.4.1.tgz#fa1f8710a6b5d7cfe5c6caa61d161be7cae4a022" integrity sha512-bv8IfFYo/xG6dxri9OwDnK3yCagYPeRIjTlrcdYJSx+FDWlCeBDepIHUpqROmhPtZ53jyna0aUajZRk0I3rXNA== -"@types/d3-shape@^1", "@types/d3-shape@^1.2.3": +"@types/d3-shape@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-2.1.0.tgz#cc7bbc9fc2c25f092bd457887a3224a21a55ca55" + integrity sha512-xTMEs8eITRksXclcVxMHIONRdyjj2TjDIwO4XFOPTVBNK9/oC4ZOhUbvTz1IpcsEsS/mClwuulP+OoawSAbSGA== + dependencies: + "@types/d3-path" "^1" + +"@types/d3-shape@^1": version "1.3.8" resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.8.tgz#c3c15ec7436b4ce24e38de517586850f1fea8e89" integrity sha512-gqfnMz6Fd5H6GOLYixOZP/xlrMtJms9BaS+6oWxTKHNqPGZ93BkWWupQSCYm6YHqx6h9wjRupuJb90bun6ZaYg== @@ -10461,6 +10468,11 @@ d3-path@1: resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.7.tgz#8de7cd693a75ac0b5480d3abaccd94793e58aae8" integrity sha512-q0cW1RpvA5c5ma2rch62mX8AYaiLX0+bdaSM2wxSU9tXjU4DNvkx9qiUvjkuWCj3p22UO/hlPivujqMiR9PDzA== +"d3-path@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8" + integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA== + d3-sankey@^0.12.3: version "0.12.3" resolved "https://registry.yarnpkg.com/d3-sankey/-/d3-sankey-0.12.3.tgz#b3c268627bd72e5d80336e8de6acbfec9d15d01d" @@ -10507,6 +10519,13 @@ d3-selection@1.3.0: resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d" integrity sha512-qgpUOg9tl5CirdqESUAu0t9MU/t3O9klYfGfyKsXEmhyxyzLpzpeh08gaxBUTQw1uXIOkr/30Ut2YRjSSxlmHA== +d3-shape@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-2.1.0.tgz#3b6a82ccafbc45de55b57fcf956c584ded3b666f" + integrity sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA== + dependencies: + d3-path "2" + d3-shape@^1.1.0, d3-shape@^1.2.0: version "1.3.7" resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"