Skip to content

Commit

Permalink
[Gauge Chart] Enabling legend multi selection (#33524)
Browse files Browse the repository at this point in the history
  • Loading branch information
srmukher authored Dec 30, 2024
1 parent 8bd0e68 commit d61ac31
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "[Gauge Chart] Enabling legend multi selection",
"packageName": "@fluentui/react-charting",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
return (
<GaugeChart
{...transformPlotlyJsonToGaugeProps(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 @@ -13,6 +13,7 @@ import { IProcessedStyleSet } from '@fluentui/react/lib/Styling';
import { convertToLocaleString } from '../../utilities/locale-util';
import {
Points,
areArraysEqual,
formatValueWithSIPrefix,
getAccessibleDataObject,
getColorFromToken,
Expand Down Expand Up @@ -106,7 +107,7 @@ interface IYValue extends Omit<IYValueHover, 'y'> {
}
export interface IGaugeChartState {
hoveredLegend: string;
selectedLegend: string;
selectedLegends: string[];
focusedElement?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
calloutTarget: any;
Expand Down Expand Up @@ -144,7 +145,7 @@ export class GaugeChartBase extends React.Component<IGaugeChartProps, IGaugeChar

this.state = {
hoveredLegend: '',
selectedLegend: props.legendProps?.selectedLegend ?? '',
selectedLegends: props.legendProps?.selectedLegends || [],
focusedElement: '',
calloutTarget: null,
isCalloutVisible: false,
Expand All @@ -168,9 +169,9 @@ export class GaugeChartBase extends React.Component<IGaugeChartProps, IGaugeChar
}

public componentDidUpdate(prevProps: IGaugeChartProps): 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 @@ -496,13 +497,6 @@ export class GaugeChartBase extends React.Component<IGaugeChartProps, IGaugeChar
return {
title: segment.legend,
color,
action: () => {
if (this.state.selectedLegend === segment.legend) {
this.setState({ selectedLegend: '' });
} else {
this.setState({ selectedLegend: segment.legend });
}
},
hoverAction: () => {
this.setState({ hoveredLegend: segment.legend });
},
Expand All @@ -514,30 +508,57 @@ export class GaugeChartBase extends React.Component<IGaugeChartProps, IGaugeChar

return (
<div className={this._classNames.legendsContainer}>
<Legends legends={legends} centerLegends {...this.props.legendProps} />
<Legends
legends={legends}
centerLegends
{...this.props.legendProps}
// eslint-disable-next-line react/jsx-no-bind
onChange={this._onLegendSelectionChange.bind(this)}
/>
</div>
);
};

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);
}
}

/**
* 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 _legendHighlighted = (legend: string) => {
return (
this.state.selectedLegend === legend || (this.state.selectedLegend === '' && this.state.hoveredLegend === legend)
);
return this._getHighlightedLegend().includes(legend!);
};

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

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

private _handleFocus = (focusEvent: React.FocusEvent<SVGElement>, focusedElement: string) => {
this._showCallout(focusEvent.target, focusedElement, true);
};
Expand Down Expand Up @@ -580,9 +601,7 @@ export class GaugeChartBase extends React.Component<IGaugeChartProps, IGaugeChar
calloutTarget: target,
/** Show the callout if highlighted arc is hovered/focused and Hide it if unhighlighted arc is hovered/focused */
isCalloutVisible:
['Needle', 'Chart value'].includes(legend) ||
this.state.selectedLegend === '' ||
this.state.selectedLegend === legend,
['Needle', 'Chart value'].includes(legend) || this._noLegendHighlighted() || this._legendHighlighted(legend),
hoverXValue,
hoverYValues,
...(isFocusEvent ? { focusedElement: legend } : {}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,24 @@ describe('GaugeChart interaction and accessibility tests', () => {
}
}
});

it(`should highlight multiple segments when the legend multi select is enabled`, () => {
const { container } = render(
<GaugeChart
segments={segments}
chartValue={25}
calloutProps={{ doNotLayer: true }}
legendProps={{ canSelectMultipleLegends: true }}
/>,
);

fireEvent.click(screen.getByText(segments[0].legend));
fireEvent.click(screen.getByText(segments[1].legend));
const segs = container.querySelectorAll('[class^="segment"]');
expect(segs[0]).toHaveStyle('fill-opacity: 1');
expect(segs[1]).toHaveStyle('fill-opacity: 1');
expect(segs[2]).toHaveStyle('fill-opacity: 0.1');
});
});

describe('Gauge Chart - axe-core', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface IGCBasicExampleState {
hideMinMax: boolean;
enableGradient: boolean;
roundedCorners: boolean;
legendMultiSelect: boolean;
}

export class GaugeChartBasicExample extends React.Component<{}, IGCBasicExampleState> {
Expand All @@ -29,6 +30,7 @@ export class GaugeChartBasicExample extends React.Component<{}, IGCBasicExampleS
hideMinMax: false,
enableGradient: false,
roundedCorners: false,
legendMultiSelect: false,
};
}

Expand Down Expand Up @@ -99,6 +101,14 @@ export class GaugeChartBasicExample extends React.Component<{}, IGCBasicExampleS
checked={this.state.roundedCorners}
onChange={this._onToggleRoundedCorners}
/>
&nbsp;&nbsp;
<Toggle
label="Select Multiple Legends"
onText="ON"
offText="OFF"
checked={this.state.legendMultiSelect}
onChange={this._onToggleLegendMultiSelect}
/>
</div>

<GaugeChart
Expand Down Expand Up @@ -129,6 +139,9 @@ export class GaugeChartBasicExample extends React.Component<{}, IGCBasicExampleS
variant={GaugeChartVariant.MultipleSegments}
enableGradient={this.state.enableGradient}
roundCorners={this.state.roundedCorners}
legendProps={{
canSelectMultipleLegends: this.state.legendMultiSelect,
}}
/>
</>
);
Expand All @@ -155,4 +168,8 @@ export class GaugeChartBasicExample extends React.Component<{}, IGCBasicExampleS
private _onToggleRoundedCorners = (ev: React.MouseEvent<HTMLElement>, checked: boolean) => {
this.setState({ roundedCorners: checked });
};

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

0 comments on commit d61ac31

Please sign in to comment.