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

[Donut] Legends multi selection for Donut Charts #33447

Open
wants to merge 21 commits into
base: master
Choose a base branch
from

Conversation

srmukher
Copy link
Contributor

Legends multi selection for Donut Charts
image

@srmukher srmukher requested a review from a team as a code owner December 11, 2024 11:37
Copy link

github-actions bot commented Dec 11, 2024

📊 Bundle size report

✅ No changes found

Copy link

Pull request demo site: URL

roundCorners: boolean;
}

export class DonutChartMultipleExample extends React.Component<IDonutChartProps, IDonutChartState> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DonutChartMultipleExample

don't create a new example. Add a toggle in the existing example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

const opacity: number =
this.props.activeArc === this.props.data!.data.legend || this.props.activeArc === '' ? 1 : 0.1;

const isActive = activeArc?.includes(this.props.data?.data.legend!);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const isActive

why repeating this logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so that it can be used to calculate both opacity and data-is-focusable without the need to add same checks again

@@ -354,7 +374,7 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
* Note: This won't work in case of multiple legends selection.
*/
private _getHighlightedLegend() {
return this.state.selectedLegend || this.state.activeLegend;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return this.state.selectedLegend || this.state.activeLegend;

This is an or condition between selected and active.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For multiple select also checking if this.state.selectedLegends is populated other return the this.state.activeLegend
return this.state.selectedLegends.length > 0 ? this.state.selectedLegends : [this.state.activeLegend];

if (point.legend === highlightedLegend || (this.state.showHover && point.legend === this.state.legend)) {
if (
highlightedLegends.includes(point.legend!) ||
(this.state.showHover && point.legend === this.state.legend)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

point.legend === this.state.legend

you are adding selected legend and hover legend values together?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separated the logic

@@ -265,11 +263,17 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
focusZonePropsInHoverCard={this.props.focusZonePropsForLegendsInHoverCard}
overflowText={this.props.legendsOverflowText}
{...this.props.legendProps}
canSelectMultipleLegends={this.props.canSelectMultipleLegends}
onChange={this._onLegendChange}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onChange={this._onLegendChange}

keep using the action event handler. As this can be overridden for a specific legend as per the caller.

Dont modify existing behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modified as discussed offline

* Prop to enable the multiple legend selection
* @default false
*/
canSelectMultipleLegends?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canSelectMultipleLegends?: boolean;

not needed. This is an existing property of the legends control

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

this.props.activeArc === this.props.data!.data.legend || this.props.activeArc === '' ? 1 : 0.1;

const isActive = activeArc?.includes(this.props.data?.data.legend!);
const opacity: number = activeArc && activeArc.length > 0 ? (isActive ? 1 : 0.1) : 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

activeArc && activeArc.length > 0 ? (isActive ? 1 : 0.1) : 1;

move this logic to isActive

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -129,7 +131,8 @@ export class Arc extends React.Component<IArcProps, IArcState> {
if (
hideLabels ||
Math.abs(data!.endAngle - data!.startAngle) < Math.PI / 12 ||
(activeArc !== data!.data.legend && activeArc !== '')
!Array.isArray(activeArc) ||
(data!.data.legend && activeArc.includes(data!.data.legend))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same logic is repeating

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to a function

@@ -354,7 +365,7 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
* Note: This won't work in case of multiple legends selection.
*/
private _getHighlightedLegend() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_getHighlightedLegend

modify this function for multiselect

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was already modified

@@ -488,6 +488,7 @@ export interface IDonutChart {
// @public
export interface IDonutChartProps extends ICartesianChartProps {
calloutProps?: Partial<ICalloutProps>;
canSelectMultipleLegends?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canSelectMultipleLegends

revert this change

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -123,13 +124,17 @@ export class Arc extends React.Component<IArcProps, IArcState> {
return point.callOutAccessibilityData?.ariaLabel || (legend ? `${legend}, ` : '') + `${yValue}.`;
};

private _shouldHighlightArc = (activeArc: string[], legend?: string): boolean => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_shouldHighlightArc

this same function can serve all 3 places.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_shouldHighlightArc has already been used in other places too, any other specific places ?

@@ -24,9 +24,10 @@ export interface IDonutChartState {
xCalloutValue?: string;
yCalloutValue?: string;
focusedArcId?: string;
selectedLegend: string;
selectedLegend?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selectedLegend

Selected legend is not being used anywhere

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

@@ -138,7 +140,9 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
hoverLeaveCallback={this._hoverLeave}
uniqText={this._uniqText}
onBlurCallback={this._onBlur}
activeArc={this._getHighlightedLegend()}
activeArc={this._getHighlightedLegend().filter(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_getHighlightedLegend

use isLegendHighlighted here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

},
hoverAction: () => {
this._handleChartMouseLeave();
this.setState({ activeLegend: point.legend! });
},
onMouseOutAction: () => {
this.setState({ activeLegend: '' });
this.setState({ activeLegend: undefined });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined

setting as empty should be ok here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, checked with a legend having '' title, it does not work in that case

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

};

private _isLegendHovered = (legend: string): boolean => {
return this._noLegendsHighlighted() || this._isLegendHighlighted(legend);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this._noLegendsHighlighted()

this is redundant

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keeping this for now as we will take up the change in experience task later.

@@ -354,7 +387,7 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
* Note: This won't work in case of multiple legends selection.
*/
private _getHighlightedLegend() {
return this.state.selectedLegend || this.state.activeLegend;
return this.state.selectedLegends.length > 0 ? this.state.selectedLegends : [this.state.activeLegend];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return this.state.selectedLegends.length > 0 ? this.state.selectedLegends : [this.state.activeLegend];

this wont work if 2 legend are selected and a third legend is hovered

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But, in line multiple chart, on multiple legend selection, legend hover is disabled. Shall the experience be not the same?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. We will need a different representation. Currently hover behavior is lost once one or more legend is selected. We will plan for it. You can continue with the current behavior for now.

@srmukher srmukher force-pushed the users/srmukher/LegendsMultiSel branch from d4339d8 to 6cbdf51 Compare December 19, 2024 13:20
@srmukher srmukher force-pushed the users/srmukher/LegendsMultiSel branch from 6cbdf51 to be186ba Compare December 19, 2024 13:38
@srmukher srmukher force-pushed the users/srmukher/LegendsMultiSel branch from ca45bd3 to 943cff8 Compare December 20, 2024 09:55
@@ -265,16 +259,41 @@ export class DonutChartBase extends React.Component<IDonutChartProps, IDonutChar
focusZonePropsInHoverCard={this.props.focusZonePropsForLegendsInHoverCard}
overflowText={this.props.legendsOverflowText}
{...this.props.legendProps}
// eslint-disable-next-line react/jsx-no-bind
onChange={this._onLegendSelectionChange.bind(this)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this._onLegendSelectionChange.bind(this)

do you need to use this twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used just once

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i mean the class instance "this"

if (this.props.legendProps?.canSelectMultipleLegends) {
this.setState({ selectedLegends });
} else {
this.setState({ selectedLegends: selectedLegends.slice(-1) });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selectedLegends.slice(-1)

does this work when the legend is unselected

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes working


private _noLegendsHighlighted = (): boolean => {
return this._getHighlightedLegend().length === 0;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keep these functions together with getHighlightedLegends

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

private _focusCallback = (data: IChartDataPoint, id: string, element: SVGPathElement): void => {
this._currentHoverElement = element;
this.setState({
/** Show the callout if highlighted arc is focused and Hide it if unhighlighted arc is focused */
showHover: this.state.selectedLegend === '' || this.state.selectedLegend === data.legend,
showHover: this._noLegendsHighlighted() || this._isLegendHighlighted(data.legend!),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!

move this ! check inside isLegendHighlighted function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

const highlightedLegends = this._getHighlightedLegend().filter(legend => legend !== undefined);

if (this.state.showHover) {
const legendValue = data.find(point => point.legend === this.state.legend);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

legendValue

call this variable point

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

calling it pointValue as point is used in data.find

}

private _isLegendHighlighted = (legend: string): boolean => {
return this._getHighlightedLegend().indexOf(legend) > -1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.indexOf(legend) > -1;

use .includes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -123,13 +124,16 @@ export class Arc extends React.Component<IArcProps, IArcState> {
return point.callOutAccessibilityData?.ariaLabel || (legend ? `${legend}, ` : '') + `${yValue}.`;
};

private _shouldHighlightArc = (activeArc: string[], legend?: string): boolean => {
Copy link
Contributor

@AtishayMsft AtishayMsft Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

activeArc

activeArc can come from this.props. Not needed as argument. All the empty and length checks can move to this. Right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved

@@ -2443,22 +2429,6 @@ exports[`DonutChart snapShot testing Should render arc labels in percentage form
onMouseOver={[Function]}
role="img"
/>
<text
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

text

why this change

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated snapshots

</div>
</div>
`;
exports[`Donut chart interactions Should reflect theme change 1`] = `<div />`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<div />;

why did the div disappear

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated snapshots

@srmukher srmukher force-pushed the users/srmukher/LegendsMultiSel branch from 943cff8 to 36e6bdc Compare December 22, 2024 14:06
@srmukher srmukher changed the title Legends multi selection for Donut Charts [Donut] Legends multi selection for Donut Charts Dec 23, 2024
@srmukher srmukher force-pushed the users/srmukher/LegendsMultiSel branch from 6f40ac0 to cfd09ea Compare December 23, 2024 12:20
const { arc, data, innerRadius, outerRadius, showLabelsInPercent, totalValue, hideLabels, activeArc } = this.props;
private _shouldHighlightArc = (legend?: string): boolean => {
const { activeArc } = this.props;
return !(activeArc && activeArc.length > 0 && legend !== undefined && !activeArc.includes(legend));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!

why is there negation sign here

@@ -94,6 +94,9 @@ export const transformPlotlyJsonToDonutProps = (
hideLabels,
showLabelsInPercent: firstData.textinfo ? firstData.textinfo === 'percent' : true,
styles,
legendProps: {
canSelectMultipleLegends: true,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

legend props are defined in DeclarativeChart.tsx. Use that only.

dataPointCalloutProps?: IChartDataPoint;
callOutAccessibilityData?: IAccessibilityProps;
selectedLegends: string[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selectedLegends

can you resolve all addressed comments


const getById = queryAllByAttribute.bind(null, 'id');
expect(getById(container, /Pie.*?first/i)[0]).toHaveStyle('opacity: 1.0');
expect(getById(container, /Pie.*?second/i)[0]).toHaveStyle('opacity: 1.0');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is good. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants