Skip to content

Commit 6cc506e

Browse files
Anush2303Anush
andauthored
fix(react-charts): fix date format and multi selection of legends in donut chart (microsoft#35163)
Co-authored-by: Anush <[email protected]>
1 parent ff91a7e commit 6cc506e

File tree

10 files changed

+98
-93
lines changed

10 files changed

+98
-93
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "fix date format and multi selection of legends in donut chart",
4+
"packageName": "@fluentui/react-charts",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/charts/react-charts/library/src/components/AreaChart/AreaChart.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import {
2828
tooltipOfAxislabels,
2929
getNextColor,
3030
getColorFromToken,
31-
formatDate,
3231
getSecureProps,
3332
areArraysEqual,
3433
getCurveFactory,
@@ -47,6 +46,7 @@ import type { JSXElement } from '@fluentui/react-utilities';
4746
import { Legend, LegendContainer, Legends } from '../Legends/index';
4847
import { ScaleLinear } from 'd3-scale';
4948
import { toImage } from '../../utilities/image-export-utils';
49+
import { formatDateToLocaleString } from '@fluentui/chart-utilities';
5050

5151
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5252
const bisect = bisector((d: any) => d.x).left;
@@ -236,7 +236,9 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
236236
// eslint-disable-next-line @typescript-eslint/no-shadow
237237
const { xAxisCalloutData, xAxisCalloutAccessibilityData } = lineChartData![0].data[index as number];
238238
const formattedDate =
239-
pointToHighlight instanceof Date ? formatDate(pointToHighlight, props.useUTC) : pointToHighlight;
239+
pointToHighlight instanceof Date
240+
? formatDateToLocaleString(pointToHighlight, props.culture, props.useUTC as boolean)
241+
: pointToHighlight;
240242
const modifiedXVal = pointToHighlight instanceof Date ? pointToHighlight.getTime() : pointToHighlight;
241243
// eslint-disable-next-line @typescript-eslint/no-explicit-any
242244
const found: any = find(_calloutPoints, (element: { x: string | number }) => {
@@ -866,7 +868,7 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
866868
_updatePosition(cx, cy);
867869

868870
const { x, y, xAxisCalloutData } = props.data.lineChartData![lineIndex].data[pointIndex];
869-
const formattedDate = x instanceof Date ? formatDate(x, props.useUTC) : x;
871+
const formattedDate = x instanceof Date ? formatDateToLocaleString(x, props.culture, props.useUTC as boolean) : x;
870872
const modifiedXVal = x instanceof Date ? x.getTime() : x;
871873
// eslint-disable-next-line @typescript-eslint/no-explicit-any
872874
const found: any = _calloutPoints.find((e: { x: string | number }) => e.x === modifiedXVal);
@@ -901,7 +903,8 @@ export const AreaChart: React.FunctionComponent<AreaChartProps> = React.forwardR
901903
function _getAriaLabel(lineIndex: number, pointIndex: number): string {
902904
const line = props.data.lineChartData![lineIndex];
903905
const point = line.data[pointIndex];
904-
const formattedDate = point.x instanceof Date ? formatDate(point.x, props.useUTC) : point.x;
906+
const formattedDate =
907+
point.x instanceof Date ? formatDateToLocaleString(point.x, props.culture, props.useUTC as boolean) : point.x;
905908
const xValue = point.xAxisCalloutData || formattedDate;
906909
const legend = line.legend;
907910
const yValue = point.yAxisCalloutData || point.y;

packages/charts/react-charts/library/src/components/DonutChart/Arc/Arc.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ export const Arc: React.FunctionComponent<ArcProps> = React.forwardRef<HTMLDivEl
5353
}
5454

5555
function _renderArcLabel(className: string) {
56-
const { data, innerRadius, outerRadius, showLabelsInPercent, totalValue, hideLabels, activeArc } = props;
56+
const { data, innerRadius, outerRadius, showLabelsInPercent, totalValue, hideLabels } = props;
5757

5858
if (
5959
hideLabels ||
6060
Math.abs(data!.endAngle - data!.startAngle) < Math.PI / 12 ||
61-
(activeArc !== data!.data.legend && activeArc !== '')
61+
!_shouldHighlightArc(data!.data.legend!)
6262
) {
6363
return null;
6464
}
@@ -92,13 +92,14 @@ export const Arc: React.FunctionComponent<ArcProps> = React.forwardRef<HTMLDivEl
9292
}
9393
}
9494

95-
const { href, focusedArcId } = props;
95+
const { href, focusedArcId, activeArc } = props;
9696
//TO DO 'replace' is throwing error
9797
const id =
9898
props.uniqText! +
9999
(typeof props.data!.data.legend === 'string' ? props.data!.data.legend.replace(/\s+/g, '') : '') +
100100
props.data!.data.data;
101-
const opacity: number = props.activeArc === props.data!.data.legend || props.activeArc === '' ? 1 : 0.1;
101+
const opacity: number =
102+
activeArc && activeArc.length > 0 ? (activeArc.includes(props.data?.data.legend!) ? 1 : 0.1) : 1;
102103
const cornerRadius = props.roundCorners ? 3 : 0;
103104
return (
104105
<g ref={currentRef}>
@@ -129,11 +130,10 @@ export const Arc: React.FunctionComponent<ArcProps> = React.forwardRef<HTMLDivEl
129130
className={classes.root}
130131
style={{ fill: props.color, cursor: href ? 'pointer' : 'default' }}
131132
onFocus={event => _onFocus(props.data!.data, id, event)}
132-
data-is-focusable={props.activeArc === props.data!.data.legend || props.activeArc === ''}
133133
onMouseOver={event => _hoverOn(props.data!.data, event)}
134134
onMouseMove={event => _hoverOn(props.data!.data, event)}
135135
onMouseLeave={_hoverOff}
136-
tabIndex={_shouldHighlightArc(props.data!.data.legend!) ? 0 : undefined}
136+
tabIndex={_shouldHighlightArc(props.data!.data.legend!) || props.activeArc?.length === 0 ? 0 : undefined}
137137
onBlur={_onBlur}
138138
opacity={opacity}
139139
onClick={props.data?.data.onClick}

packages/charts/react-charts/library/src/components/DonutChart/Arc/Arc.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export interface ArcProps {
6868
/**
6969
* Active Arc for chart
7070
*/
71-
activeArc?: string;
71+
activeArc?: string[];
7272

7373
/**
7474
* internal prop for href

packages/charts/react-charts/library/src/components/DonutChart/DonutChart.tsx

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { DonutChartProps } from './DonutChart.types';
55
import { useDonutChartStyles } from './useDonutChartStyles.styles';
66
import { ChartDataPoint } from '../../DonutChart';
77
import { formatToLocaleString } from '@fluentui/chart-utilities';
8-
import { getColorFromToken, getNextColor, MIN_DONUT_RADIUS, useRtl } from '../../utilities/index';
8+
import { areArraysEqual, getColorFromToken, getNextColor, MIN_DONUT_RADIUS, useRtl } from '../../utilities/index';
99
import { Legend, Legends, LegendContainer } from '../../index';
1010
import { useId } from '@fluentui/react-utilities';
1111
import type { JSXElement } from '@fluentui/react-utilities';
@@ -35,22 +35,33 @@ export const DonutChart: React.FunctionComponent<DonutChartProps> = React.forwar
3535
const [legend, setLegend] = React.useState<string | undefined>('');
3636
const [_width, setWidth] = React.useState<number | undefined>(props.width || 200);
3737
const [_height, setHeight] = React.useState<number | undefined>(props.height || 200);
38-
const [activeLegend, setActiveLegend] = React.useState<string>('');
38+
const [activeLegend, setActiveLegend] = React.useState<string | undefined>(undefined);
3939
const [color, setColor] = React.useState<string | undefined>('');
4040
const [xCalloutValue, setXCalloutValue] = React.useState<string>('');
4141
const [yCalloutValue, setYCalloutValue] = React.useState<string>('');
42-
const [selectedLegend, setSelectedLegend] = React.useState<string>('');
42+
const [selectedLegends, setSelectedLegends] = React.useState<string[]>(props.legendProps?.selectedLegends || []);
4343
const [focusedArcId, setFocusedArcId] = React.useState<string>('');
4444
const [dataPointCalloutProps, setDataPointCalloutProps] = React.useState<ChartDataPoint | undefined>();
4545
const [clickPosition, setClickPosition] = React.useState({ x: 0, y: 0 });
4646
const [isPopoverOpen, setPopoverOpen] = React.useState(false);
47+
const prevPropsRef = React.useRef<DonutChartProps | null>(null);
4748
const _legendsRef = React.useRef<LegendContainer>(null);
4849
const _isRTL: boolean = useRtl();
4950

5051
React.useEffect(() => {
5152
_fitParentContainer();
5253
}, []);
5354

55+
React.useEffect(() => {
56+
if (prevPropsRef.current) {
57+
const prevProps = prevPropsRef.current;
58+
if (!areArraysEqual(prevProps.legendProps?.selectedLegends, props.legendProps?.selectedLegends)) {
59+
setSelectedLegends(props.legendProps?.selectedLegends || []);
60+
}
61+
}
62+
prevPropsRef.current = props;
63+
}, [props]);
64+
5465
React.useEffect(() => {
5566
if (prevSize.current.height !== props.height || prevSize.current.width !== props.width) {
5667
_fitParentContainer();
@@ -98,19 +109,12 @@ export const DonutChart: React.FunctionComponent<DonutChartProps> = React.forwar
98109
const legend: Legend = {
99110
title: point.legend!,
100111
color,
101-
action: () => {
102-
if (selectedLegend === point.legend) {
103-
setSelectedLegend('');
104-
} else {
105-
setSelectedLegend(point.legend!);
106-
}
107-
},
108112
hoverAction: () => {
109113
_handleChartMouseLeave();
110114
setActiveLegend(point.legend!);
111115
},
112116
onMouseOutAction: () => {
113-
setActiveLegend('');
117+
setActiveLegend(undefined);
114118
},
115119
};
116120
return legend;
@@ -121,11 +125,27 @@ export const DonutChart: React.FunctionComponent<DonutChartProps> = React.forwar
121125
centerLegends
122126
overflowText={props.legendsOverflowText}
123127
{...props.legendProps}
128+
// eslint-disable-next-line react/jsx-no-bind
129+
onChange={_onLegendSelectionChange}
124130
legendRef={_legendsRef}
125131
/>
126132
);
127133
return legends;
128134
}
135+
function _onLegendSelectionChange(
136+
selectedLegends: string[],
137+
event: React.MouseEvent<HTMLButtonElement>,
138+
currentLegend?: Legend,
139+
): void {
140+
if (props.legendProps && props.legendProps?.canSelectMultipleLegends) {
141+
setSelectedLegends(selectedLegends);
142+
} else {
143+
setSelectedLegends(selectedLegends.slice(-1));
144+
}
145+
if (props.legendProps?.onChange) {
146+
props.legendProps.onChange(selectedLegends, event, currentLegend);
147+
}
148+
}
129149

130150
function _focusCallback(data: ChartDataPoint, id: string, e: React.FocusEvent<SVGPathElement>): void {
131151
let cx = 0;
@@ -135,7 +155,7 @@ export const DonutChart: React.FunctionComponent<DonutChartProps> = React.forwar
135155
cx = targetRect.left + targetRect.width / 2;
136156
cy = targetRect.top + targetRect.height / 2;
137157
updatePosition(cx, cy);
138-
setPopoverOpen(selectedLegend === '' || selectedLegend === data.legend);
158+
setPopoverOpen(_noLegendsHighlighted() || _isLegendHighlighted(data.legend));
139159
setValue(data.data!.toString());
140160
setLegend(data.legend);
141161
setColor(data.color!);
@@ -148,7 +168,7 @@ export const DonutChart: React.FunctionComponent<DonutChartProps> = React.forwar
148168
function _hoverCallback(data: ChartDataPoint, e: React.MouseEvent<SVGPathElement>): void {
149169
if (_calloutAnchorPoint !== data) {
150170
_calloutAnchorPoint = data;
151-
setPopoverOpen(selectedLegend === '' || selectedLegend === data.legend);
171+
setPopoverOpen(_noLegendsHighlighted() || _isLegendHighlighted(data.legend));
152172
setValue(data.data!.toString());
153173
setLegend(data.legend);
154174
setColor(data.color!);
@@ -172,16 +192,22 @@ export const DonutChart: React.FunctionComponent<DonutChartProps> = React.forwar
172192
}
173193

174194
function _valueInsideDonut(valueInsideDonut: string | number | undefined, data: ChartDataPoint[]) {
175-
const highlightedLegend = _getHighlightedLegend();
176-
if (valueInsideDonut !== undefined && (highlightedLegend !== '' || isPopoverOpen)) {
177-
let legendValue = valueInsideDonut;
178-
data!.map((point: ChartDataPoint, index: number) => {
179-
if (point.legend === highlightedLegend || (isPopoverOpen && point.legend === legend)) {
180-
legendValue = point.yAxisCalloutData ? point.yAxisCalloutData : point.data!;
195+
const highlightedLegends = _getHighlightedLegend();
196+
if (valueInsideDonut !== undefined && (highlightedLegends.length === 1 || isPopoverOpen)) {
197+
const pointValue = data.find(point => _isLegendHighlighted(point.legend));
198+
return pointValue
199+
? pointValue.yAxisCalloutData
200+
? pointValue.yAxisCalloutData
201+
: pointValue.data!
202+
: valueInsideDonut;
203+
} else if (highlightedLegends.length > 0) {
204+
let totalValue = 0;
205+
data.forEach(point => {
206+
if (highlightedLegends.includes(point.legend!)) {
207+
totalValue += point.data!;
181208
}
182-
return;
183209
});
184-
return legendValue;
210+
return totalValue;
185211
} else {
186212
return valueInsideDonut;
187213
}
@@ -199,10 +225,17 @@ export const DonutChart: React.FunctionComponent<DonutChartProps> = React.forwar
199225
* This function returns
200226
* the selected legend if there is one
201227
* or the hovered legend if none of the legends is selected.
202-
* Note: This won't work in case of multiple legends selection.
203228
*/
204229
function _getHighlightedLegend() {
205-
return selectedLegend || activeLegend;
230+
return selectedLegends.length > 0 ? selectedLegends : activeLegend ? [activeLegend] : [];
231+
}
232+
233+
function _isLegendHighlighted(legend: string | undefined): boolean {
234+
return _getHighlightedLegend().includes(legend!);
235+
}
236+
237+
function _noLegendsHighlighted(): boolean {
238+
return _getHighlightedLegend().length === 0;
206239
}
207240

208241
function _isChartEmpty(): boolean {

packages/charts/react-charts/library/src/components/DonutChart/Pie/Pie.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export interface PieProps {
5252
/**
5353
* Active Arc for chart
5454
*/
55-
activeArc?: string;
55+
activeArc?: string[];
5656

5757
/**
5858
* string for callout id

0 commit comments

Comments
 (0)