Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[VSBC] Support for multiple legend selection for Vertical Stacked Bar Chart #33466

Merged
merged 19 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bc4577d
Support for multiple legend selection for Vertical Stacked Bar Chart
srmukher Dec 13, 2024
216f82e
Adding change file
srmukher Dec 13, 2024
0235b7f
Adding example for legend multi select for VSBC
srmukher Dec 13, 2024
0505415
Handle legend click in VSBC
srmukher Dec 16, 2024
980d1aa
Removing legend click logic as it is handled in legend component
srmukher Dec 16, 2024
cdde37e
Adding handlers for legend multi select
srmukher Dec 19, 2024
672ba92
Merge branch 'master' of https://github.com/microsoft/fluentui into u…
srmukher Dec 19, 2024
9df11b9
Merge branch 'master' into users/srmukher/legendsMultiSelectVBC
srmukher Dec 19, 2024
d0158ea
Fixing tests
srmukher Dec 19, 2024
ba28aee
Merge branch 'users/srmukher/legendsMultiSelectVBC' of https://github…
srmukher Dec 19, 2024
24f1360
Callout to be visible on;y if stack has any data
srmukher Dec 22, 2024
395c027
Merge branch 'master' into users/srmukher/legendsMultiSelectVBC
srmukher Dec 22, 2024
2c55a79
Callout to be visible on;y if stack has any data
srmukher Dec 22, 2024
38000d6
Adding tests for multiple legend selection
srmukher Dec 24, 2024
b7b36ff
Merge branch 'users/srmukher/legendsMultiSelectVBC' of https://github…
srmukher Dec 24, 2024
5c071bc
Merge branch 'master' of https://github.com/microsoft/fluentui into u…
srmukher Dec 24, 2024
714863b
Initializing the selected legends and renaming function
srmukher Dec 26, 2024
3dc2179
Merge branch 'users/srmukher/legendsMultiSelectVBC' of https://github…
srmukher Dec 26, 2024
bc8cd16
Merge branch 'master' into users/srmukher/legendsMultiSelectVBC
srmukher Dec 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
srmukher marked this conversation as resolved.
Show resolved Hide resolved
"type": "patch",
"comment": "Support for multiple legend selection for Vertical Stacked Bar Chart ",
"packageName": "@fluentui/react-charting",
"email": "[email protected]",
"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 @@ -299,7 +300,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 @@ -398,7 +399,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 @@ -436,7 +437,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 @@ -456,11 +457,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 @@ -472,7 +473,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 @@ -493,11 +494,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 @@ -567,18 +568,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 @@ -587,7 +576,7 @@ export class VerticalStackedBarChartBase

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

Expand Down Expand Up @@ -622,9 +611,6 @@ export class VerticalStackedBarChartBase
const legend: ILegend = {
title: point.legend,
color,
action: () => {
this._onLegendClick(point.legend);
},
hoverAction: allowHoverOnLegend
? () => {
this._handleChartMouseLeave();
Expand All @@ -644,9 +630,6 @@ export class VerticalStackedBarChartBase
title: point.title,
color: point.color,
isLineLegendInBarChart: true,
action: () => {
this._onLegendClick(point.title);
},
hoverAction: allowHoverOnLegend
? () => {
this._handleChartMouseLeave();
Expand All @@ -667,10 +650,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 @@ -698,7 +705,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 @@ -752,6 +759,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 @@ -760,9 +774,10 @@ export class VerticalStackedBarChartBase
item.shouldDrawBorderBottom = true;
});
}

this.setState({
refSelected,
isCalloutVisible: true,
isCalloutVisible: stack.chartData.length > 0 || (stack.lineData?.length ?? 0) > 0,
srmukher marked this conversation as resolved.
Show resolved Hide resolved
YValueHover: isLinesPresent
? [...lineData!.sort((a, b) => (a.data! < b.data! ? 1 : -1)), ...stack.chartData.slice().reverse()]
: stack.chartData.slice().reverse(),
Expand Down Expand Up @@ -888,7 +903,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 @@ -995,7 +1010,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 @@ -1121,18 +1136,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
Loading