Skip to content

Commit 2d6aca2

Browse files
authored
fix(react-charting): Fixing donut colors and segment orders (#35131)
1 parent 624002a commit 2d6aca2

10 files changed

+551
-374
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(react-charting): Fixing donut colors and segment orders",
4+
"packageName": "@fluentui/react-charting",
5+
"email": "120183316+srmukher@users.noreply.github.com",
6+
"dependentChangeType": "patch"
7+
}

packages/charts/react-charting/etc/react-charting.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,7 @@ export interface IDonutChartProps extends ICartesianChartProps {
608608
hideLabels?: boolean;
609609
innerRadius?: number;
610610
onRenderCalloutPerDataPoint?: IRenderFunction<IChartDataPoint>;
611+
order?: 'default' | 'sorted';
611612
roundCorners?: boolean;
612613
showLabelsInPercent?: boolean;
613614
styles?: IStyleFunctionOrObject<IDonutChartStyleProps, IDonutChartStyles>;

packages/charts/react-charting/src/components/DeclarativeChart/DeclarativeChart.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -454,11 +454,21 @@ export const DeclarativeChart: React.FunctionComponent<DeclarativeChartProps> =
454454
gridProperties.templateRows === SINGLE_REPEAT &&
455455
gridProperties.templateColumns === SINGLE_REPEAT
456456
) {
457-
Object.keys(groupedTraces).forEach((key, index) => {
458-
if (index > 0) {
459-
delete groupedTraces[key];
460-
}
461-
});
457+
if (chart.type === 'donut') {
458+
// If there are multiple data traces for donut/pie, picking the last one similar to plotly
459+
const keys = Object.keys(groupedTraces);
460+
keys.forEach((key, index) => {
461+
if (index < keys.length - 1) {
462+
delete groupedTraces[key];
463+
}
464+
});
465+
} else {
466+
Object.keys(groupedTraces).forEach((key, index) => {
467+
if (index > 0) {
468+
delete groupedTraces[key];
469+
}
470+
});
471+
}
462472
isMultiPlot.current = false;
463473
}
464474

packages/charts/react-charting/src/components/DeclarativeChart/PlotlyColorAdapter.ts

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { areArraysEqual } from '../../utilities/utilities';
66
import { DataVizPalette, getColorFromToken, getNextColor } from '../../utilities/colors';
77
import { scaleLinear as d3ScaleLinear } from 'd3-scale';
88

9-
type PlotlyColorway = 'plotly' | 'others';
9+
type PlotlyColorway = 'plotly' | 'd3' | 'others';
1010

1111
// The color sequences in plotly express are defined here:
1212
// https://plotly.com/python/discrete-color/#:~:text=Join%20now.-,Color%20Sequences%20in%20Plotly%20Express,-By%20default%2C%20Plotly
@@ -25,6 +25,21 @@ const DEFAULT_PLOTLY_COLORWAY = [
2525
'#fecb52', //10
2626
];
2727

28+
// Default D3 qualitative colorway (Category10), matching Plotly Express px.colors.qualitative.D3
29+
// Source: https://plotly.com/python/discrete-color/#:~:text=Join%20now.-,Color%20Sequences%20in%20Plotly%20Express,-By%20default%2C%20Plotly
30+
export const DEFAULT_D3_COLORWAY = [
31+
'#1f77b4', //1
32+
'#ff7f0e', //2
33+
'#2ca02c', //3
34+
'#d62728', //4
35+
'#9467bd', //5
36+
'#8c564b', //6
37+
'#e377c2', //7
38+
'#7f7f7f', //8
39+
'#bcbd22', //9
40+
'#17becf', //10
41+
];
42+
2843
const PLOTLY_FLUENTVIZ_COLORWAY_MAPPING = [
2944
DataVizPalette.color1, //1
3045
DataVizPalette.warning, //2
@@ -38,38 +53,74 @@ const PLOTLY_FLUENTVIZ_COLORWAY_MAPPING = [
3853
DataVizPalette.color10, //10
3954
];
4055

41-
function getPlotlyColorway(colorway: string[] | undefined): PlotlyColorway {
42-
const isPlotlyColorway =
43-
isArrayOrTypedArray(colorway) &&
44-
areArraysEqual(
45-
colorway?.map(c => c.toLowerCase()),
46-
DEFAULT_PLOTLY_COLORWAY,
47-
);
56+
// Mapping from D3 Category10 order to Fluent DataViz tokens (light/dark handled via getColorFromToken)
57+
// D3: [blue, orange, green, red, purple, brown, pink, gray, olive, cyan]
58+
export const D3_FLUENTVIZ_COLORWAY_MAPPING: string[] = [
59+
DataVizPalette.color26, // blue -> lightBlue.shade10
60+
DataVizPalette.warning, // orange -> semantic warning
61+
DataVizPalette.color5, // green -> lightGreen.primary
62+
DataVizPalette.error, // red -> semantic error
63+
DataVizPalette.color4, // purple -> orchid.tint10
64+
DataVizPalette.color17, // brown -> pumpkin.shade20
65+
DataVizPalette.color22, // pink -> hotPink.tint20
66+
DataVizPalette.disabled, // gray -> semantic disabled
67+
DataVizPalette.color10, // olive/yellow-green -> gold.shade10
68+
DataVizPalette.color3, // cyan/teal -> teal.tint20
69+
];
4870

49-
return isPlotlyColorway ? 'plotly' : 'others';
71+
function getPlotlyColorway(colorway: string[] | undefined, isDonut: boolean = false): PlotlyColorway {
72+
if (!colorway || !isArrayOrTypedArray(colorway)) {
73+
return 'others';
74+
}
75+
const lower = colorway.map(c => c.toLowerCase());
76+
if (isDonut && areArraysEqual(lower, D3_FLUENTVIZ_COLORWAY_MAPPING)) {
77+
return 'd3';
78+
}
79+
if (areArraysEqual(lower, DEFAULT_PLOTLY_COLORWAY)) {
80+
return 'plotly';
81+
}
82+
return 'others';
5083
}
5184

52-
function tryMapFluentDataViz(hexColor: string, templateColorway: PlotlyColorway, isDarkTheme?: boolean): string {
85+
function tryMapFluentDataViz(
86+
hexColor: string,
87+
templateColorway: PlotlyColorway,
88+
isDarkTheme?: boolean,
89+
isDonut?: boolean,
90+
): string {
5391
if (templateColorway !== 'plotly') {
5492
return hexColor;
5593
}
56-
const index = DEFAULT_PLOTLY_COLORWAY.indexOf(hexColor.toLowerCase());
57-
if (index !== -1) {
58-
return getColorFromToken(PLOTLY_FLUENTVIZ_COLORWAY_MAPPING[index], isDarkTheme);
94+
let defaultColorway: string[] = DEFAULT_PLOTLY_COLORWAY;
95+
let defaultMapping: string[] = PLOTLY_FLUENTVIZ_COLORWAY_MAPPING;
96+
if (isDonut) {
97+
defaultColorway = templateColorway === 'plotly' ? DEFAULT_PLOTLY_COLORWAY : DEFAULT_D3_COLORWAY;
98+
defaultMapping = templateColorway === 'plotly' ? PLOTLY_FLUENTVIZ_COLORWAY_MAPPING : D3_FLUENTVIZ_COLORWAY_MAPPING;
99+
}
100+
const idx = defaultColorway.indexOf(hexColor.toLowerCase());
101+
if (idx !== -1) {
102+
return getColorFromToken(defaultMapping[idx], !!isDarkTheme);
59103
}
60104
return hexColor;
61105
}
62106

63107
export const getColor = (
64108
legendLabel: string,
65109
colorMap: React.MutableRefObject<Map<string, string>>,
110+
templateColorway: PlotlyColorway,
66111
isDarkTheme?: boolean,
112+
isDonut?: boolean,
67113
): string => {
68114
if (!colorMap.current.has(legendLabel)) {
69115
let nextColor: string;
70-
if (colorMap.current.size < PLOTLY_FLUENTVIZ_COLORWAY_MAPPING.length) {
116+
const defaultColorMapping = isDonut
117+
? templateColorway === 'plotly'
118+
? PLOTLY_FLUENTVIZ_COLORWAY_MAPPING
119+
: D3_FLUENTVIZ_COLORWAY_MAPPING
120+
: PLOTLY_FLUENTVIZ_COLORWAY_MAPPING;
121+
if (colorMap.current.size < defaultColorMapping.length) {
71122
// Get first 10 colors from plotly-fluentviz colorway mapping
72-
nextColor = getColorFromToken(PLOTLY_FLUENTVIZ_COLORWAY_MAPPING[colorMap.current.size], isDarkTheme);
123+
nextColor = getColorFromToken(defaultColorMapping[colorMap.current.size], isDarkTheme);
73124
} else {
74125
nextColor = getNextColor(colorMap.current.size, 0, isDarkTheme);
75126
}
@@ -85,17 +136,18 @@ export const getSchemaColors = (
85136
colors: PieColors | Color | Color[] | string | null | undefined,
86137
colorMap: React.MutableRefObject<Map<string, string>>,
87138
isDarkTheme?: boolean,
139+
isDonut?: boolean,
88140
): string[] | string | undefined => {
89141
const hexColors: string[] = [];
90142
if (!colors) {
91143
return undefined;
92144
}
93-
const templateColorway = getPlotlyColorway(colorway);
145+
const templateColorway = getPlotlyColorway(colorway, isDonut);
94146
if (isArrayOrTypedArray(colors)) {
95147
// eslint-disable-next-line @typescript-eslint/no-explicit-any
96148
(colors as any[]).forEach((element, index) => {
97149
const colorString = element?.toString().trim();
98-
const nextFluentColor = getColor(`Label_${index}`, colorMap, isDarkTheme);
150+
const nextFluentColor = getColor(`Label_${index}`, colorMap, templateColorway, isDarkTheme, isDonut);
99151
if (colorString) {
100152
const parsedColor = d3Color(colorString);
101153
hexColors.push(
@@ -109,7 +161,7 @@ export const getSchemaColors = (
109161
const parsedColor = d3Color(colors);
110162
return parsedColor
111163
? tryMapFluentDataViz(parsedColor.formatHex(), templateColorway, isDarkTheme)
112-
: getColor('Label_0', colorMap, isDarkTheme);
164+
: getColor('Label_0', colorMap, templateColorway, isDarkTheme, isDonut);
113165
}
114166
return hexColors;
115167
};
@@ -120,24 +172,30 @@ export const extractColor = (
120172
colors: PieColors | Color | Color[] | string | null | undefined,
121173
colorMap: React.MutableRefObject<Map<string, string>>,
122174
isDarkTheme?: boolean,
175+
isDonut?: boolean,
123176
): string | string[] | undefined => {
124-
return colorwayType === 'default' && colors ? getSchemaColors(colorway, colors, colorMap, isDarkTheme) : undefined;
177+
return colorwayType === 'default' && colors
178+
? getSchemaColors(colorway, colors, colorMap, isDarkTheme, isDonut)
179+
: undefined;
125180
};
126181

127182
export const resolveColor = (
128183
extractedColors: string[] | string | null | undefined,
129184
index: number,
130185
legend: string,
131186
colorMap: React.MutableRefObject<Map<string, string>>,
187+
colorway: string[] | undefined,
132188
isDarkTheme?: boolean,
189+
isDonut?: boolean,
133190
): string => {
134191
let color = '';
192+
const templateColorway = getPlotlyColorway(colorway, isDonut);
135193
if (extractedColors && isArrayOrTypedArray(extractedColors) && extractedColors.length > 0) {
136194
color = extractedColors[index % extractedColors.length];
137195
} else if (typeof extractedColors === 'string') {
138196
color = extractedColors;
139197
} else {
140-
color = getColor(legend, colorMap, isDarkTheme);
198+
color = getColor(legend, colorMap, templateColorway, isDarkTheme, isDonut);
141199
}
142200
return color;
143201
};

0 commit comments

Comments
 (0)