Skip to content

Commit

Permalink
Sankey diagram: Support number formatting (#31113)
Browse files Browse the repository at this point in the history
Co-authored-by: Laxmi Bhavani Malkareddy <lamalkar@microsoft.com>
  • Loading branch information
LaxmiBhavaniM and Laxmi Bhavani Malkareddy committed Apr 23, 2024
1 parent 21ae4f9 commit 120868a
Show file tree
Hide file tree
Showing 6 changed files with 1,496 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Sankey Diagram: Support number formatting",
"packageName": "@fluentui/react-charting",
"email": "lamalkar@microsoft.com",
"dependentChangeType": "patch"
}
1 change: 1 addition & 0 deletions packages/react-charting/etc/react-charting.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1121,6 +1121,7 @@ export interface ISankeyChartProps {
className?: string;
colorsForNodes?: string[];
data: IChartProps;
formatNumberOptions?: Intl.NumberFormatOptions;
height?: number;
parentRef?: HTMLElement | null;
pathColor?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -617,27 +617,6 @@ type AccessibilityRenderer = {
linkAriaLabel: (link: SLink) => string;
};

function linkCalloutAttributes(
singleLink: SLink,
from: string,
): IChartHoverCardProps & {
selectedLink: SLink;
isCalloutVisible: boolean;
color: string;
xCalloutValue: string;
yCalloutValue: string;
descriptionMessage: string;
} {
return {
selectedLink: singleLink,
isCalloutVisible: true,
color: (singleLink.source as SNode).color!,
xCalloutValue: (singleLink.target as SNode).name,
yCalloutValue: singleLink.unnormalizedValue!.toString(),
descriptionMessage: from,
};
}

// NOTE: To start employing React.useMemo properly, we need to convert this code from a React.Component
// to a function component. This will require a significant refactor of the code in this file.
// https://stackoverflow.com/questions/60223362/fast-way-to-convert-react-class-component-to-functional-component
Expand Down Expand Up @@ -758,8 +737,13 @@ export class SankeyChartBase extends React.Component<ISankeyChartProps, ISankeyC
return {
emptyAriaLabel: accessibility?.emptyAriaLabel || 'Graph has no data to display',
linkAriaLabel: (link: SLink) =>
format(linkString, (link.source as SNode).name, (link.target as SNode).name, link.unnormalizedValue),
nodeAriaLabel: (node: SNode, weight: number) => format(nodeString, node.name, weight),
format(
linkString,
(link.source as SNode).name,
(link.target as SNode).name,
link.unnormalizedValue ? this._formatNumber(link.unnormalizedValue) : link.unnormalizedValue,
),
nodeAriaLabel: (node: SNode, weight: number) => format(nodeString, node.name, this._formatNumber(weight)),
};
})(props.accessibility);
// NOTE: Memoizing the `_createNodes` and `_createLinks` methods would break the hoverability of the chart
Expand Down Expand Up @@ -891,6 +875,27 @@ export class SankeyChartBase extends React.Component<ISankeyChartProps, ISankeyC
);
}

private _linkCalloutAttributes(
singleLink: SLink,
from: string,
): IChartHoverCardProps & {
selectedLink: SLink;
isCalloutVisible: boolean;
color: string;
xCalloutValue: string;
yCalloutValue: string;
descriptionMessage: string;
} {
return {
selectedLink: singleLink,
isCalloutVisible: true,
color: (singleLink.source as SNode).color!,
xCalloutValue: (singleLink.target as SNode).name,
yCalloutValue: this._formatNumber(singleLink.unnormalizedValue!),
descriptionMessage: from,
};
}

private _normalizeSankeyData(
data: ISankeyChartData,
containerWidth: number,
Expand Down Expand Up @@ -1076,7 +1081,7 @@ export class SankeyChartBase extends React.Component<ISankeyChartProps, ISankeyC
fill={textColor}
fontSize={14}
>
{actualValue}
{actualValue ? this._formatNumber(actualValue) : actualValue}
</text>
</g>
)}
Expand Down Expand Up @@ -1113,12 +1118,17 @@ export class SankeyChartBase extends React.Component<ISankeyChartProps, ISankeyC
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,
yCalloutValue: this._formatNumber(singleNode.actualValue!),
});
}
}

private _formatNumber(value: number): string {
return this.props.formatNumberOptions
? value.toLocaleString(undefined, this.props.formatNumberOptions)
: value.toString();
}

private _onStreamHover(mouseEvent: React.MouseEvent<SVGElement>, singleLink: SLink, from: string) {
mouseEvent.persist();
this._onCloseCallout();
Expand All @@ -1129,7 +1139,7 @@ export class SankeyChartBase extends React.Component<ISankeyChartProps, ISankeyC
selectedNodes: new Set<number>(Array.from(selectedNodes).map(node => node.index!)),
selectedLinks: new Set<number>(Array.from(selectedLinks).map(link => link.index!)),
refSelected: mouseEvent,
...linkCalloutAttributes(singleLink, from),
...this._linkCalloutAttributes(singleLink, from),
});
}
}
Expand All @@ -1153,7 +1163,7 @@ export class SankeyChartBase extends React.Component<ISankeyChartProps, ISankeyC
this._onCloseCallout();
this.setState({
refSelected: element.currentTarget,
...linkCalloutAttributes(singleLink, from),
...this._linkCalloutAttributes(singleLink, from),
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,107 @@ describe('Sankey Chart snapShot testing', () => {
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

describe('number formatting', () => {
it('renders Sankey correctly by formatting large numbers', () => {
// ARRANGE
const data2 = {
chartTitle: 'Sankey Chart',
SankeyChartData: {
nodes: [
{ nodeId: 0, name: 'First' },
{ nodeId: 1, name: 'Second' },
{ nodeId: 2, name: 'Third' },
{ nodeId: 3, name: 'Fourth' },
{ nodeId: 4, name: 'Five' },
{ nodeId: 5, name: 'Six' },
{ nodeId: 6, name: 'Seven' },
],
links: [
{ source: 0, target: 1, value: 1234567890 },
{ source: 0, target: 2, value: 100000000 },
{ source: 0, target: 5, value: 1234 },
{ source: 0, target: 6, value: 100 },
{ source: 1, target: 3, value: 1000000000 },
{ source: 1, target: 4, value: 234567890 },
{ source: 2, target: 3, value: 1000 },
{ source: 2, target: 4, value: 9999000 },
],
},
};
const strings: ISankeyChartStrings = {
linkFrom: 'source {0}',
};
const accessibilityStrings: ISankeyChartAccessibilityProps = {
linkAriaLabel: '{2} items moved from {0} to {1}',
nodeAriaLabel: 'element {0} with size {1}',
};
// ACT
const component = renderer.create(
<SankeyChart
data={data2}
height={500}
width={800}
strings={strings}
accessibility={accessibilityStrings}
formatNumberOptions={{
maximumFractionDigits: 2,
notation: 'compact',
compactDisplay: 'short',
}}
/>,
);
// ASSERT
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders Sankey correctly by styling numbers as percentages', () => {
// ARRANGE
const data2 = {
chartTitle: 'Sankey Chart',
SankeyChartData: {
nodes: [
{ nodeId: 0, name: 'First' },
{ nodeId: 1, name: 'Second' },
{ nodeId: 2, name: 'Third' },
{ nodeId: 3, name: 'Fourth' },
{ nodeId: 4, name: 'Five' },
],
links: [
{ source: 0, target: 1, value: 0.6 },
{ source: 0, target: 2, value: 0.4 },
{ source: 1, target: 3, value: 0.25 },
{ source: 1, target: 4, value: 0.35 },
{ source: 2, target: 3, value: 0.15 },
{ source: 2, target: 4, value: 0.25 },
],
},
};
const strings: ISankeyChartStrings = {
linkFrom: 'source {0}',
};
const accessibilityStrings: ISankeyChartAccessibilityProps = {
linkAriaLabel: '{2} items moved from {0} to {1}',
nodeAriaLabel: 'element {0} with size {1}',
};
// ACT
const component = renderer.create(
<SankeyChart
data={data2}
height={500}
width={800}
strings={strings}
accessibility={accessibilityStrings}
formatNumberOptions={{
style: 'percent',
}}
/>,
);
// ASSERT
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
});

describe('Render calling with respective to props', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ export interface ISankeyChartProps {
* Localized strings to use for the chart's accessibility features.
*/
accessibility?: ISankeyChartAccessibilityProps;

/**
* Format node and link values.
*/
formatNumberOptions?: Intl.NumberFormatOptions;
}

/**
Expand Down

0 comments on commit 120868a

Please sign in to comment.