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

[VBC] Select multiple legends for Vertical bar chart #33510

Merged
merged 14 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
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 @@
{
"type": "patch",
"comment": "Select multiple legends for Vertical 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 @@ -52,6 +52,7 @@ import {
createStringYAxis,
formatDate,
getNextGradient,
areArraysEqual,
} from '../../utilities/index';
import { IChart } from '../../types/index';

Expand All @@ -73,6 +74,7 @@ export interface IVerticalBarChartState extends IBasestate {
hoverXValue?: string | number | null;
srmukher marked this conversation as resolved.
Show resolved Hide resolved
callOutAccessibilityData?: IAccessibilityProps;
calloutLegend: string;
selectedLegends: string[];
}

type ColorScale = (_p?: number) => string;
Expand Down Expand Up @@ -118,8 +120,8 @@ export class VerticalBarChartBase
dataForHoverCard: 0,
isCalloutVisible: false,
refSelected: null,
selectedLegend: props.legendProps?.selectedLegend ?? '',
activeLegend: '',
selectedLegends: props.legendProps?.selectedLegends || [],
srmukher marked this conversation as resolved.
Show resolved Hide resolved
activeLegend: undefined,
xCalloutValue: '',
yCalloutValue: '',
activeXdataPoint: null,
Expand All @@ -141,9 +143,9 @@ export class VerticalBarChartBase
}

public componentDidUpdate(prevProps: IVerticalBarChartProps): void {
if (prevProps.legendProps?.selectedLegend !== this.props.legendProps?.selectedLegend) {
if (!areArraysEqual(prevProps.legendProps?.selectedLegends, this.props.legendProps?.selectedLegends)) {
this.setState({
selectedLegend: this.props.legendProps?.selectedLegend ?? '',
selectedLegends: this.props.legendProps?.selectedLegends || [],
});
}
}
Expand Down Expand Up @@ -199,7 +201,8 @@ export class VerticalBarChartBase
createYAxis={createNumericYAxis}
calloutProps={calloutProps}
tickParams={tickParams}
{...(this._isHavingLine && this._noLegendHighlighted() && { isCalloutForStack: true })}
{...(this._isHavingLine &&
(this._noLegendHighlighted() || this._getHighlightedLegend().length > 1) && { isCalloutForStack: true })}
legendBars={legendBars}
datasetForXAxisDomain={this._xAxisLabels}
barwidth={this._barWidth}
Expand Down Expand Up @@ -404,11 +407,11 @@ export class VerticalBarChartBase
xAxisPoint: string | number | Date,
legend: string,
): { visibility: CircleVisbility; radius: number } => {
const { selectedLegend, activeXdataPoint } = this.state;
if (selectedLegend !== '') {
if (xAxisPoint === activeXdataPoint && selectedLegend === legend) {
const { activeXdataPoint } = this.state;
if (!this._noLegendHighlighted()) {
if (xAxisPoint === activeXdataPoint && this._legendHighlighted(legend)) {
return { visibility: CircleVisbility.show, radius: 8 };
} else if (selectedLegend === legend) {
} else if (this._legendHighlighted(legend)) {
// Don't hide the circle to keep it focusable. For more information,
// see https://fuzzbomb.github.io/accessibility-demos/visually-hidden-focus-test.html
return { visibility: CircleVisbility.show, radius: 0.3 };
Expand Down Expand Up @@ -539,7 +542,11 @@ export class VerticalBarChartBase
: this._createColors()(1);

// there might be no y value of the line for the hovered bar. so we need to check this condition
if (this._isHavingLine && selectedPoint[0].lineData?.y !== undefined) {
if (
this._isHavingLine &&
selectedPoint[0].lineData?.y !== undefined &&
(this._legendHighlighted(lineLegendText) || this._noLegendHighlighted())
) {
// callout data for the line
YValueHover.push({
legend: lineLegendText,
Expand All @@ -549,18 +556,20 @@ export class VerticalBarChartBase
yAxisCalloutData: selectedPoint[0].lineData?.yAxisCalloutData,
});
}
// callout data for the bar
YValueHover.push({
legend: selectedPoint[0].legend,
y: selectedPoint[0].y,
color: enableGradient
? useSingleColor
? getNextGradient(0, 0)[0]
: selectedPoint[0].gradient?.[0] || getNextGradient(pointIndex, 0)[0]
: calloutColor,
data: selectedPoint[0].yAxisCalloutData,
yAxisCalloutData: selectedPoint[0].yAxisCalloutData,
});
if (this._legendHighlighted(selectedPoint[0].legend) || this._noLegendHighlighted()) {
// callout data for the bar
YValueHover.push({
legend: selectedPoint[0].legend,
y: selectedPoint[0].y,
color: enableGradient
? useSingleColor
? getNextGradient(0, 0)[0]
: selectedPoint[0].gradient?.[0] || getNextGradient(pointIndex, 0)[0]
: calloutColor,
data: selectedPoint[0].yAxisCalloutData,
yAxisCalloutData: selectedPoint[0].yAxisCalloutData,
});
}
const hoverXValue = point.x instanceof Date ? formatDate(point.x, this.props.useUTC) : point.x.toString();
return {
YValueHover,
Expand All @@ -581,7 +590,7 @@ export class VerticalBarChartBase
this.setState({
refSelected: mouseEvent,
/** Show the callout if highlighted bar is hovered and Hide it if unhighlighted bar is hovered */
isCalloutVisible: this.state.selectedLegend === '' || this.state.selectedLegend === point.legend,
isCalloutVisible: this._noLegendHighlighted() || this._legendHighlighted(point.legend),
dataForHoverCard: point.y,
calloutLegend: point.legend!,
color: point.color || color,
Expand All @@ -592,7 +601,7 @@ export class VerticalBarChartBase
yCalloutValue: point.yAxisCalloutData!,
dataPointCalloutProps: point,
// Hovering over a bar should highlight corresponding line points only when no legend is selected
activeXdataPoint: this._noLegendHighlighted() ? point.x : null,
activeXdataPoint: this._noLegendHighlighted() || this._legendHighlighted(point.legend) ? point.x : null,
YValueHover,
hoverXValue,
callOutAccessibilityData: point.callOutAccessibilityData,
Expand Down Expand Up @@ -621,7 +630,7 @@ export class VerticalBarChartBase
this.setState({
refSelected: obj.refElement,
/** Show the callout if highlighted bar is focused and Hide it if unhighlighted bar is focused */
isCalloutVisible: this.state.selectedLegend === '' || this.state.selectedLegend === point.legend,
isCalloutVisible: this._noLegendHighlighted() || this._legendHighlighted(point.legend),
calloutLegend: point.legend!,
dataForHoverCard: point.y,
color: point.color || color,
Expand Down Expand Up @@ -1059,18 +1068,6 @@ export class VerticalBarChartBase
});
};

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 @@ -1079,7 +1076,7 @@ export class VerticalBarChartBase

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

Expand All @@ -1106,9 +1103,6 @@ export class VerticalBarChartBase
const legend: ILegend = {
title: legendTitle,
color,
action: () => {
this._onLegendClick(legendTitle);
},
hoverAction: () => {
this._handleChartMouseLeave();
this._onLegendHover(legendTitle);
Expand All @@ -1123,9 +1117,6 @@ export class VerticalBarChartBase
const lineLegend: ILegend = {
title: lineLegendText,
color: lineLegendColor,
action: () => {
this._onLegendClick(lineLegendText);
},
hoverAction: () => {
this._handleChartMouseLeave();
this._onLegendHover(lineLegendText);
Expand All @@ -1145,11 +1136,27 @@ export class VerticalBarChartBase
focusZonePropsInHoverCard={this.props.focusZonePropsForLegendsInHoverCard}
overflowText={this.props.legendsOverflowText}
{...this.props.legendProps}
onChange={this._onLegendSelectionChange.bind(this)}
/>
);
return legends;
};

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 _getAxisData = (yAxisData: IAxisData) => {
if (yAxisData && yAxisData.yAxisDomainValues.length) {
const { yAxisDomainValues: domainValue } = yAxisData;
Expand All @@ -1164,20 +1171,25 @@ export class VerticalBarChartBase
* 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 _legendHighlighted = (legendTitle: string | undefined) => {
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 _getHighlightedLegend() {
return this.state.selectedLegends.length > 0
? this.state.selectedLegends
: this.state.activeLegend
? [this.state.activeLegend]
: [];
}

private _getAriaLabel = (point: IVerticalBarChartDataPoint): string => {
const xValue = point.xAxisCalloutData
? point.xAxisCalloutData
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,87 @@ describe('Vertical bar chart - Subcomponent Legends', () => {
expect(bars[7]).toHaveStyle('opacity: 0.1');
},
);

testWithWait(
'Should reduce the opacity of the other bars/lines and their legends on mouse over multiple legends',
VerticalBarChart,
{ data: pointsWithLine, lineLegendText: 'just line', legendProps: { canSelectMultipleLegends: true } },
container => {
const bars = getById(container, /_VBC_bar/i);
const line = getById(container, /_VBC_line/i)[0];
const legends = screen.getAllByText((content, element) => element!.tagName.toLowerCase() === 'button');
expect(line).toBeDefined();
expect(bars).toHaveLength(8);
expect(legends).toHaveLength(9);
fireEvent.click(screen.getByText('just line'));
fireEvent.click(screen.getByText('Oranges'));
expect(line.getAttribute('opacity')).toEqual('1');
expect(screen.getByText('Oranges')).not.toHaveAttribute('opacity');
expect(screen.getByText('Dogs')).toHaveStyle('opacity: 0.67');
srmukher marked this conversation as resolved.
Show resolved Hide resolved
expect(screen.getByText('Apples')).toHaveStyle('opacity: 0.67');
expect(screen.getByText('Bananas')).toHaveStyle('opacity: 0.67');
expect(screen.getByText('Giraffes')).toHaveStyle('opacity: 0.67');
expect(screen.getByText('Cats')).toHaveStyle('opacity: 0.67');
expect(screen.getByText('Elephants')).toHaveStyle('opacity: 0.67');
expect(screen.getByText('Monkeys')).toHaveStyle('opacity: 0.67');
expect(line).toBeDefined();
expect(bars[0]).toBeDefined();
expect(bars[0]).not.toHaveAttribute('opacity');
expect(bars[1]).toBeDefined();
expect(bars[1]).toHaveStyle('opacity: 0.1');
expect(bars[2]).toBeDefined();
expect(bars[2]).toHaveStyle('opacity: 0.1');
expect(bars[3]).toBeDefined();
expect(bars[3]).toHaveStyle('opacity: 0.1');
expect(bars[4]).toBeDefined();
expect(bars[4]).toHaveStyle('opacity: 0.1');
expect(bars[5]).toBeDefined();
expect(bars[5]).toHaveStyle('opacity: 0.1');
expect(bars[6]).toBeDefined();
expect(bars[6]).toHaveStyle('opacity: 0.1');
expect(bars[7]).toBeDefined();
expect(bars[7]).toHaveStyle('opacity: 0.1');
},
);

testWithWait(
'Should reduce the opacity of the other bars/lines and their legends on mouse over multiple legends',
VerticalBarChart,
{ data: pointsWithLine, lineLegendText: 'just line', legendProps: { canSelectMultipleLegends: true } },
container => {
const bars = getById(container, /_VBC_bar/i);
const line = getById(container, /_VBC_line/i)[0];
const legends = screen.getAllByText((content, element) => element!.tagName.toLowerCase() === 'button');
expect(line).toBeDefined();
expect(bars).toHaveLength(8);
expect(legends).toHaveLength(9);
fireEvent.click(screen.getByText('just line'));
fireEvent.click(screen.getByText('Oranges'));
expect(line.getAttribute('opacity')).toEqual('1');
expect(screen.getByText('Dogs')).toHaveStyle('opacity: 0.67');
expect(screen.getByText('Apples')).toHaveStyle('opacity: 0.67');
expect(screen.getByText('Bananas')).toHaveStyle('opacity: 0.67');
expect(screen.getByText('Giraffes')).toHaveStyle('opacity: 0.67');
expect(screen.getByText('Cats')).toHaveStyle('opacity: 0.67');
expect(screen.getByText('Elephants')).toHaveStyle('opacity: 0.67');
expect(screen.getByText('Monkeys')).toHaveStyle('opacity: 0.67');
expect(line).toBeDefined();
expect(bars[1]).toBeDefined();
expect(bars[1]).toHaveStyle('opacity: 0.1');
expect(bars[2]).toBeDefined();
expect(bars[2]).toHaveStyle('opacity: 0.1');
expect(bars[3]).toBeDefined();
expect(bars[3]).toHaveStyle('opacity: 0.1');
expect(bars[4]).toBeDefined();
expect(bars[4]).toHaveStyle('opacity: 0.1');
expect(bars[5]).toBeDefined();
expect(bars[5]).toHaveStyle('opacity: 0.1');
expect(bars[6]).toBeDefined();
expect(bars[6]).toHaveStyle('opacity: 0.1');
expect(bars[7]).toBeDefined();
expect(bars[7]).toHaveStyle('opacity: 0.1');
},
);
});

describe('Vertical bar chart - Subcomponent callout', () => {
Expand Down
Loading
Loading