From bc4577d83bd976fccf7713a3a6d8283e35e9f659 Mon Sep 17 00:00:00 2001 From: srmukher <120183316+srmukher@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:04:35 +0000 Subject: [PATCH 01/11] Support for multiple legend selection for Vertical Stacked Bar Chart --- .../VerticalStackedBarChart.base.tsx | 51 +++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx index dddf57a74ccb2..c41fd12279804 100644 --- a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx +++ b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx @@ -93,6 +93,7 @@ export interface IVerticalStackedBarChartState extends IBasestate { activeXAxisDataPoint: number | string | Date; callOutAccessibilityData?: IAccessibilityProps; calloutLegend: string; + selectedLegends: string[]; } export class VerticalStackedBarChartBase extends React.Component< IVerticalStackedBarChartProps, @@ -128,8 +129,8 @@ export class VerticalStackedBarChartBase extends React.Component< super(props); this.state = { isCalloutVisible: false, - selectedLegend: '', - activeLegend: '', + selectedLegends: [], + activeLegend: undefined, refSelected: null, dataForHoverCard: 0, color: '', @@ -281,7 +282,7 @@ export class VerticalStackedBarChartBase extends React.Component< const { isCalloutForStack = false } = this.props; let shouldFocusStackOnly: boolean = false; if (_isHavingLines) { - if (this.state.selectedLegend !== '') { + if (this.state.selectedLegends.length === 1) { shouldFocusStackOnly = false; } else { shouldFocusStackOnly = true; @@ -418,7 +419,7 @@ export class VerticalStackedBarChartBase extends React.Component< strokeLinecap="round" stroke={lineObject[item][i].color} transform={`translate(${xScaleBandwidthTranslate}, 0)`} - {...(this.state.selectedLegend === item && { + {...(this.state.selectedLegends.includes(item) && { onMouseOver: this._lineHover.bind(this, lineObject[item][i - 1]), onMouseLeave: this._lineHoverOut, })} @@ -438,11 +439,11 @@ export class VerticalStackedBarChartBase extends React.Component< circlePoint.useSecondaryYScale && secondaryYScale ? secondaryYScale(circlePoint.y) : yScale(circlePoint.y) } onMouseOver={ - this.state.selectedLegend === item + this.state.selectedLegends.includes(item) ? this._lineHover.bind(this, circlePoint) : this._onStackHover.bind(this, circlePoint.xItem) } - {...(this.state.selectedLegend === item && { + {...(this.state.selectedLegends.includes(item) && { onMouseLeave: this._lineHoverOut, })} r={this._getCircleVisibilityAndRadius(circlePoint.xItem.xAxisPoint, circlePoint.legend).radius} @@ -475,11 +476,11 @@ export class VerticalStackedBarChartBase extends React.Component< xAxisPoint: string | number | Date, legend: string, ): { visibility: CircleVisbility; radius: number } => { - const { selectedLegend, activeXAxisDataPoint } = this.state; - if (selectedLegend !== '') { - if (xAxisPoint === activeXAxisDataPoint && selectedLegend === legend) { + const { selectedLegends, activeXAxisDataPoint } = this.state; + if (selectedLegends.length > 0) { + if (xAxisPoint === activeXAxisDataPoint && selectedLegends.includes(legend)) { return { visibility: CircleVisbility.show, radius: 8 }; - } else if (selectedLegend === legend) { + } else if (selectedLegends.includes(legend)) { return { visibility: CircleVisbility.show, radius: 0.3 }; } else { return { visibility: CircleVisbility.hide, radius: 0 }; @@ -550,7 +551,7 @@ export class VerticalStackedBarChartBase extends React.Component< }; private _onLegendClick(legendTitle: string): void { - if (this.state.selectedLegend === legendTitle) { + if (this.state.selectedLegends.includes(legendTitle)) { this.setState({ selectedLegend: '', }); @@ -569,7 +570,7 @@ export class VerticalStackedBarChartBase extends React.Component< private _onLegendLeave(): void { this.setState({ - activeLegend: '', + activeLegend: undefined, }); } @@ -649,10 +650,20 @@ export class VerticalStackedBarChartBase extends React.Component< focusZonePropsInHoverCard={this.props.focusZonePropsForLegendsInHoverCard} overflowText={this.props.legendsOverflowText} {...this.props.legendProps} + onChange={this._onLegendChange} + canSelectMultipleLegends /> ); } + private _onLegendChange = ( + selectedLegends: string[], + event: React.MouseEvent, + legend: ILegend, + ) => { + this.setState({ selectedLegends }); + }; + private _onRectHover( xAxisPoint: string, point: IVSChartDataPoint, @@ -680,7 +691,7 @@ export class VerticalStackedBarChartBase extends React.Component< * Show the callout if highlighted bar is focused/hovered * and Hide it if unhighlighted bar is focused/hovered */ - isCalloutVisible: this.state.selectedLegend === '' || this.state.selectedLegend === point.legend, + isCalloutVisible: this.state.selectedLegends.length === 0 || this.state.selectedLegends.includes(point.legend), calloutLegend: point.legend, dataForHoverCard: point.data, color, @@ -734,6 +745,13 @@ export class VerticalStackedBarChartBase extends React.Component< stack: IVerticalStackedChartProps, refSelected: React.MouseEvent | SVGGElement, ): void { + if (this.state.selectedLegends.length > 1) { + stack = { + ...stack, + chartData: stack.chartData.filter(dataPoint => this.state.selectedLegends.includes(dataPoint.legend)), + lineData: stack.lineData?.filter(dataPoint => this.state.selectedLegends.includes(dataPoint.legend)), + }; + } const lineData = stack.lineData; const isLinesPresent: boolean = lineData !== undefined && lineData.length > 0; if (isLinesPresent) { @@ -742,6 +760,7 @@ export class VerticalStackedBarChartBase extends React.Component< item.shouldDrawBorderBottom = true; }); } + this.setState({ refSelected, isCalloutVisible: true, @@ -1105,8 +1124,8 @@ export class VerticalStackedBarChartBase extends React.Component< */ private _legendHighlighted = (legendTitle: string) => { return ( - this.state.selectedLegend === legendTitle || - (this.state.selectedLegend === '' && this.state.activeLegend === legendTitle) + this.state.selectedLegends.includes(legendTitle) || + (this.state.selectedLegends.length === 0 && this.state.activeLegend === legendTitle) ); }; @@ -1114,7 +1133,7 @@ export class VerticalStackedBarChartBase extends React.Component< * This function checks if none of the legends is selected or hovered. */ private _noLegendHighlighted = () => { - return this.state.selectedLegend === '' && this.state.activeLegend === ''; + return this.state.selectedLegends.length === 0 && this.state.activeLegend === undefined; }; private _getAriaLabel = (singleChartData: IVerticalStackedChartProps, point?: IVSChartDataPoint): string => { From 216f82e89da70bf4e43fa50646dd68ea487b8063 Mon Sep 17 00:00:00 2001 From: srmukher <120183316+srmukher@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:11:39 +0000 Subject: [PATCH 02/11] Adding change file --- ...eact-charting-891c4db7-8af3-489e-80f8-b8a02b227536.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@fluentui-react-charting-891c4db7-8af3-489e-80f8-b8a02b227536.json diff --git a/change/@fluentui-react-charting-891c4db7-8af3-489e-80f8-b8a02b227536.json b/change/@fluentui-react-charting-891c4db7-8af3-489e-80f8-b8a02b227536.json new file mode 100644 index 0000000000000..9c9a1d6e8eddd --- /dev/null +++ b/change/@fluentui-react-charting-891c4db7-8af3-489e-80f8-b8a02b227536.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Support for multiple legend selection for Vertical Stacked Bar Chart ", + "packageName": "@fluentui/react-charting", + "email": "120183316+srmukher@users.noreply.github.com", + "dependentChangeType": "patch" +} From 0235b7f5d06f32b9e366998754b4a6d9aea76332 Mon Sep 17 00:00:00 2001 From: srmukher <120183316+srmukher@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:19:36 +0000 Subject: [PATCH 03/11] Adding example for legend multi select for VSBC --- .../react-charting/etc/react-charting.api.md | 1 + .../VerticalStackedBarChart.base.tsx | 12 ++++-------- .../VerticalStackedBarChart.types.ts | 6 ++++++ .../VerticalStackedBarChart.Basic.Example.tsx | 14 ++++++++++++++ 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/charts/react-charting/etc/react-charting.api.md b/packages/charts/react-charting/etc/react-charting.api.md index 543e8d624e267..6481fd7695bc1 100644 --- a/packages/charts/react-charting/etc/react-charting.api.md +++ b/packages/charts/react-charting/etc/react-charting.api.md @@ -1476,6 +1476,7 @@ export interface IVerticalStackedBarChartProps extends ICartesianChartProps { barMinimumHeight?: number; barWidth?: number | 'default' | 'auto'; calloutProps?: Partial; + canSelectMultipleLegends?: boolean; chartTitle?: string; // @deprecated colors?: string[]; diff --git a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx index c41fd12279804..d45ee07ca8c48 100644 --- a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx +++ b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx @@ -553,11 +553,11 @@ export class VerticalStackedBarChartBase extends React.Component< private _onLegendClick(legendTitle: string): void { if (this.state.selectedLegends.includes(legendTitle)) { this.setState({ - selectedLegend: '', + selectedLegends: this.state.selectedLegends.filter((selectedLegend: string) => selectedLegend !== legendTitle), }); } else { this.setState({ - selectedLegend: legendTitle, + selectedLegends: [...this.state.selectedLegends, legendTitle], }); } } @@ -651,16 +651,12 @@ export class VerticalStackedBarChartBase extends React.Component< overflowText={this.props.legendsOverflowText} {...this.props.legendProps} onChange={this._onLegendChange} - canSelectMultipleLegends + canSelectMultipleLegends={this.props.canSelectMultipleLegends} /> ); } - private _onLegendChange = ( - selectedLegends: string[], - event: React.MouseEvent, - legend: ILegend, - ) => { + private _onLegendChange = (selectedLegends: string[]) => { this.setState({ selectedLegends }); }; diff --git a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.types.ts b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.types.ts index 5e47df049599c..f4ee2ce798b66 100644 --- a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.types.ts +++ b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.types.ts @@ -155,6 +155,12 @@ export interface IVerticalStackedBarChartProps extends ICartesianChartProps { * The prop used to enable rounded corners for the chart. */ roundCorners?: boolean; + + /** + * The prop used to enable multiple legend selection. + * @default false + */ + canSelectMultipleLegends?: boolean; } /** diff --git a/packages/react-examples/src/react-charting/VerticalStackedBarChart/VerticalStackedBarChart.Basic.Example.tsx b/packages/react-examples/src/react-charting/VerticalStackedBarChart/VerticalStackedBarChart.Basic.Example.tsx index d8c71162218f1..e825a3d91ef11 100644 --- a/packages/react-examples/src/react-charting/VerticalStackedBarChart/VerticalStackedBarChart.Basic.Example.tsx +++ b/packages/react-examples/src/react-charting/VerticalStackedBarChart/VerticalStackedBarChart.Basic.Example.tsx @@ -21,6 +21,7 @@ interface IVerticalStackedBarState { margins: {}; enableGradient: boolean; roundCorners: boolean; + legendMultiSelect: boolean; } export class VerticalStackedBarChartBasicExample extends React.Component<{}, IVerticalStackedBarState> { @@ -41,6 +42,7 @@ export class VerticalStackedBarChartBasicExample extends React.Component<{}, IVe }, enableGradient: false, roundCorners: false, + legendMultiSelect: false, }; } @@ -94,6 +96,10 @@ export class VerticalStackedBarChartBasicExample extends React.Component<{}, IVe this.setState({ roundCorners: checked }); }; + private _onToggleLegendMultiSelect = (ev: React.MouseEvent, checked: boolean) => { + this.setState({ legendMultiSelect: checked }); + }; + private _basicExample(): JSX.Element { const { showLine } = this.state; const firstChartPoints: IVSChartDataPoint[] = [ @@ -307,6 +313,13 @@ export class VerticalStackedBarChartBasicExample extends React.Component<{}, IVe    +    + {this.state.showAxisTitles && (
@@ -328,6 +341,7 @@ export class VerticalStackedBarChartBasicExample extends React.Component<{}, IVe xAxisTitle={this.state.showAxisTitles ? 'Number of days' : undefined} enableGradient={this.state.enableGradient} roundCorners={this.state.roundCorners} + canSelectMultipleLegends={this.state.legendMultiSelect} />
)} From 05054150d632d51bbdf5b76a0495198858470f32 Mon Sep 17 00:00:00 2001 From: srmukher <120183316+srmukher@users.noreply.github.com> Date: Mon, 16 Dec 2024 06:52:17 +0000 Subject: [PATCH 04/11] Handle legend click in VSBC --- .../VerticalStackedBarChart.base.tsx | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx index d45ee07ca8c48..e5f0d4b9cd3b2 100644 --- a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx +++ b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx @@ -550,14 +550,30 @@ export class VerticalStackedBarChartBase extends React.Component< : null; }; - private _onLegendClick(legendTitle: string): void { - if (this.state.selectedLegends.includes(legendTitle)) { - this.setState({ - selectedLegends: this.state.selectedLegends.filter((selectedLegend: string) => selectedLegend !== legendTitle), - }); + private _onLegendClick(legend: string): void { + // If multiple legends cannot be selected, set the selection to the clicked legend + if (!this.props.canSelectMultipleLegends) { + this.setState({ selectedLegends: [legend] }); } else { + // Get all legend titles + const allLegends = this._points.map((point: IVerticalStackedChartProps) => + point.chartData.map(data => data.legend), + ); + let selected: string[] = []; + if (this.state.selectedLegends.includes(legend)) { + selected = this.state.selectedLegends.filter((selectedLegend: string) => selectedLegend !== legend); + } else { + selected = [...this.state.selectedLegends, legend]; + } + + // Reset selection if all legends are selected + const allSelected = allLegends?.flat().every(l => selected.includes(l)); + if (allSelected) { + selected = []; + } + this.setState({ - selectedLegends: [...this.state.selectedLegends, legendTitle], + selectedLegends: selected, }); } } From 980d1aa38a434014dfc269915b81ac6414679bf4 Mon Sep 17 00:00:00 2001 From: srmukher <120183316+srmukher@users.noreply.github.com> Date: Mon, 16 Dec 2024 07:10:28 +0000 Subject: [PATCH 05/11] Removing legend click logic as it is handled in legend component --- .../VerticalStackedBarChart.base.tsx | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx index e5f0d4b9cd3b2..755db0f39b814 100644 --- a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx +++ b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx @@ -550,34 +550,6 @@ export class VerticalStackedBarChartBase extends React.Component< : null; }; - private _onLegendClick(legend: string): void { - // If multiple legends cannot be selected, set the selection to the clicked legend - if (!this.props.canSelectMultipleLegends) { - this.setState({ selectedLegends: [legend] }); - } else { - // Get all legend titles - const allLegends = this._points.map((point: IVerticalStackedChartProps) => - point.chartData.map(data => data.legend), - ); - let selected: string[] = []; - if (this.state.selectedLegends.includes(legend)) { - selected = this.state.selectedLegends.filter((selectedLegend: string) => selectedLegend !== legend); - } else { - selected = [...this.state.selectedLegends, legend]; - } - - // Reset selection if all legends are selected - const allSelected = allLegends?.flat().every(l => selected.includes(l)); - if (allSelected) { - selected = []; - } - - this.setState({ - selectedLegends: selected, - }); - } - } - private _onLegendHover(legendTitle: string): void { this.setState({ activeLegend: legendTitle, @@ -621,9 +593,6 @@ export class VerticalStackedBarChartBase extends React.Component< const legend: ILegend = { title: point.legend, color, - action: () => { - this._onLegendClick(point.legend); - }, hoverAction: allowHoverOnLegend ? () => { this._handleChartMouseLeave(); From cdde37ef7dabc757f23592edf61607590582bbee Mon Sep 17 00:00:00 2001 From: srmukher <120183316+srmukher@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:43:26 +0000 Subject: [PATCH 06/11] Adding handlers for legend multi select --- .../react-charting/etc/react-charting.api.md | 1 - .../VerticalStackedBarChart.base.tsx | 57 ++++++++++++------- .../VerticalStackedBarChart.types.ts | 6 -- .../VerticalStackedBarChart.Basic.Example.tsx | 3 +- 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/packages/charts/react-charting/etc/react-charting.api.md b/packages/charts/react-charting/etc/react-charting.api.md index 6481fd7695bc1..543e8d624e267 100644 --- a/packages/charts/react-charting/etc/react-charting.api.md +++ b/packages/charts/react-charting/etc/react-charting.api.md @@ -1476,7 +1476,6 @@ export interface IVerticalStackedBarChartProps extends ICartesianChartProps { barMinimumHeight?: number; barWidth?: number | 'default' | 'auto'; calloutProps?: Partial; - canSelectMultipleLegends?: boolean; chartTitle?: string; // @deprecated colors?: string[]; diff --git a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx index 755db0f39b814..ea14b3d7dd38f 100644 --- a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx +++ b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx @@ -419,7 +419,7 @@ export class VerticalStackedBarChartBase extends React.Component< strokeLinecap="round" stroke={lineObject[item][i].color} transform={`translate(${xScaleBandwidthTranslate}, 0)`} - {...(this.state.selectedLegends.includes(item) && { + {...(this._isLegendHighlighted(item) && { onMouseOver: this._lineHover.bind(this, lineObject[item][i - 1]), onMouseLeave: this._lineHoverOut, })} @@ -439,11 +439,11 @@ export class VerticalStackedBarChartBase extends React.Component< circlePoint.useSecondaryYScale && secondaryYScale ? secondaryYScale(circlePoint.y) : yScale(circlePoint.y) } onMouseOver={ - this.state.selectedLegends.includes(item) + this._isLegendHighlighted(item) ? this._lineHover.bind(this, circlePoint) : this._onStackHover.bind(this, circlePoint.xItem) } - {...(this.state.selectedLegends.includes(item) && { + {...(this._isLegendHighlighted(item) && { onMouseLeave: this._lineHoverOut, })} r={this._getCircleVisibilityAndRadius(circlePoint.xItem.xAxisPoint, circlePoint.legend).radius} @@ -476,11 +476,11 @@ export class VerticalStackedBarChartBase extends React.Component< xAxisPoint: string | number | Date, legend: string, ): { visibility: CircleVisbility; radius: number } => { - const { selectedLegends, activeXAxisDataPoint } = this.state; - if (selectedLegends.length > 0) { - if (xAxisPoint === activeXAxisDataPoint && selectedLegends.includes(legend)) { + const { activeXAxisDataPoint } = this.state; + if (!this._noLegendHighlighted()) { + if (xAxisPoint === activeXAxisDataPoint && this._isLegendHighlighted(legend)) { return { visibility: CircleVisbility.show, radius: 8 }; - } else if (selectedLegends.includes(legend)) { + } else if (this._isLegendHighlighted(legend)) { return { visibility: CircleVisbility.show, radius: 0.3 }; } else { return { visibility: CircleVisbility.hide, radius: 0 }; @@ -612,9 +612,6 @@ export class VerticalStackedBarChartBase extends React.Component< title: point.title, color: point.color, isLineLegendInBarChart: true, - action: () => { - this._onLegendClick(point.title); - }, hoverAction: allowHoverOnLegend ? () => { this._handleChartMouseLeave(); @@ -635,14 +632,36 @@ export class VerticalStackedBarChartBase extends React.Component< focusZonePropsInHoverCard={this.props.focusZonePropsForLegendsInHoverCard} overflowText={this.props.legendsOverflowText} {...this.props.legendProps} - onChange={this._onLegendChange} - canSelectMultipleLegends={this.props.canSelectMultipleLegends} + onChange={this._onLegendSelectionChange} /> ); } - private _onLegendChange = (selectedLegends: string[]) => { - this.setState({ selectedLegends }); + private _onLegendSelectionChange = ( + selectedLegends: string[], + event: React.MouseEvent, + currentLegend?: ILegend, + ) => { + if (this.props.legendProps?.canSelectMultipleLegends) { + this.setState({ selectedLegends }); + } else { + this.setState({ selectedLegends: selectedLegends.slice(-1) }); + } + if (this.props.legendProps?.onChange) { + this.props.legendProps.onChange(selectedLegends, event, currentLegend); + } + }; + + private _getHighlightedLegend() { + return this.state.selectedLegends.length > 0 + ? this.state.selectedLegends + : this.state.activeLegend + ? [this.state.activeLegend] + : []; + } + + private _isLegendHighlighted = (legend: string): boolean => { + return this._getHighlightedLegend().indexOf(legend) > -1; }; private _onRectHover( @@ -672,7 +691,7 @@ export class VerticalStackedBarChartBase extends React.Component< * Show the callout if highlighted bar is focused/hovered * and Hide it if unhighlighted bar is focused/hovered */ - isCalloutVisible: this.state.selectedLegends.length === 0 || this.state.selectedLegends.includes(point.legend), + isCalloutVisible: this._noLegendHighlighted() || this._legendHighlighted(point.legend), calloutLegend: point.legend, dataForHoverCard: point.data, color, @@ -726,11 +745,11 @@ export class VerticalStackedBarChartBase extends React.Component< stack: IVerticalStackedChartProps, refSelected: React.MouseEvent | SVGGElement, ): void { - if (this.state.selectedLegends.length > 1) { + if (!this._noLegendHighlighted()) { stack = { ...stack, - chartData: stack.chartData.filter(dataPoint => this.state.selectedLegends.includes(dataPoint.legend)), - lineData: stack.lineData?.filter(dataPoint => this.state.selectedLegends.includes(dataPoint.legend)), + chartData: stack.chartData.filter(dataPoint => this._legendHighlighted(dataPoint.legend)), + lineData: stack.lineData?.filter(dataPoint => this._legendHighlighted(dataPoint.legend)), }; } const lineData = stack.lineData; @@ -1114,7 +1133,7 @@ export class VerticalStackedBarChartBase extends React.Component< * This function checks if none of the legends is selected or hovered. */ private _noLegendHighlighted = () => { - return this.state.selectedLegends.length === 0 && this.state.activeLegend === undefined; + return this._getHighlightedLegend().length === 0; }; private _getAriaLabel = (singleChartData: IVerticalStackedChartProps, point?: IVSChartDataPoint): string => { diff --git a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.types.ts b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.types.ts index f4ee2ce798b66..5e47df049599c 100644 --- a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.types.ts +++ b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.types.ts @@ -155,12 +155,6 @@ export interface IVerticalStackedBarChartProps extends ICartesianChartProps { * The prop used to enable rounded corners for the chart. */ roundCorners?: boolean; - - /** - * The prop used to enable multiple legend selection. - * @default false - */ - canSelectMultipleLegends?: boolean; } /** diff --git a/packages/react-examples/src/react-charting/VerticalStackedBarChart/VerticalStackedBarChart.Basic.Example.tsx b/packages/react-examples/src/react-charting/VerticalStackedBarChart/VerticalStackedBarChart.Basic.Example.tsx index e825a3d91ef11..d90e45872e488 100644 --- a/packages/react-examples/src/react-charting/VerticalStackedBarChart/VerticalStackedBarChart.Basic.Example.tsx +++ b/packages/react-examples/src/react-charting/VerticalStackedBarChart/VerticalStackedBarChart.Basic.Example.tsx @@ -334,6 +334,7 @@ export class VerticalStackedBarChartBasicExample extends React.Component<{}, IVe lineOptions={lineOptions} legendProps={{ allowFocusOnLegends: true, + canSelectMultipleLegends: this.state.legendMultiSelect, }} hideLabels={this.state.hideLabels} enableReflow={true} @@ -341,7 +342,6 @@ export class VerticalStackedBarChartBasicExample extends React.Component<{}, IVe xAxisTitle={this.state.showAxisTitles ? 'Number of days' : undefined} enableGradient={this.state.enableGradient} roundCorners={this.state.roundCorners} - canSelectMultipleLegends={this.state.legendMultiSelect} /> )} @@ -358,6 +358,7 @@ export class VerticalStackedBarChartBasicExample extends React.Component<{}, IVe lineOptions={lineOptions} legendProps={{ allowFocusOnLegends: true, + canSelectMultipleLegends: this.state.legendMultiSelect, }} hideLabels={this.state.hideLabels} enableReflow={true} From d0158eaf67449e3eed390701639732fec6266afb Mon Sep 17 00:00:00 2001 From: srmukher <120183316+srmukher@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:37:57 +0000 Subject: [PATCH 07/11] Fixing tests --- .../VerticalStackedBarChart.base.tsx | 8 ++++---- .../VerticalStackedBarChartRTL.test.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx index ea14b3d7dd38f..6ef0e6f699f2e 100644 --- a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx +++ b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx @@ -632,16 +632,16 @@ export class VerticalStackedBarChartBase extends React.Component< focusZonePropsInHoverCard={this.props.focusZonePropsForLegendsInHoverCard} overflowText={this.props.legendsOverflowText} {...this.props.legendProps} - onChange={this._onLegendSelectionChange} + onChange={this._onLegendSelectionChange.bind(this)} /> ); } - private _onLegendSelectionChange = ( + private _onLegendSelectionChange( selectedLegends: string[], event: React.MouseEvent, currentLegend?: ILegend, - ) => { + ): void { if (this.props.legendProps?.canSelectMultipleLegends) { this.setState({ selectedLegends }); } else { @@ -650,7 +650,7 @@ export class VerticalStackedBarChartBase extends React.Component< if (this.props.legendProps?.onChange) { this.props.legendProps.onChange(selectedLegends, event, currentLegend); } - }; + } private _getHighlightedLegend() { return this.state.selectedLegends.length > 0 diff --git a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChartRTL.test.tsx b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChartRTL.test.tsx index d3a36f9ac55d9..973905fdc35a3 100644 --- a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChartRTL.test.tsx +++ b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChartRTL.test.tsx @@ -503,7 +503,7 @@ describe('Vertical stacked bar chart - Subcomponent Legends', () => { { data: simplePointsWithLine, calloutProps: { doNotLayer: true } }, container => { // eslint-disable-next-line - const handleMouseClick = jest.spyOn(VerticalStackedBarChartBase.prototype as any, '_onLegendClick'); + const handleMouseClick = jest.spyOn(VerticalStackedBarChartBase.prototype as any, '_onLegendSelectionChange'); const legends = screen.getAllByText((content, element) => element!.tagName.toLowerCase() === 'button'); fireEvent.click(legends[0]); // Assert From 24f1360be7ecca5e9a8aafbb460f9fe6aa8a6f6c Mon Sep 17 00:00:00 2001 From: srmukher <120183316+srmukher@users.noreply.github.com> Date: Sun, 22 Dec 2024 18:11:11 +0000 Subject: [PATCH 08/11] Callout to be visible on;y if stack has any data --- .../VerticalStackedBarChart.base.tsx | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx index 6ef0e6f699f2e..bb249aba190c6 100644 --- a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx +++ b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx @@ -282,7 +282,7 @@ export class VerticalStackedBarChartBase extends React.Component< const { isCalloutForStack = false } = this.props; let shouldFocusStackOnly: boolean = false; if (_isHavingLines) { - if (this.state.selectedLegends.length === 1) { + if (this._getHighlightedLegend().length === 1) { shouldFocusStackOnly = false; } else { shouldFocusStackOnly = true; @@ -419,7 +419,7 @@ export class VerticalStackedBarChartBase extends React.Component< strokeLinecap="round" stroke={lineObject[item][i].color} transform={`translate(${xScaleBandwidthTranslate}, 0)`} - {...(this._isLegendHighlighted(item) && { + {...(this._legendHighlighted(item) && { onMouseOver: this._lineHover.bind(this, lineObject[item][i - 1]), onMouseLeave: this._lineHoverOut, })} @@ -439,11 +439,11 @@ export class VerticalStackedBarChartBase extends React.Component< circlePoint.useSecondaryYScale && secondaryYScale ? secondaryYScale(circlePoint.y) : yScale(circlePoint.y) } onMouseOver={ - this._isLegendHighlighted(item) + this._legendHighlighted(item) ? this._lineHover.bind(this, circlePoint) : this._onStackHover.bind(this, circlePoint.xItem) } - {...(this._isLegendHighlighted(item) && { + {...(this._legendHighlighted(item) && { onMouseLeave: this._lineHoverOut, })} r={this._getCircleVisibilityAndRadius(circlePoint.xItem.xAxisPoint, circlePoint.legend).radius} @@ -478,9 +478,9 @@ export class VerticalStackedBarChartBase extends React.Component< ): { visibility: CircleVisbility; radius: number } => { const { activeXAxisDataPoint } = this.state; if (!this._noLegendHighlighted()) { - if (xAxisPoint === activeXAxisDataPoint && this._isLegendHighlighted(legend)) { + if (xAxisPoint === activeXAxisDataPoint && this._legendHighlighted(legend)) { return { visibility: CircleVisbility.show, radius: 8 }; - } else if (this._isLegendHighlighted(legend)) { + } else if (this._legendHighlighted(legend)) { return { visibility: CircleVisbility.show, radius: 0.3 }; } else { return { visibility: CircleVisbility.hide, radius: 0 }; @@ -660,10 +660,6 @@ export class VerticalStackedBarChartBase extends React.Component< : []; } - private _isLegendHighlighted = (legend: string): boolean => { - return this._getHighlightedLegend().indexOf(legend) > -1; - }; - private _onRectHover( xAxisPoint: string, point: IVSChartDataPoint, @@ -763,7 +759,7 @@ export class VerticalStackedBarChartBase extends React.Component< this.setState({ refSelected, - isCalloutVisible: true, + isCalloutVisible: stack.chartData.length > 0 || (stack.lineData?.length ?? 0) > 0, YValueHover: isLinesPresent ? [...lineData!.sort((a, b) => (a.data! < b.data! ? 1 : -1)), ...stack.chartData.slice().reverse()] : stack.chartData.slice().reverse(), @@ -1122,10 +1118,11 @@ export class VerticalStackedBarChartBase extends React.Component< * 1. selection: if the user clicks on it * 2. hovering: if there is no selected legend and the user hovers over it */ - private _legendHighlighted = (legendTitle: string) => { + private _legendHighlighted = (legendTitle: string): boolean => { + const highlightedLegends = this._getHighlightedLegend(); return ( - this.state.selectedLegends.includes(legendTitle) || - (this.state.selectedLegends.length === 0 && this.state.activeLegend === legendTitle) + highlightedLegends.includes(legendTitle) || + (highlightedLegends.length === 0 && this.state.activeLegend === legendTitle) ); }; From 2c55a797f0b85cb956f22e1527abff1cd899a9ca Mon Sep 17 00:00:00 2001 From: srmukher <120183316+srmukher@users.noreply.github.com> Date: Sun, 22 Dec 2024 18:11:11 +0000 Subject: [PATCH 09/11] Callout to be visible on;y if stack has any data --- .../VerticalStackedBarChart.base.tsx | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx index 6ef0e6f699f2e..e12c06887639c 100644 --- a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx +++ b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx @@ -282,7 +282,7 @@ export class VerticalStackedBarChartBase extends React.Component< const { isCalloutForStack = false } = this.props; let shouldFocusStackOnly: boolean = false; if (_isHavingLines) { - if (this.state.selectedLegends.length === 1) { + if (this._getHighlightedLegend().length === 1) { shouldFocusStackOnly = false; } else { shouldFocusStackOnly = true; @@ -419,7 +419,7 @@ export class VerticalStackedBarChartBase extends React.Component< strokeLinecap="round" stroke={lineObject[item][i].color} transform={`translate(${xScaleBandwidthTranslate}, 0)`} - {...(this._isLegendHighlighted(item) && { + {...(this._legendHighlighted(item) && { onMouseOver: this._lineHover.bind(this, lineObject[item][i - 1]), onMouseLeave: this._lineHoverOut, })} @@ -439,11 +439,11 @@ export class VerticalStackedBarChartBase extends React.Component< circlePoint.useSecondaryYScale && secondaryYScale ? secondaryYScale(circlePoint.y) : yScale(circlePoint.y) } onMouseOver={ - this._isLegendHighlighted(item) + this._legendHighlighted(item) ? this._lineHover.bind(this, circlePoint) : this._onStackHover.bind(this, circlePoint.xItem) } - {...(this._isLegendHighlighted(item) && { + {...(this._legendHighlighted(item) && { onMouseLeave: this._lineHoverOut, })} r={this._getCircleVisibilityAndRadius(circlePoint.xItem.xAxisPoint, circlePoint.legend).radius} @@ -478,9 +478,9 @@ export class VerticalStackedBarChartBase extends React.Component< ): { visibility: CircleVisbility; radius: number } => { const { activeXAxisDataPoint } = this.state; if (!this._noLegendHighlighted()) { - if (xAxisPoint === activeXAxisDataPoint && this._isLegendHighlighted(legend)) { + if (xAxisPoint === activeXAxisDataPoint && this._legendHighlighted(legend)) { return { visibility: CircleVisbility.show, radius: 8 }; - } else if (this._isLegendHighlighted(legend)) { + } else if (this._legendHighlighted(legend)) { return { visibility: CircleVisbility.show, radius: 0.3 }; } else { return { visibility: CircleVisbility.hide, radius: 0 }; @@ -660,10 +660,6 @@ export class VerticalStackedBarChartBase extends React.Component< : []; } - private _isLegendHighlighted = (legend: string): boolean => { - return this._getHighlightedLegend().indexOf(legend) > -1; - }; - private _onRectHover( xAxisPoint: string, point: IVSChartDataPoint, @@ -763,7 +759,7 @@ export class VerticalStackedBarChartBase extends React.Component< this.setState({ refSelected, - isCalloutVisible: true, + isCalloutVisible: stack.chartData.length > 0 || (stack.lineData?.length ?? 0) > 0, YValueHover: isLinesPresent ? [...lineData!.sort((a, b) => (a.data! < b.data! ? 1 : -1)), ...stack.chartData.slice().reverse()] : stack.chartData.slice().reverse(), @@ -1122,11 +1118,8 @@ export class VerticalStackedBarChartBase extends React.Component< * 1. selection: if the user clicks on it * 2. hovering: if there is no selected legend and the user hovers over it */ - private _legendHighlighted = (legendTitle: string) => { - return ( - this.state.selectedLegends.includes(legendTitle) || - (this.state.selectedLegends.length === 0 && this.state.activeLegend === legendTitle) - ); + private _legendHighlighted = (legendTitle: string): boolean => { + return this._getHighlightedLegend().includes(legendTitle); }; /** From 38000d6a0417e68b127f595bca530deac4c6b9cd Mon Sep 17 00:00:00 2001 From: srmukher <120183316+srmukher@users.noreply.github.com> Date: Tue, 24 Dec 2024 12:38:49 +0000 Subject: [PATCH 10/11] Adding tests for multiple legend selection --- .../VerticalStackedBarChartRTL.test.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChartRTL.test.tsx b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChartRTL.test.tsx index 973905fdc35a3..605c6b47cd75d 100644 --- a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChartRTL.test.tsx +++ b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChartRTL.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent, act } from '@testing-library/react'; +import { render, screen, fireEvent, act, waitFor } from '@testing-library/react'; import * as React from 'react'; import { DarkTheme } from '@fluentui/theme-samples'; import { ThemeProvider, resetIds } from '@fluentui/react'; @@ -510,6 +510,23 @@ describe('Vertical stacked bar chart - Subcomponent Legends', () => { expect(handleMouseClick).toHaveBeenCalled(); }, ); + + testWithoutWait( + 'Should select multiple legends on click', + VerticalStackedBarChart, + { data: simplePoints, legendProps: { canSelectMultipleLegends: true }, calloutProps: { doNotLayer: true } }, + container => { + const firstLegend = screen.queryByText('Metadata1')?.closest('button'); + const secondLegend = screen.queryByText('Metadata2')?.closest('button'); + expect(firstLegend).toBeDefined(); + expect(secondLegend).toBeDefined(); + fireEvent.click(firstLegend!); + fireEvent.click(secondLegend!); + //Assert + expect(firstLegend).toHaveAttribute('aria-selected', 'true'); + expect(secondLegend).toHaveAttribute('aria-selected', 'true'); + }, + ); }); describe('Vertical stacked bar chart - Subcomponent callout', () => { From 714863b15bfc7f9cf38d165114c0c1fda7a4f1e5 Mon Sep 17 00:00:00 2001 From: srmukher <120183316+srmukher@users.noreply.github.com> Date: Thu, 26 Dec 2024 06:01:55 +0000 Subject: [PATCH 11/11] Initializing the selected legends and renaming function --- .../VerticalStackedBarChart.base.tsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx index e12c06887639c..e96c1aeacb6e4 100644 --- a/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx +++ b/packages/charts/react-charting/src/components/VerticalStackedBarChart/VerticalStackedBarChart.base.tsx @@ -129,7 +129,7 @@ export class VerticalStackedBarChartBase extends React.Component< super(props); this.state = { isCalloutVisible: false, - selectedLegends: [], + selectedLegends: props.legendProps?.selectedLegend ? [props.legendProps.selectedLegend] : [], activeLegend: undefined, refSelected: null, dataForHoverCard: 0, @@ -381,7 +381,7 @@ export class VerticalStackedBarChartBase extends React.Component< const xScaleBandwidthTranslate = this._xAxisType !== XAxisTypes.StringAxis ? 0 : xScale.bandwidth() / 2; Object.keys(lineObject).forEach((item: string, index: number) => { - const shouldHighlight = this._legendHighlighted(item) || this._noLegendHighlighted(); // item is legend name + const shouldHighlight = this._isLegendHighlighted(item) || this._noLegendHighlighted(); // item is legend name for (let i = 1; i < lineObject[item].length; i++) { const x1 = xScale(lineObject[item][i - 1].xItem.xAxisPoint); const useSecondaryYScale = @@ -419,7 +419,7 @@ export class VerticalStackedBarChartBase extends React.Component< strokeLinecap="round" stroke={lineObject[item][i].color} transform={`translate(${xScaleBandwidthTranslate}, 0)`} - {...(this._legendHighlighted(item) && { + {...(this._isLegendHighlighted(item) && { onMouseOver: this._lineHover.bind(this, lineObject[item][i - 1]), onMouseLeave: this._lineHoverOut, })} @@ -439,11 +439,11 @@ export class VerticalStackedBarChartBase extends React.Component< circlePoint.useSecondaryYScale && secondaryYScale ? secondaryYScale(circlePoint.y) : yScale(circlePoint.y) } onMouseOver={ - this._legendHighlighted(item) + this._isLegendHighlighted(item) ? this._lineHover.bind(this, circlePoint) : this._onStackHover.bind(this, circlePoint.xItem) } - {...(this._legendHighlighted(item) && { + {...(this._isLegendHighlighted(item) && { onMouseLeave: this._lineHoverOut, })} r={this._getCircleVisibilityAndRadius(circlePoint.xItem.xAxisPoint, circlePoint.legend).radius} @@ -455,7 +455,7 @@ export class VerticalStackedBarChartBase extends React.Component< // When no legend is highlighted: Line points are automatically displayed along with the bars // at the same x-axis point in the stack callout. So to prevent an increase in focusable elements // and avoid conveying duplicate info, make these line points non-focusable. - data-is-focusable={this._legendHighlighted(item)} + data-is-focusable={this._isLegendHighlighted(item)} ref={e => (circleRef.refElement = e)} onFocus={this._lineFocus.bind(this, circlePoint, circleRef)} onBlur={this._lineHoverOut} @@ -478,9 +478,9 @@ export class VerticalStackedBarChartBase extends React.Component< ): { visibility: CircleVisbility; radius: number } => { const { activeXAxisDataPoint } = this.state; if (!this._noLegendHighlighted()) { - if (xAxisPoint === activeXAxisDataPoint && this._legendHighlighted(legend)) { + if (xAxisPoint === activeXAxisDataPoint && this._isLegendHighlighted(legend)) { return { visibility: CircleVisbility.show, radius: 8 }; - } else if (this._legendHighlighted(legend)) { + } else if (this._isLegendHighlighted(legend)) { return { visibility: CircleVisbility.show, radius: 0.3 }; } else { return { visibility: CircleVisbility.hide, radius: 0 }; @@ -687,7 +687,7 @@ export class VerticalStackedBarChartBase extends React.Component< * Show the callout if highlighted bar is focused/hovered * and Hide it if unhighlighted bar is focused/hovered */ - isCalloutVisible: this._noLegendHighlighted() || this._legendHighlighted(point.legend), + isCalloutVisible: this._noLegendHighlighted() || this._isLegendHighlighted(point.legend), calloutLegend: point.legend, dataForHoverCard: point.data, color, @@ -744,8 +744,8 @@ export class VerticalStackedBarChartBase extends React.Component< if (!this._noLegendHighlighted()) { stack = { ...stack, - chartData: stack.chartData.filter(dataPoint => this._legendHighlighted(dataPoint.legend)), - lineData: stack.lineData?.filter(dataPoint => this._legendHighlighted(dataPoint.legend)), + chartData: stack.chartData.filter(dataPoint => this._isLegendHighlighted(dataPoint.legend)), + lineData: stack.lineData?.filter(dataPoint => this._isLegendHighlighted(dataPoint.legend)), }; } const lineData = stack.lineData; @@ -885,7 +885,7 @@ export class VerticalStackedBarChartBase extends React.Component< const ref: IRefArrayData = {}; - const shouldHighlight = this._legendHighlighted(point.legend) || this._noLegendHighlighted() ? true : false; + const shouldHighlight = this._isLegendHighlighted(point.legend) || this._noLegendHighlighted() ? true : false; this._classNames = getClassNames(this.props.styles!, { theme: this.props.theme!, shouldHighlight, @@ -992,7 +992,7 @@ export class VerticalStackedBarChartBase extends React.Component< barLabel = barTotalValue; } else { barsToDisplay.forEach(point => { - if (this._legendHighlighted(point.legend)) { + if (this._isLegendHighlighted(point.legend)) { showLabel = true; barLabel += point.data; } @@ -1118,7 +1118,7 @@ export class VerticalStackedBarChartBase extends React.Component< * 1. selection: if the user clicks on it * 2. hovering: if there is no selected legend and the user hovers over it */ - private _legendHighlighted = (legendTitle: string): boolean => { + private _isLegendHighlighted = (legendTitle: string): boolean => { return this._getHighlightedLegend().includes(legendTitle); };