Skip to content

Commit

Permalink
[VBC] Select multiple legends for Vertical bar chart (#33510)
Browse files Browse the repository at this point in the history
  • Loading branch information
srmukher authored Dec 27, 2024
1 parent 18396b7 commit fc4f676
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 55 deletions.
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 @@ -142,12 +142,18 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
[exportAsImage],
);

const multiSelectLegendProps = {
...legendProps,
canSelectMultipleLegends: true,
selectedLegends: activeLegends,
};

switch (data[0].type) {
case 'pie':
return (
<DonutChart
{...transformPlotlyJsonToDonutProps(plotlySchema, colorMap, isDarkTheme)}
legendProps={{ ...legendProps, canSelectMultipleLegends: true, selectedLegends: activeLegends }}
legendProps={multiSelectLegendProps}
componentRef={chartRef}
// Bubble event to prevent right click to open menu on the callout
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
Expand All @@ -169,7 +175,7 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
return (
<GroupedVerticalBarChart
{...transformPlotlyJsonToGVBCProps(plotlySchema, colorMap, isDarkTheme)}
legendProps={{ ...legendProps, canSelectMultipleLegends: true, selectedLegends: activeLegends }}
legendProps={multiSelectLegendProps}
componentRef={chartRef}
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
/>
Expand All @@ -178,7 +184,7 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
return (
<VerticalStackedBarChart
{...transformPlotlyJsonToVSBCProps(plotlySchema, colorMap, isDarkTheme)}
legendProps={{ ...legendProps, canSelectMultipleLegends: true, selectedLegends: activeLegends }}
legendProps={multiSelectLegendProps}
componentRef={chartRef}
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
/>
Expand Down Expand Up @@ -227,7 +233,7 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
return (
<VerticalStackedBarChart
{...transformPlotlyJsonToVSBCProps(plotlySchema, colorMap, isDarkTheme)}
legendProps={{ ...legendProps, canSelectMultipleLegends: true, selectedLegends: activeLegends }}
legendProps={multiSelectLegendProps}
componentRef={chartRef}
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
/>
Expand Down Expand Up @@ -265,7 +271,7 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
return (
<VerticalBarChart
{...transformPlotlyJsonToVBCProps(plotlySchema, colorMap, isDarkTheme)}
legendProps={legendProps}
legendProps={multiSelectLegendProps}
componentRef={chartRef}
calloutProps={{ layerProps: { eventBubblingEnabled: true } }}
/>
Expand Down
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;
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 || [],
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');
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

0 comments on commit fc4f676

Please sign in to comment.