Skip to content

Commit

Permalink
[Horizontal Bar Chart With Axis] Enable multiple legend selection (#3…
Browse files Browse the repository at this point in the history
  • Loading branch information
srmukher authored Dec 30, 2024
1 parent 9b025e9 commit 98e4092
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "[Horizontal Bar Chart With Axis] Enable multiple legend selection",
"packageName": "@fluentui/react-charting",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
return (
<HorizontalBarChartWithAxis
{...transformPlotlyJsonToHorizontalBarWithAxisProps(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 @@ -40,6 +40,7 @@ import {
domainRangeOfNumericForHorizontalBarChartWithAxis,
createStringYAxisForHorizontalBarChartWithAxis,
getNextGradient,
areArraysEqual,
} from '../../utilities/index';

const getClassNames = classNamesFunction<IHorizontalBarChartWithAxisStyleProps, IHorizontalBarChartWithAxisStyles>();
Expand All @@ -55,6 +56,7 @@ export interface IHorizontalBarChartWithAxisState extends IBasestate {
callOutAccessibilityData?: IAccessibilityProps;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tooltipElement?: any;
selectedLegends: string[];
}

type ColorScale = (_p?: number) => string;
Expand Down Expand Up @@ -98,6 +100,7 @@ export class HorizontalBarChartWithAxisBase
activeXdataPoint: null,
YValueHover: [],
hoverXValue: '',
selectedLegends: [],
};
this._calloutId = getId('callout');
this._tooltipId = getId('HBCWATooltipID_');
Expand All @@ -114,9 +117,9 @@ export class HorizontalBarChartWithAxisBase
}

public componentDidUpdate(prevProps: IHorizontalBarChartWithAxisProps): 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 @@ -355,8 +358,7 @@ export class HorizontalBarChartWithAxisBase

const { YValueHover, hoverXValue } = this._getCalloutContentForBar(point);
if (
(this.state.isLegendSelected === false ||
(this.state.isLegendSelected && this.state.selectedLegendTitle === point.legend)) &&
(this.state.isLegendSelected === false || this._isLegendHighlighted(point.legend)) &&
this._calloutAnchorPoint !== point
) {
this._calloutAnchorPoint = point;
Expand Down Expand Up @@ -398,8 +400,8 @@ export class HorizontalBarChartWithAxisBase
color: string,
): void => {
if (
this.state.isLegendSelected === false ||
(this.state.isLegendSelected && this.state.selectedLegendTitle === point.legend)
(this.state.isLegendSelected === false || this._isLegendHighlighted(point.legend)) &&
this._calloutAnchorPoint !== point
) {
const { YValueHover, hoverXValue } = this._getCalloutContentForBar(point);
this._refArray.forEach((obj: IRefArrayData, index: number) => {
Expand Down Expand Up @@ -476,7 +478,7 @@ export class HorizontalBarChartWithAxisBase
const bars = sortedBars.map((point: IHorizontalBarChartWithAxisDataPoint, index: number) => {
let shouldHighlight = true;
if (this.state.isLegendHovered || this.state.isLegendSelected) {
shouldHighlight = this.state.selectedLegendTitle === point.legend;
shouldHighlight = this._isLegendHighlighted(point.legend);
}
this._classNames = getClassNames(this.props.styles!, {
theme: this.props.theme!,
Expand Down Expand Up @@ -603,8 +605,8 @@ export class HorizontalBarChartWithAxisBase
const { useSingleColor = false } = this.props;
const bars = this._points.map((point: IHorizontalBarChartWithAxisDataPoint, index: number) => {
let shouldHighlight = true;
if (this.state.isLegendHovered || this.state.isLegendSelected) {
shouldHighlight = this.state.selectedLegendTitle === point.legend;
if (this._getHighlightedLegend().length > 0) {
shouldHighlight = this._isLegendHighlighted(point.legend);
}
this._classNames = getClassNames(this.props.styles!, {
theme: this.props.theme!,
Expand Down Expand Up @@ -717,28 +719,8 @@ export class HorizontalBarChartWithAxisBase
});
};

private _onLegendClick(customMessage: string): void {
if (this.state.isLegendSelected) {
if (this.state.selectedLegendTitle === customMessage) {
this.setState({
isLegendSelected: false,
selectedLegendTitle: customMessage,
});
} else {
this.setState({
selectedLegendTitle: customMessage,
});
}
} else {
this.setState({
isLegendSelected: true,
selectedLegendTitle: customMessage,
});
}
}

private _onLegendHover(customMessage: string): void {
if (this.state.isLegendSelected === false) {
if (!this._isLegendSelected()) {
this.setState({
isLegendHovered: true,
selectedLegendTitle: customMessage,
Expand All @@ -747,11 +729,11 @@ export class HorizontalBarChartWithAxisBase
}

private _onLegendLeave(isLegendFocused?: boolean): void {
if (!!isLegendFocused || this.state.isLegendSelected === false) {
if (!!isLegendFocused || !this._isLegendSelected()) {
this.setState({
isLegendHovered: false,
selectedLegendTitle: '',
isLegendSelected: isLegendFocused ? false : this.state.isLegendSelected,
isLegendSelected: isLegendFocused ? false : this._isLegendSelected(),
});
}
}
Expand All @@ -778,9 +760,6 @@ export class HorizontalBarChartWithAxisBase
const legend: ILegend = {
title: point.legend!,
color,
action: () => {
this._onLegendClick(point.legend!);
},
hoverAction: () => {
this._handleChartMouseLeave();
this._onLegendHover(point.legend!);
Expand All @@ -799,11 +778,58 @@ export class HorizontalBarChartWithAxisBase
focusZonePropsInHoverCard={this.props.focusZonePropsForLegendsInHoverCard}
overflowText={this.props.legendsOverflowText}
{...this.props.legendProps}
onChange={this._onLegendSelectionChange.bind(this)}
/>
);
return legends;
};

private _isLegendSelected = (): boolean => {
return this.state.isLegendSelected!;
};

/**
* This function checks if the given legend is highlighted or not.
* A legend can be highlighted in 2 ways:
* 1. selection: if the user clicks on it
* 2. hovering: if there is no selected legend and the user hovers over it
*/
private _isLegendHighlighted = (legend?: string) => {
return this._getHighlightedLegend().includes(legend!);
};

private _getHighlightedLegend() {
return this.state.selectedLegends.length > 0
? this.state.selectedLegends
: this.state.selectedLegendTitle
? [this.state.selectedLegendTitle]
: [];
}

private _onLegendSelectionChange(
selectedLegends: string[],
event: React.MouseEvent<HTMLButtonElement>,
currentLegend?: ILegend,
): void {
if (this.props.legendProps?.canSelectMultipleLegends) {
this.setState({
selectedLegends,
selectedLegendTitle: currentLegend?.title!,
});
} else {
this.setState({
selectedLegends: selectedLegends.slice(-1),
selectedLegendTitle: currentLegend?.title!,
});
}
this.setState({
isLegendSelected: selectedLegends.length > 0,
});
if (this.props.legendProps?.onChange) {
this.props.legendProps.onChange(selectedLegends, event, currentLegend);
}
}

private _getAxisData = (yAxisData: IAxisData) => {
if (yAxisData && yAxisData.yAxisDomainValues.length) {
// For HBCWA x and y Values are swapped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,31 @@ describe('Horizontal bar chart with axis- Subcomponent Legends', () => {
expect(legendsAfterClickEvent[3]).toHaveAttribute('aria-selected', 'false');
},
);

testWithoutWait(
'Should select multiple legends on multiple mouse click on legends',
HorizontalBarChartWithAxis,
{ data: chartPointsHBCWA, legendProps: { canSelectMultipleLegends: true } },
container => {
// const legends = screen.getAllByText((content, element) => element!.tagName.toLowerCase() === 'button');
const legend1 = screen.getByText('Grapes')?.closest('button');
const legend2 = screen.getByText('Apples')?.closest('button');

expect(legend1).toBeDefined();
expect(legend2).toBeDefined();

fireEvent.click(legend1!);
fireEvent.click(legend2!);
const legendsAfterClickEvent = screen.getAllByText(
(content, element) => element!.tagName.toLowerCase() === 'button',
);
// Assert
expect(legendsAfterClickEvent[0]).toHaveAttribute('aria-selected', 'false');
expect(legendsAfterClickEvent[1]).toHaveAttribute('aria-selected', 'true');
expect(legendsAfterClickEvent[2]).toHaveAttribute('aria-selected', 'true');
expect(legendsAfterClickEvent[3]).toHaveAttribute('aria-selected', 'false');
},
);
});

describe('Horizontal bar chart with axis - Subcomponent callout', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface IHorizontalBarChartWithAxisState {
useSingleColor: boolean;
enableGradient: boolean;
roundCorners: boolean;
selectMultipleLegends: boolean;
}

const options: IChoiceGroupOption[] = [
Expand All @@ -38,6 +39,7 @@ export class HorizontalBarChartWithAxisBasicExample extends React.Component<
useSingleColor: false,
enableGradient: false,
roundCorners: false,
selectMultipleLegends: false,
};
}

Expand Down Expand Up @@ -71,6 +73,10 @@ export class HorizontalBarChartWithAxisBasicExample extends React.Component<
this.setState({ roundCorners: checked });
};

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

private _basicExample(): JSX.Element {
const points: IHorizontalBarChartWithAxisDataPoint[] = [
{
Expand Down Expand Up @@ -148,6 +154,13 @@ export class HorizontalBarChartWithAxisBasicExample extends React.Component<
<Toggle label="Enable Gradient" onText="ON" offText="OFF" onChange={this._onToggleGradient} />
&nbsp;&nbsp;
<Toggle label="Rounded Corners" onText="ON" offText="OFF" onChange={this._onToggleRoundCorners} />
&nbsp;&nbsp;
<Toggle
label="Select multiple legends"
onText="ON"
offText="OFF"
onChange={this._onToggleRoundMultipleLegendSelection}
/>
</div>
<br />

Expand All @@ -168,6 +181,9 @@ export class HorizontalBarChartWithAxisBasicExample extends React.Component<
enableReflow={true}
enableGradient={this.state.enableGradient}
roundCorners={this.state.roundCorners}
legendProps={{
canSelectMultipleLegends: this.state.selectMultipleLegends,
}}
/>
</div>
</>
Expand Down

0 comments on commit 98e4092

Please sign in to comment.