Skip to content

Commit

Permalink
[VSBC] Support for multiple legend selection for Vertical Stacked Bar…
Browse files Browse the repository at this point in the history
… Chart (#33466)
  • Loading branch information
srmukher authored Dec 26, 2024
1 parent 37e5f0b commit 864f029
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export interface IVerticalStackedBarChartState extends IBasestate {
activeXAxisDataPoint: number | string | Date;
callOutAccessibilityData?: IAccessibilityProps;
calloutLegend: string;
selectedLegends: string[];
}
export class VerticalStackedBarChartBase
extends React.Component<IVerticalStackedBarChartProps, IVerticalStackedBarChartState>
Expand Down Expand Up @@ -140,8 +141,8 @@ export class VerticalStackedBarChartBase

this.state = {
isCalloutVisible: false,
selectedLegend: props.legendProps?.selectedLegend ?? '',
activeLegend: '',
selectedLegends: props.legendProps?.selectedLegends || [],
activeLegend: undefined,
refSelected: null,
dataForHoverCard: 0,
color: '',
Expand Down Expand Up @@ -305,7 +306,7 @@ export class VerticalStackedBarChartBase
const { isCalloutForStack = false } = this.props;
let shouldFocusStackOnly: boolean = false;
if (_isHavingLines) {
if (this.state.selectedLegend !== '') {
if (this._getHighlightedLegend().length === 1) {
shouldFocusStackOnly = false;
} else {
shouldFocusStackOnly = true;
Expand Down Expand Up @@ -404,7 +405,7 @@ export class VerticalStackedBarChartBase

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 =
Expand Down Expand Up @@ -442,7 +443,7 @@ export class VerticalStackedBarChartBase
strokeLinecap="round"
stroke={lineObject[item][i].color}
transform={`translate(${xScaleBandwidthTranslate}, 0)`}
{...(this.state.selectedLegend === item && {
{...(this._isLegendHighlighted(item) && {
onMouseOver: this._lineHover.bind(this, lineObject[item][i - 1]),
onMouseLeave: this._lineHoverOut,
})}
Expand All @@ -462,11 +463,11 @@ export class VerticalStackedBarChartBase
circlePoint.useSecondaryYScale && secondaryYScale ? secondaryYScale(circlePoint.y) : yScale(circlePoint.y)
}
onMouseOver={
this.state.selectedLegend === item
this._isLegendHighlighted(item)
? this._lineHover.bind(this, circlePoint)
: this._onStackHover.bind(this, circlePoint.xItem)
}
{...(this.state.selectedLegend === item && {
{...(this._isLegendHighlighted(item) && {
onMouseLeave: this._lineHoverOut,
})}
r={this._getCircleVisibilityAndRadius(circlePoint.xItem.xAxisPoint, circlePoint.legend).radius}
Expand All @@ -478,7 +479,7 @@ export class VerticalStackedBarChartBase
// 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}
Expand All @@ -499,11 +500,11 @@ export class VerticalStackedBarChartBase
xAxisPoint: string | number | Date,
legend: string,
): { visibility: CircleVisbility; radius: number } => {
const { selectedLegend, activeXAxisDataPoint } = this.state;
if (selectedLegend !== '') {
if (xAxisPoint === activeXAxisDataPoint && selectedLegend === legend) {
const { activeXAxisDataPoint } = this.state;
if (!this._noLegendHighlighted()) {
if (xAxisPoint === activeXAxisDataPoint && this._isLegendHighlighted(legend)) {
return { visibility: CircleVisbility.show, radius: 8 };
} else if (selectedLegend === legend) {
} else if (this._isLegendHighlighted(legend)) {
return { visibility: CircleVisbility.show, radius: 0.3 };
} else {
return { visibility: CircleVisbility.hide, radius: 0 };
Expand Down Expand Up @@ -573,18 +574,6 @@ export class VerticalStackedBarChartBase
: null;
};

private _onLegendClick(legendTitle: string): void {
if (this.state.selectedLegend === legendTitle) {
this.setState({
selectedLegend: '',
});
} else {
this.setState({
selectedLegend: legendTitle,
});
}
}

private _onLegendHover(legendTitle: string): void {
this.setState({
activeLegend: legendTitle,
Expand All @@ -593,7 +582,7 @@ export class VerticalStackedBarChartBase

private _onLegendLeave(): void {
this.setState({
activeLegend: '',
activeLegend: undefined,
});
}

Expand Down Expand Up @@ -628,9 +617,6 @@ export class VerticalStackedBarChartBase
const legend: ILegend = {
title: point.legend,
color,
action: () => {
this._onLegendClick(point.legend);
},
hoverAction: allowHoverOnLegend
? () => {
this._handleChartMouseLeave();
Expand All @@ -650,9 +636,6 @@ export class VerticalStackedBarChartBase
title: point.title,
color: point.color,
isLineLegendInBarChart: true,
action: () => {
this._onLegendClick(point.title);
},
hoverAction: allowHoverOnLegend
? () => {
this._handleChartMouseLeave();
Expand All @@ -673,10 +656,34 @@ export class VerticalStackedBarChartBase
focusZonePropsInHoverCard={this.props.focusZonePropsForLegendsInHoverCard}
overflowText={this.props.legendsOverflowText}
{...this.props.legendProps}
onChange={this._onLegendSelectionChange.bind(this)}
/>
);
}

private _onLegendSelectionChange(
selectedLegends: string[],
event: React.MouseEvent<HTMLButtonElement>,
currentLegend?: ILegend,
): void {
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 _onRectHover(
xAxisPoint: string,
point: IVSChartDataPoint,
Expand Down Expand Up @@ -704,7 +711,7 @@ export class VerticalStackedBarChartBase
* 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._noLegendHighlighted() || this._isLegendHighlighted(point.legend),
calloutLegend: point.legend,
dataForHoverCard: point.data,
color,
Expand Down Expand Up @@ -758,6 +765,13 @@ export class VerticalStackedBarChartBase
stack: IVerticalStackedChartProps,
refSelected: React.MouseEvent<SVGElement> | SVGGElement,
): void {
if (!this._noLegendHighlighted()) {
stack = {
...stack,
chartData: stack.chartData.filter(dataPoint => this._isLegendHighlighted(dataPoint.legend)),
lineData: stack.lineData?.filter(dataPoint => this._isLegendHighlighted(dataPoint.legend)),
};
}
const lineData = stack.lineData;
const isLinesPresent: boolean = lineData !== undefined && lineData.length > 0;
if (isLinesPresent) {
Expand All @@ -766,9 +780,10 @@ export class VerticalStackedBarChartBase
item.shouldDrawBorderBottom = true;
});
}

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(),
Expand Down Expand Up @@ -894,7 +909,7 @@ export class VerticalStackedBarChartBase

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,
Expand Down Expand Up @@ -1001,7 +1016,7 @@ export class VerticalStackedBarChartBase
barLabel = barTotalValue;
} else {
barsToDisplay.forEach(point => {
if (this._legendHighlighted(point.legend)) {
if (this._isLegendHighlighted(point.legend)) {
showLabel = true;
barLabel += point.data;
}
Expand Down Expand Up @@ -1127,18 +1142,15 @@ export class VerticalStackedBarChartBase
* 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.selectedLegend === legendTitle ||
(this.state.selectedLegend === '' && this.state.activeLegend === legendTitle)
);
private _isLegendHighlighted = (legendTitle: string): boolean => {
return this._getHighlightedLegend().includes(legendTitle);
};

/**
* This function checks if none of the legends is selected or hovered.
*/
private _noLegendHighlighted = () => {
return this.state.selectedLegend === '' && this.state.activeLegend === '';
return this._getHighlightedLegend().length === 0;
};

private _getAriaLabel = (singleChartData: IVerticalStackedChartProps, point?: IVSChartDataPoint): string => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -503,13 +503,30 @@ 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
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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface IVerticalStackedBarState {
margins: {};
enableGradient: boolean;
roundCorners: boolean;
legendMultiSelect: boolean;
}

export class VerticalStackedBarChartBasicExample extends React.Component<{}, IVerticalStackedBarState> {
Expand All @@ -41,6 +42,7 @@ export class VerticalStackedBarChartBasicExample extends React.Component<{}, IVe
},
enableGradient: false,
roundCorners: false,
legendMultiSelect: false,
};
}

Expand Down Expand Up @@ -94,6 +96,10 @@ export class VerticalStackedBarChartBasicExample extends React.Component<{}, IVe
this.setState({ roundCorners: checked });
};

private _onToggleLegendMultiSelect = (ev: React.MouseEvent<HTMLElement>, checked: boolean) => {
this.setState({ legendMultiSelect: checked });
};

private _basicExample(): JSX.Element {
const { showLine } = this.state;
const firstChartPoints: IVSChartDataPoint[] = [
Expand Down Expand Up @@ -307,6 +313,13 @@ export class VerticalStackedBarChartBasicExample extends React.Component<{}, IVe
<Toggle label="Enable Gradient" onText="ON" offText="OFF" onChange={this._onEnableGradientChange} />
&nbsp;&nbsp;
<Toggle label="Rounded Corners" onText="ON" offText="OFF" onChange={this._onRoundCornersChange} />
&nbsp;&nbsp;
<Toggle
label="Select Multiple Legends"
onText="ON"
offText="OFF"
onChange={this._onToggleLegendMultiSelect}
/>
</div>
{this.state.showAxisTitles && (
<div style={rootStyle}>
Expand All @@ -321,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}
Expand All @@ -344,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}
Expand Down

0 comments on commit 864f029

Please sign in to comment.