Skip to content

Commit ec2c01b

Browse files
Anush2303Anush
andauthored
fix(react-charts): handling y object values for bar chart (microsoft#35115)
Co-authored-by: Anush <[email protected]>
1 parent 40fab9b commit ec2c01b

File tree

3 files changed

+192
-15
lines changed

3 files changed

+192
-15
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": "handling y object values for bar chart",
4+
"packageName": "@fluentui/react-charts",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/charts/react-charts/library/src/components/DeclarativeChart/PlotlySchemaAdapter.ts

Lines changed: 184 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ import {
6565
isYearArray,
6666
isInvalidValue,
6767
formatToLocaleString,
68+
isNumber,
69+
isObjectArray,
6870
getAxisIds,
6971
getAxisKey,
7072
} from '@fluentui/chart-utilities';
@@ -333,6 +335,139 @@ export const resolveXAxisPoint = (
333335
return x;
334336
};
335337

338+
/**
339+
* Checks if a key should be ignored during normalization
340+
* @param key The key to check
341+
* @returns true if the key should be ignored
342+
*/
343+
const shouldIgnoreKey = (key: string): boolean => {
344+
const lowerKey = key.toLowerCase();
345+
if (lowerKey.includes('style') || lowerKey === 'style') {
346+
return true;
347+
}
348+
// Use regex to match common CSS property patterns
349+
// (color, fill, stroke, border, background, font, shadow, outline, etc.)
350+
const cssKeyRegex = new RegExp(
351+
'^(color|fill|stroke|border|background|font|shadow|outline|margin|padding|gap|align|justify|display|flex|grid|' +
352+
'text|line|letter|word|vertical|horizontal|overflow|position|top|right|bottom|left|zindex|z-index|opacity|' +
353+
'filter|clip|cursor|resize|transition|animation|transform|box|column|row|direction|visibility|' +
354+
'content|width|height|aspect|image|user|pointer|caret|scroll|%)|(-webkit-|-moz-|-ms-|-o-)',
355+
'i',
356+
);
357+
if (cssKeyRegex.test(lowerKey)) {
358+
return true;
359+
}
360+
return false;
361+
};
362+
363+
/**
364+
* Flattens a nested object into a single level object with dot notation keys
365+
* @param obj Object to flatten
366+
* @param prefix Optional prefix for keys
367+
* @returns Flattened object
368+
*/
369+
const flattenObject = (obj: Record<string, unknown>, prefix: string = ''): Record<string, unknown> => {
370+
const flattened: Record<string, unknown> = {};
371+
372+
for (const key in obj) {
373+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
374+
const newKey = prefix ? `${prefix}.${key}` : key;
375+
const value = obj[key];
376+
377+
if (typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)) {
378+
// Recursively flatten nested objects
379+
Object.assign(flattened, flattenObject(value as Record<string, unknown>, newKey));
380+
} else {
381+
flattened[newKey] = value;
382+
}
383+
}
384+
}
385+
386+
return flattened;
387+
};
388+
389+
/**
390+
* Normalizes an array of objects by flattening nested structures and creating grouped data
391+
* Uses json_normalize approach with D3 color detection and filtering
392+
* @param data Array of objects to normalize
393+
* @returns Object containing traces for grouped vertical bar chart
394+
*/
395+
export const normalizeObjectArrayForGVBC = (
396+
data: Array<Record<string, unknown>>,
397+
xLabels?: string[],
398+
): { traces: Array<Record<string, unknown>>; x: string[] } => {
399+
if (!data || data.length === 0) {
400+
return { traces: [], x: [] };
401+
}
402+
403+
// Use provided xLabels if available, otherwise default to Item 1, Item 2, ...
404+
const x = xLabels && xLabels.length === data.length ? xLabels : data.map((_, index) => `Item ${index + 1}`);
405+
406+
// First, flatten all objects and collect all unique keys, excluding style keys
407+
const flattenedObjects = data.map((item, index) => {
408+
if (typeof item === 'object' && item !== null) {
409+
const flattened = flattenObject(item);
410+
// Only keep keys where the value is numeric (number or numeric string) and not a style key
411+
const filtered: Record<string, unknown> = {};
412+
Object.keys(flattened).forEach(key => {
413+
const value = flattened[key];
414+
if (!shouldIgnoreKey(key) && (typeof value === 'number' || (typeof value === 'string' && isNumber(value)))) {
415+
filtered[key] = value;
416+
}
417+
});
418+
return filtered;
419+
} else if (typeof item === 'number' || (typeof item === 'string' && isNumber(item))) {
420+
// Only keep primitive numeric values
421+
return { [x[index] || `item_${index}`]: item };
422+
} else {
423+
// Non-numeric primitive, ignore by returning empty object
424+
return {};
425+
}
426+
});
427+
428+
// Collect all unique keys across all objects
429+
const allKeys = new Set<string>();
430+
flattenedObjects.forEach(obj => {
431+
Object.keys(obj).forEach(key => allKeys.add(key));
432+
});
433+
434+
// Create traces for each key (property)
435+
const traces: Array<Record<string, unknown>> = [];
436+
437+
allKeys.forEach(key => {
438+
const yValues: number[] = [];
439+
let hasValidData = false;
440+
let isNumericData = false;
441+
442+
flattenedObjects.forEach((obj, index) => {
443+
const value = obj[key];
444+
if (typeof value === 'number') {
445+
yValues.push(value);
446+
hasValidData = true;
447+
isNumericData = true;
448+
} else if (typeof value === 'string' && isNumber(value)) {
449+
yValues.push(parseFloat(value));
450+
hasValidData = true;
451+
isNumericData = true;
452+
}
453+
});
454+
455+
// Only create trace if we have valid numeric data
456+
if (hasValidData && isNumericData) {
457+
const trace: Record<string, unknown> = {
458+
type: 'bar',
459+
name: key,
460+
x,
461+
y: yValues,
462+
};
463+
464+
traces.push(trace);
465+
}
466+
});
467+
468+
return { traces, x };
469+
};
470+
336471
export const transformPlotlyJsonToDonutProps = (
337472
input: PlotlySchema,
338473
isMultiPlot: boolean,
@@ -536,17 +671,52 @@ export const transformPlotlyJsonToGVBCProps = (
536671
colorwayType: ColorwayType,
537672
isDarkTheme?: boolean,
538673
): GroupedVerticalBarChartProps => {
674+
// Handle object arrays in y values by normalizing the data first
675+
let processedInput = { ...input };
676+
677+
// Check if any bar traces have object arrays as y values
678+
const hasObjectArrayData = input.data.some(
679+
(series: Partial<PlotData>) => series.type === 'bar' && isObjectArray(series.y),
680+
);
681+
682+
if (hasObjectArrayData) {
683+
// Process each trace that has object array y values
684+
const processedData = input.data
685+
.map((series: Partial<PlotData>, index: number) => {
686+
if (series.type === 'bar' && isObjectArray(series.y)) {
687+
// Normalize the object array to create multiple traces for GVBC
688+
const { traces } = normalizeObjectArrayForGVBC(
689+
series.y as unknown as Array<Record<string, unknown>>,
690+
Array.isArray(series.x) ? (series.x as string[]) : undefined,
691+
);
692+
693+
// Return all the new traces, each representing a property from the objects
694+
return traces.map((trace: Record<string, unknown>) => ({
695+
...trace,
696+
// Copy other properties from the original series if needed
697+
marker: series.marker,
698+
}));
699+
}
700+
return [series];
701+
})
702+
.flat();
703+
704+
processedInput = {
705+
...input,
706+
data: processedData,
707+
};
708+
}
539709
const mapXToDataPoints: Record<string, GroupedVerticalBarChartData> = {};
540-
const secondaryYAxisValues = getSecondaryYAxisValues(input.data, input.layout, 0, 0);
541-
const { legends, hideLegend } = getLegendProps(input.data, input.layout, isMultiPlot);
710+
const secondaryYAxisValues = getSecondaryYAxisValues(processedInput.data, processedInput.layout, 0, 0);
711+
const { legends, hideLegend } = getLegendProps(processedInput.data, processedInput.layout, isMultiPlot);
542712

543713
let colorScale: ((value: number) => string) | undefined = undefined;
544-
const yAxisTickFormat = getYAxisTickFormat(input.data[0], input.layout);
545-
input.data.forEach((series: Partial<PlotData>, index1: number) => {
546-
colorScale = createColorScale(input.layout, series, colorScale);
714+
const yAxisTickFormat = getYAxisTickFormat(processedInput.data[0], processedInput.layout);
715+
processedInput.data.forEach((series: Partial<PlotData>, index1: number) => {
716+
colorScale = createColorScale(processedInput.layout, series, colorScale);
547717
// extract colors for each series only once
548718
const extractedColors = extractColor(
549-
input.layout?.template?.layout?.colorway,
719+
processedInput.layout?.template?.layout?.colorway,
550720
colorwayType,
551721
series.marker?.color,
552722
colorMap,
@@ -579,7 +749,7 @@ export const transformPlotlyJsonToGVBCProps = (
579749
xAxisCalloutData: x as string,
580750
color: rgb(color).copy({ opacity }).formatHex8() ?? color,
581751
legend,
582-
useSecondaryYScale: usesSecondaryYScale(series, input.layout),
752+
useSecondaryYScale: usesSecondaryYScale(series, processedInput.layout),
583753
yAxisCalloutData: getFormattedCalloutYData(yVal, yAxisTickFormat),
584754
});
585755
}
@@ -590,21 +760,21 @@ export const transformPlotlyJsonToGVBCProps = (
590760

591761
return {
592762
data: gvbcData,
593-
width: input.layout?.width,
594-
height: input.layout?.height ?? 350,
763+
width: processedInput.layout?.width,
764+
height: processedInput.layout?.height ?? 350,
595765
barWidth: 'auto',
596766
mode: 'plotly',
597767
...secondaryYAxisValues,
598768
hideTickOverlap: true,
599769
wrapXAxisLables: typeof gvbcData[0]?.name === 'string',
600770
hideLegend,
601771
roundCorners: true,
602-
...getTitles(input.layout),
603-
...getYMinMaxValues(input.data[0], input.layout),
604-
...getXAxisTickFormat(input.data[0], input.layout),
772+
...getTitles(processedInput.layout),
773+
...getAxisCategoryOrderProps(processedInput.data, processedInput.layout),
774+
...getYMinMaxValues(processedInput.data[0], processedInput.layout),
775+
...getXAxisTickFormat(processedInput.data[0], processedInput.layout),
605776
...yAxisTickFormat,
606-
...getAxisCategoryOrderProps(input.data, input.layout),
607-
...getBarProps(input.data, input.layout),
777+
...getBarProps(processedInput.data, processedInput.layout),
608778
};
609779
};
610780

packages/charts/react-charts/library/src/components/DeclarativeChart/PlotlySchemaAdapterUT.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ describe('transform Plotly Json To chart Props', () => {
236236
transformPlotlyJsonToGVBCProps(plotlySchema, false, { current: colorMap }, 'default', true),
237237
).toMatchSnapshot();
238238
} catch (e) {
239-
expect(e).toStrictEqual(TypeError("Cannot read properties of undefined (reading 'forEach')"));
239+
expect(e).toStrictEqual(TypeError("Cannot read properties of undefined (reading 'some')"));
240240
}
241241
});
242242

0 commit comments

Comments
 (0)