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

[Gauge Chart] Enabling legend multi selection #33524

Merged
merged 3 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all 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": "[Gauge Chart] Enabling legend multi selection",
"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 @@ -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 });
};
}
Loading