@@ -65,6 +65,8 @@ import {
65
65
isYearArray ,
66
66
isInvalidValue ,
67
67
formatToLocaleString ,
68
+ isNumber ,
69
+ isObjectArray ,
68
70
getAxisIds ,
69
71
getAxisKey ,
70
72
} from '@fluentui/chart-utilities' ;
@@ -333,6 +335,139 @@ export const resolveXAxisPoint = (
333
335
return x ;
334
336
} ;
335
337
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
+
336
471
export const transformPlotlyJsonToDonutProps = (
337
472
input : PlotlySchema ,
338
473
isMultiPlot : boolean ,
@@ -536,17 +671,52 @@ export const transformPlotlyJsonToGVBCProps = (
536
671
colorwayType : ColorwayType ,
537
672
isDarkTheme ?: boolean ,
538
673
) : 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
+ }
539
709
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 ) ;
542
712
543
713
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 ) ;
547
717
// extract colors for each series only once
548
718
const extractedColors = extractColor (
549
- input . layout ?. template ?. layout ?. colorway ,
719
+ processedInput . layout ?. template ?. layout ?. colorway ,
550
720
colorwayType ,
551
721
series . marker ?. color ,
552
722
colorMap ,
@@ -579,7 +749,7 @@ export const transformPlotlyJsonToGVBCProps = (
579
749
xAxisCalloutData : x as string ,
580
750
color : rgb ( color ) . copy ( { opacity } ) . formatHex8 ( ) ?? color ,
581
751
legend,
582
- useSecondaryYScale : usesSecondaryYScale ( series , input . layout ) ,
752
+ useSecondaryYScale : usesSecondaryYScale ( series , processedInput . layout ) ,
583
753
yAxisCalloutData : getFormattedCalloutYData ( yVal , yAxisTickFormat ) ,
584
754
} ) ;
585
755
}
@@ -590,21 +760,21 @@ export const transformPlotlyJsonToGVBCProps = (
590
760
591
761
return {
592
762
data : gvbcData ,
593
- width : input . layout ?. width ,
594
- height : input . layout ?. height ?? 350 ,
763
+ width : processedInput . layout ?. width ,
764
+ height : processedInput . layout ?. height ?? 350 ,
595
765
barWidth : 'auto' ,
596
766
mode : 'plotly' ,
597
767
...secondaryYAxisValues ,
598
768
hideTickOverlap : true ,
599
769
wrapXAxisLables : typeof gvbcData [ 0 ] ?. name === 'string' ,
600
770
hideLegend,
601
771
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 ) ,
605
776
...yAxisTickFormat ,
606
- ...getAxisCategoryOrderProps ( input . data , input . layout ) ,
607
- ...getBarProps ( input . data , input . layout ) ,
777
+ ...getBarProps ( processedInput . data , processedInput . layout ) ,
608
778
} ;
609
779
} ;
610
780
0 commit comments