@automattic/charts 3.1.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/TOKENS.md +80 -46
  3. package/dist/index.cjs +1224 -662
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +72 -23
  6. package/dist/index.d.cts +230 -162
  7. package/dist/index.d.ts +230 -162
  8. package/dist/index.js +1224 -663
  9. package/dist/index.js.map +1 -1
  10. package/package.json +10 -10
  11. package/src/charts/area-chart/area-chart.tsx +39 -25
  12. package/src/charts/area-chart/test/area-chart.test.tsx +4 -33
  13. package/src/charts/area-chart/test/tick-placement.test.tsx +55 -0
  14. package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +86 -0
  15. package/src/charts/area-chart/test/tooltip-resolution.test.tsx +69 -0
  16. package/src/charts/area-chart/types.ts +4 -7
  17. package/src/charts/bar-chart/bar-chart.tsx +38 -11
  18. package/src/charts/bar-chart/private/test/use-bar-chart-options-formatting.test.tsx +69 -0
  19. package/src/charts/bar-chart/private/use-bar-chart-options.ts +27 -15
  20. package/src/charts/bar-chart/test/bar-chart-pointer-events.test.tsx +67 -0
  21. package/src/charts/bar-chart/test/bar-chart.test.tsx +79 -0
  22. package/src/charts/bar-list-chart/bar-list-chart.tsx +45 -2
  23. package/src/charts/bar-list-chart/test/bar-list-chart.test.tsx +38 -0
  24. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +33 -23
  25. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +138 -182
  26. package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +46 -0
  27. package/src/charts/geo-chart/geo-chart.module.scss +1 -0
  28. package/src/charts/geo-chart/geo-chart.tsx +5 -10
  29. package/src/charts/geo-chart/test/geo-chart.test.tsx +2 -2
  30. package/src/charts/heatmap-chart/heatmap-chart.module.scss +21 -16
  31. package/src/charts/heatmap-chart/heatmap-chart.tsx +45 -28
  32. package/src/charts/heatmap-chart/private/build-calendar-data.ts +3 -0
  33. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +11 -4
  34. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +51 -12
  35. package/src/charts/heatmap-chart/types.ts +2 -2
  36. package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
  37. package/src/charts/leaderboard-chart/hooks/use-leaderboard-legend-items.ts +3 -5
  38. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +31 -19
  39. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +27 -17
  40. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +14 -0
  41. package/src/charts/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx +2 -9
  42. package/src/charts/line-chart/line-chart.module.scss +10 -18
  43. package/src/charts/line-chart/line-chart.tsx +109 -25
  44. package/src/charts/line-chart/private/line-chart-annotation.tsx +18 -19
  45. package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +74 -0
  46. package/src/charts/line-chart/test/line-chart.test.tsx +74 -3
  47. package/src/charts/line-chart/test/tick-placement.test.tsx +84 -0
  48. package/src/charts/line-chart/test/tooltip-resolution.test.tsx +140 -0
  49. package/src/charts/line-chart/types.ts +4 -1
  50. package/src/charts/pie-chart/pie-chart.module.scss +13 -1
  51. package/src/charts/pie-chart/pie-chart.tsx +30 -40
  52. package/src/charts/pie-chart/test/pie-chart.test.tsx +56 -0
  53. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +8 -3
  54. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +23 -37
  55. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +50 -0
  56. package/src/charts/private/chart-layout/chart-layout.module.scss +3 -2
  57. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
  58. package/src/charts/private/test/axis-formatting.test.tsx +85 -0
  59. package/src/charts/private/test/time-axis-formatting.test.ts +109 -0
  60. package/src/charts/private/test/time-axis-options.test.ts +267 -0
  61. package/src/charts/private/test/time-axis-tick-invariants.test.ts +153 -0
  62. package/src/charts/private/test/time-axis-tick-values.test.ts +280 -0
  63. package/src/charts/private/test/time-axis.test.ts +9 -69
  64. package/src/charts/private/time-axis-options.ts +106 -0
  65. package/src/charts/private/time-axis.ts +318 -136
  66. package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
  67. package/src/charts/private/xy-plot/xy-plot.module.scss +8 -0
  68. package/src/charts/sparkline/sparkline.tsx +9 -3
  69. package/src/charts/sparkline/test/sparkline.test.tsx +29 -56
  70. package/src/charts/sparkline/types.ts +2 -2
  71. package/src/components/legend/private/base-legend.module.scss +9 -5
  72. package/src/components/tooltip/accessible-tooltip.tsx +38 -10
  73. package/src/components/tooltip/base-tooltip.module.scss +3 -3
  74. package/src/components/tooltip/private/bounded-tooltip.tsx +172 -0
  75. package/src/components/tooltip/test/accessible-tooltip.test.tsx +67 -0
  76. package/src/components/tooltip/test/bounded-tooltip.test.tsx +129 -0
  77. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +279 -0
  78. package/src/components/tooltip/xy-chart-tooltip.tsx +288 -0
  79. package/src/components/trend-indicator/trend-indicator.module.scss +2 -2
  80. package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +17 -10
  81. package/src/hooks/test/use-xychart-theme.test.tsx +63 -5
  82. package/src/hooks/use-xychart-theme.ts +33 -23
  83. package/src/index.ts +4 -0
  84. package/src/providers/chart-context/global-charts-provider.tsx +64 -54
  85. package/src/providers/chart-context/hooks/use-chart-formatting.ts +22 -0
  86. package/src/providers/chart-context/index.ts +1 -0
  87. package/src/providers/chart-context/private/catalog-pointers.ts +42 -0
  88. package/src/providers/chart-context/private/series-palette.ts +35 -0
  89. package/src/providers/chart-context/test/catalog-pointers.test.ts +61 -0
  90. package/src/providers/chart-context/test/chart-context.test.tsx +177 -792
  91. package/src/providers/chart-context/test/use-global-charts-theme.test.tsx +7 -18
  92. package/src/providers/chart-context/themes.ts +7 -47
  93. package/src/providers/chart-context/types.ts +3 -0
  94. package/src/styles/chart-scope.scss +63 -58
  95. package/src/styles/test/chart-scope.test.ts +25 -40
  96. package/src/test-utils/series-fixtures.ts +31 -0
  97. package/src/types.ts +103 -73
  98. package/src/utils/bucket-info.ts +87 -0
  99. package/src/utils/color-utils.ts +0 -27
  100. package/src/utils/date-formatting.ts +174 -0
  101. package/src/utils/get-styles.ts +7 -1
  102. package/src/utils/index.ts +3 -0
  103. package/src/utils/test/bucket-info.test.ts +185 -0
  104. package/src/utils/test/color-utils.test.ts +1 -48
  105. package/src/utils/test/date-formatting.test.ts +89 -0
  106. package/src/utils/test/get-styles.test.ts +10 -7
  107. package/src/utils/test/merge-themes.test.ts +18 -46
  108. package/src/visx/types.ts +29 -2
  109. package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +0 -28
  110. package/src/providers/chart-context/private/theme-override-vars.ts +0 -87
  111. package/src/providers/chart-context/private/with-catalog-pointers.ts +0 -50
  112. package/src/providers/chart-context/test/sparse-theme.test.ts +0 -35
  113. package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +0 -29
  114. package/src/providers/chart-context/test/theme-override-vars.test.ts +0 -118
  115. package/src/providers/chart-context/test/with-catalog-pointers.test.ts +0 -47
  116. package/src/styles/test/wpds-fallbacks.test.ts +0 -131
@@ -6,13 +6,26 @@ import { useChartRegistration } from '../hooks/use-chart-registration';
6
6
  import { useGlobalChartsContext } from '../hooks/use-global-charts-context';
7
7
  import { defaultTheme } from '../themes';
8
8
  import type { BaseLegendItem } from '../../../components/legend';
9
- import type { ChartTheme, SeriesData } from '../../../types';
9
+ import type { SeriesData } from '../../../types';
10
10
  import type { GlobalChartsContextValue } from '../types';
11
11
 
12
12
  describe( 'ChartContext', () => {
13
- const mockTheme: ChartTheme = {
14
- colors: [ '#ff0000', '#00ff00', '#0000ff' ],
15
- } as ChartTheme;
13
+ // On `body` rather than the document root, so the palette-resolution suite below can mount
14
+ // outside it and start from an undeclared palette.
15
+ const PALETTE = [ '#ff0000', '#00ff00', '#0000ff' ];
16
+
17
+ const setPaletteRoles = ( element: HTMLElement, colors: readonly string[] ) =>
18
+ colors.forEach( ( color, slot ) =>
19
+ element.style.setProperty( `--a8c-charts-color-series-${ slot + 1 }`, color )
20
+ );
21
+
22
+ beforeAll( () => setPaletteRoles( document.body, PALETTE ) );
23
+
24
+ afterAll( () =>
25
+ PALETTE.forEach( ( _color, slot ) =>
26
+ document.body.style.removeProperty( `--a8c-charts-color-series-${ slot + 1 }` )
27
+ )
28
+ );
16
29
 
17
30
  const mockLegendItems: BaseLegendItem[] = [
18
31
  { label: 'Series 1', value: '100', color: '#ff0000' },
@@ -66,7 +79,7 @@ describe( 'ChartContext', () => {
66
79
  expect( contextValue.isColorPaletteResolved ).toBe( true );
67
80
  } );
68
81
 
69
- it( 'resolves palette again after theme change', () => {
82
+ it( 'keeps the palette resolved across a theme change', () => {
70
83
  let contextValue: GlobalChartsContextValue;
71
84
 
72
85
  const TestComponent = () => {
@@ -74,15 +87,8 @@ describe( 'ChartContext', () => {
74
87
  return <div>Test</div>;
75
88
  };
76
89
 
77
- const theme1: Partial< ChartTheme > = {
78
- colors: [ '#006DAB', '#1F9828' ],
79
- };
80
- const theme2: Partial< ChartTheme > = {
81
- colors: [ '#FF0000', '#00FF00' ],
82
- };
83
-
84
90
  const { rerender } = render(
85
- <GlobalChartsProvider theme={ theme1 }>
91
+ <GlobalChartsProvider theme={ { tickLength: 4 } }>
86
92
  <TestComponent />
87
93
  </GlobalChartsProvider>
88
94
  );
@@ -90,12 +96,11 @@ describe( 'ChartContext', () => {
90
96
  expect( contextValue.isColorPaletteResolved ).toBe( true );
91
97
 
92
98
  rerender(
93
- <GlobalChartsProvider theme={ theme2 }>
99
+ <GlobalChartsProvider theme={ { tickLength: 8 } }>
94
100
  <TestComponent />
95
101
  </GlobalChartsProvider>
96
102
  );
97
103
 
98
- // After theme change, palette should re-resolve to true
99
104
  expect( contextValue.isColorPaletteResolved ).toBe( true );
100
105
  } );
101
106
 
@@ -303,7 +308,7 @@ describe( 'ChartContext', () => {
303
308
  };
304
309
 
305
310
  render(
306
- <GlobalChartsProvider theme={ mockTheme }>
311
+ <GlobalChartsProvider>
307
312
  <TestComponent />
308
313
  </GlobalChartsProvider>
309
314
  );
@@ -334,7 +339,7 @@ describe( 'ChartContext', () => {
334
339
  };
335
340
 
336
341
  render(
337
- <GlobalChartsProvider theme={ mockTheme }>
342
+ <GlobalChartsProvider>
338
343
  <TestComponent />
339
344
  </GlobalChartsProvider>
340
345
  );
@@ -366,7 +371,7 @@ describe( 'ChartContext', () => {
366
371
  };
367
372
 
368
373
  render(
369
- <GlobalChartsProvider theme={ mockTheme }>
374
+ <GlobalChartsProvider>
370
375
  <TestComponent />
371
376
  </GlobalChartsProvider>
372
377
  );
@@ -395,7 +400,7 @@ describe( 'ChartContext', () => {
395
400
  };
396
401
 
397
402
  render(
398
- <GlobalChartsProvider theme={ mockTheme }>
403
+ <GlobalChartsProvider>
399
404
  <TestComponent />
400
405
  </GlobalChartsProvider>
401
406
  );
@@ -405,7 +410,7 @@ describe( 'ChartContext', () => {
405
410
  index: 0,
406
411
  } ).color;
407
412
 
408
- expect( color ).toBe( mockTheme.colors[ 0 ] );
413
+ expect( color ).toBe( PALETTE[ 0 ] );
409
414
  } );
410
415
 
411
416
  it( 'handles empty string group gracefully', () => {
@@ -417,7 +422,7 @@ describe( 'ChartContext', () => {
417
422
  };
418
423
 
419
424
  render(
420
- <GlobalChartsProvider theme={ mockTheme }>
425
+ <GlobalChartsProvider>
421
426
  <TestComponent />
422
427
  </GlobalChartsProvider>
423
428
  );
@@ -427,10 +432,10 @@ describe( 'ChartContext', () => {
427
432
  index: 0,
428
433
  } ).color;
429
434
 
430
- expect( color ).toBe( mockTheme.colors[ 0 ] );
435
+ expect( color ).toBe( PALETTE[ 0 ] );
431
436
  } );
432
437
 
433
- it( 'falls back to theme colors by index when group is invalid', () => {
438
+ it( 'falls back to the palette by index when group is invalid', () => {
434
439
  let contextValue: GlobalChartsContextValue;
435
440
 
436
441
  const TestComponent = () => {
@@ -439,7 +444,7 @@ describe( 'ChartContext', () => {
439
444
  };
440
445
 
441
446
  render(
442
- <GlobalChartsProvider theme={ mockTheme }>
447
+ <GlobalChartsProvider>
443
448
  <TestComponent />
444
449
  </GlobalChartsProvider>
445
450
  );
@@ -457,12 +462,12 @@ describe( 'ChartContext', () => {
457
462
  index: 2,
458
463
  } ).color;
459
464
 
460
- expect( color1 ).toBe( mockTheme.colors[ 0 ] );
461
- expect( color2 ).toBe( mockTheme.colors[ 1 ] );
462
- expect( color3 ).toBe( mockTheme.colors[ 2 ] );
465
+ expect( color1 ).toBe( PALETTE[ 0 ] );
466
+ expect( color2 ).toBe( PALETTE[ 1 ] );
467
+ expect( color3 ).toBe( PALETTE[ 2 ] );
463
468
  } );
464
469
 
465
- it( 'generates new colors when index exceeds theme color array', () => {
470
+ it( 'generates new colors when index exceeds the palette', () => {
466
471
  let contextValue: GlobalChartsContextValue;
467
472
 
468
473
  const TestComponent = () => {
@@ -471,7 +476,7 @@ describe( 'ChartContext', () => {
471
476
  };
472
477
 
473
478
  render(
474
- <GlobalChartsProvider theme={ mockTheme }>
479
+ <GlobalChartsProvider>
475
480
  <TestComponent />
476
481
  </GlobalChartsProvider>
477
482
  );
@@ -488,9 +493,9 @@ describe( 'ChartContext', () => {
488
493
 
489
494
  // Generated color should be different from palette colors
490
495
  expect( generatedColor ).not.toBe( paletteColor );
491
- expect( generatedColor ).not.toBe( mockTheme.colors[ 0 ] );
492
- expect( generatedColor ).not.toBe( mockTheme.colors[ 1 ] );
493
- expect( generatedColor ).not.toBe( mockTheme.colors[ 2 ] );
496
+ expect( generatedColor ).not.toBe( PALETTE[ 0 ] );
497
+ expect( generatedColor ).not.toBe( PALETTE[ 1 ] );
498
+ expect( generatedColor ).not.toBe( PALETTE[ 2 ] );
494
499
 
495
500
  // Generated color should be in hex format
496
501
  expect( generatedColor ).toMatch( /^#[0-9a-f]{6}$/i );
@@ -505,7 +510,7 @@ describe( 'ChartContext', () => {
505
510
  };
506
511
 
507
512
  render(
508
- <GlobalChartsProvider theme={ mockTheme }>
513
+ <GlobalChartsProvider>
509
514
  <TestComponent />
510
515
  </GlobalChartsProvider>
511
516
  );
@@ -532,7 +537,7 @@ describe( 'ChartContext', () => {
532
537
  };
533
538
 
534
539
  render(
535
- <GlobalChartsProvider theme={ mockTheme }>
540
+ <GlobalChartsProvider>
536
541
  <TestComponent />
537
542
  </GlobalChartsProvider>
538
543
  );
@@ -565,7 +570,7 @@ describe( 'ChartContext', () => {
565
570
  };
566
571
 
567
572
  render(
568
- <GlobalChartsProvider theme={ mockTheme }>
573
+ <GlobalChartsProvider>
569
574
  <TestComponent />
570
575
  </GlobalChartsProvider>
571
576
  );
@@ -599,7 +604,7 @@ describe( 'ChartContext', () => {
599
604
  };
600
605
 
601
606
  render(
602
- <GlobalChartsProvider theme={ mockTheme }>
607
+ <GlobalChartsProvider>
603
608
  <TestComponent />
604
609
  </GlobalChartsProvider>
605
610
  );
@@ -630,7 +635,7 @@ describe( 'ChartContext', () => {
630
635
  };
631
636
 
632
637
  render(
633
- <GlobalChartsProvider theme={ mockTheme }>
638
+ <GlobalChartsProvider>
634
639
  <TestComponent />
635
640
  </GlobalChartsProvider>
636
641
  );
@@ -684,7 +689,7 @@ describe( 'ChartContext', () => {
684
689
  };
685
690
 
686
691
  render(
687
- <GlobalChartsProvider theme={ mockTheme }>
692
+ <GlobalChartsProvider>
688
693
  <TestComponent />
689
694
  </GlobalChartsProvider>
690
695
  );
@@ -744,7 +749,7 @@ describe( 'ChartContext', () => {
744
749
  } );
745
750
 
746
751
  describe( 'Color cache performance', () => {
747
- it( 'maintains stable colors when theme remains unchanged', () => {
752
+ it( 'maintains stable colors when the palette is unchanged', () => {
748
753
  let contextValue: GlobalChartsContextValue;
749
754
 
750
755
  const TestComponent = () => {
@@ -753,7 +758,7 @@ describe( 'ChartContext', () => {
753
758
  };
754
759
 
755
760
  const { rerender } = render(
756
- <GlobalChartsProvider theme={ mockTheme }>
761
+ <GlobalChartsProvider>
757
762
  <TestComponent />
758
763
  </GlobalChartsProvider>
759
764
  );
@@ -766,7 +771,7 @@ describe( 'ChartContext', () => {
766
771
 
767
772
  // Re-render with same theme
768
773
  rerender(
769
- <GlobalChartsProvider theme={ mockTheme }>
774
+ <GlobalChartsProvider>
770
775
  <TestComponent />
771
776
  </GlobalChartsProvider>
772
777
  );
@@ -780,7 +785,7 @@ describe( 'ChartContext', () => {
780
785
  expect( afterRerenderColor ).toBe( initialColor );
781
786
  } );
782
787
 
783
- it( 'updates colors when theme changes', () => {
788
+ it( 'takes its palette from the roles in scope at its wrapper', () => {
784
789
  let contextValue: GlobalChartsContextValue;
785
790
 
786
791
  const TestComponent = () => {
@@ -788,36 +793,36 @@ describe( 'ChartContext', () => {
788
793
  return <div>Test</div>;
789
794
  };
790
795
 
791
- const newTheme: typeof mockTheme = {
792
- colors: [ '#000000', '#111111', '#222222' ],
793
- } as typeof mockTheme;
794
-
795
- const { rerender } = render(
796
- <GlobalChartsProvider theme={ mockTheme }>
796
+ render(
797
+ <GlobalChartsProvider>
797
798
  <TestComponent />
798
799
  </GlobalChartsProvider>
799
800
  );
800
801
 
801
- // Get initial generated color
802
- const initialColor = contextValue.getElementStyles( {
802
+ const rootPaletteColor = contextValue.getElementStyles( {
803
803
  data: createMockDataWithGroup( undefined ),
804
804
  index: 5,
805
805
  } ).color;
806
806
 
807
- // Re-render with different theme
808
- rerender(
809
- <GlobalChartsProvider theme={ newTheme }>
807
+ const overridden = document.createElement( 'div' );
808
+ setPaletteRoles( overridden, [ '#000000', '#111111', '#222222' ] );
809
+ document.body.appendChild( overridden );
810
+
811
+ render(
812
+ <GlobalChartsProvider>
810
813
  <TestComponent />
811
- </GlobalChartsProvider>
814
+ </GlobalChartsProvider>,
815
+ { container: overridden }
812
816
  );
813
817
 
814
- // Color should change due to different theme
815
- const afterThemeChangeColor = contextValue.getElementStyles( {
818
+ const overriddenPaletteColor = contextValue.getElementStyles( {
816
819
  data: createMockDataWithGroup( undefined ),
817
820
  index: 5,
818
821
  } ).color;
819
822
 
820
- expect( afterThemeChangeColor ).not.toBe( initialColor );
823
+ expect( overriddenPaletteColor ).not.toBe( rootPaletteColor );
824
+
825
+ document.body.removeChild( overridden );
821
826
  } );
822
827
  } );
823
828
 
@@ -842,13 +847,13 @@ describe( 'ChartContext', () => {
842
847
  };
843
848
 
844
849
  const { rerender } = render(
845
- <GlobalChartsProvider theme={ mockTheme }>
850
+ <GlobalChartsProvider>
846
851
  <TestComponent />
847
852
  </GlobalChartsProvider>
848
853
  );
849
854
 
850
855
  rerender(
851
- <GlobalChartsProvider theme={ mockTheme }>
856
+ <GlobalChartsProvider>
852
857
  <TestComponent />
853
858
  </GlobalChartsProvider>
854
859
  );
@@ -873,7 +878,7 @@ describe( 'ChartContext', () => {
873
878
  };
874
879
 
875
880
  render(
876
- <GlobalChartsProvider theme={ mockTheme }>
881
+ <GlobalChartsProvider>
877
882
  <TestComponent />
878
883
  </GlobalChartsProvider>
879
884
  );
@@ -904,7 +909,7 @@ describe( 'ChartContext', () => {
904
909
  };
905
910
 
906
911
  render(
907
- <GlobalChartsProvider theme={ mockTheme }>
912
+ <GlobalChartsProvider>
908
913
  <TestComponent />
909
914
  </GlobalChartsProvider>
910
915
  );
@@ -920,7 +925,7 @@ describe( 'ChartContext', () => {
920
925
  index: 1,
921
926
  } );
922
927
 
923
- expect( styles.color ).toBe( mockTheme.colors[ 1 ] );
928
+ expect( styles.color ).toBe( PALETTE[ 1 ] );
924
929
  expect( styles.lineStyles ).toEqual( {} );
925
930
  expect( styles.shapeStyles ).toEqual( {} );
926
931
  } );
@@ -934,7 +939,7 @@ describe( 'ChartContext', () => {
934
939
  };
935
940
 
936
941
  render(
937
- <GlobalChartsProvider theme={ mockTheme }>
942
+ <GlobalChartsProvider>
938
943
  <TestComponent />
939
944
  </GlobalChartsProvider>
940
945
  );
@@ -968,7 +973,7 @@ describe( 'ChartContext', () => {
968
973
  };
969
974
 
970
975
  render(
971
- <GlobalChartsProvider theme={ mockTheme }>
976
+ <GlobalChartsProvider>
972
977
  <TestComponent />
973
978
  </GlobalChartsProvider>
974
979
  );
@@ -1000,7 +1005,6 @@ describe( 'ChartContext', () => {
1000
1005
  };
1001
1006
 
1002
1007
  const extendedTheme = {
1003
- ...mockTheme,
1004
1008
  seriesLineStyles: [ { strokeWidth: 2 }, { strokeWidth: 3, strokeDasharray: '2 2' } ],
1005
1009
  };
1006
1010
 
@@ -1038,7 +1042,6 @@ describe( 'ChartContext', () => {
1038
1042
  };
1039
1043
 
1040
1044
  const extendedTheme = {
1041
- ...mockTheme,
1042
1045
  legend: {
1043
1046
  shapeStyles: [
1044
1047
  { fill: '#LEGEND1', stroke: '#BORDER1' },
@@ -1081,7 +1084,6 @@ describe( 'ChartContext', () => {
1081
1084
  };
1082
1085
 
1083
1086
  const extendedTheme = {
1084
- ...mockTheme,
1085
1087
  lineChart: {
1086
1088
  lineStyles: {
1087
1089
  comparison: {
@@ -1131,7 +1133,6 @@ describe( 'ChartContext', () => {
1131
1133
  };
1132
1134
 
1133
1135
  const extendedTheme = {
1134
- ...mockTheme,
1135
1136
  lineChart: {
1136
1137
  lineStyles: {
1137
1138
  comparison: {
@@ -1189,7 +1190,6 @@ describe( 'ChartContext', () => {
1189
1190
  const mockGlyph2 = jest.fn();
1190
1191
 
1191
1192
  const themeWithGlyphs = {
1192
- ...mockTheme,
1193
1193
  glyphs: [ mockGlyph1, mockGlyph2 ],
1194
1194
  };
1195
1195
 
@@ -1227,7 +1227,6 @@ describe( 'ChartContext', () => {
1227
1227
 
1228
1228
  const mockGlyph = jest.fn();
1229
1229
  const themeWithGlyphs = {
1230
- ...mockTheme,
1231
1230
  glyphs: [ mockGlyph ],
1232
1231
  };
1233
1232
 
@@ -1259,7 +1258,7 @@ describe( 'ChartContext', () => {
1259
1258
  };
1260
1259
 
1261
1260
  render(
1262
- <GlobalChartsProvider theme={ mockTheme }>
1261
+ <GlobalChartsProvider>
1263
1262
  <TestComponent />
1264
1263
  </GlobalChartsProvider>
1265
1264
  );
@@ -1289,7 +1288,6 @@ describe( 'ChartContext', () => {
1289
1288
 
1290
1289
  const mockGlyph = jest.fn();
1291
1290
  const completeTheme = {
1292
- ...mockTheme,
1293
1291
  glyphs: [ mockGlyph ],
1294
1292
  seriesLineStyles: [ { strokeWidth: 2 } ],
1295
1293
  legend: {
@@ -1337,7 +1335,6 @@ describe( 'ChartContext', () => {
1337
1335
 
1338
1336
  const mockGlyph = jest.fn();
1339
1337
  const completeTheme = {
1340
- ...mockTheme,
1341
1338
  glyphs: [ mockGlyph ],
1342
1339
  };
1343
1340
 
@@ -1381,7 +1378,7 @@ describe( 'ChartContext', () => {
1381
1378
  };
1382
1379
 
1383
1380
  render(
1384
- <GlobalChartsProvider theme={ mockTheme }>
1381
+ <GlobalChartsProvider>
1385
1382
  <TestComponent />
1386
1383
  </GlobalChartsProvider>
1387
1384
  );
@@ -1396,7 +1393,7 @@ describe( 'ChartContext', () => {
1396
1393
  expect( styles ).toHaveProperty( 'glyph' );
1397
1394
  expect( styles ).toHaveProperty( 'shapeStyles' );
1398
1395
 
1399
- expect( styles.color ).toBe( mockTheme.colors[ 0 ] );
1396
+ expect( styles.color ).toBe( PALETTE[ 0 ] );
1400
1397
  expect( styles.lineStyles ).toEqual( {} );
1401
1398
  expect( styles.glyph ).toBeUndefined();
1402
1399
  expect( styles.shapeStyles ).toEqual( {} );
@@ -1413,7 +1410,7 @@ describe( 'ChartContext', () => {
1413
1410
  };
1414
1411
 
1415
1412
  render(
1416
- <GlobalChartsProvider theme={ mockTheme }>
1413
+ <GlobalChartsProvider>
1417
1414
  <TestComponent />
1418
1415
  </GlobalChartsProvider>
1419
1416
  );
@@ -1442,7 +1439,7 @@ describe( 'ChartContext', () => {
1442
1439
  };
1443
1440
 
1444
1441
  render(
1445
- <GlobalChartsProvider theme={ mockTheme }>
1442
+ <GlobalChartsProvider>
1446
1443
  <TestComponent />
1447
1444
  </GlobalChartsProvider>
1448
1445
  );
@@ -1872,659 +1869,162 @@ describe( 'ChartContext', () => {
1872
1869
  } );
1873
1870
  } );
1874
1871
 
1875
- describe( 'GlobalChartsProvider - CSS Variable Support', () => {
1872
+ // Each test declares its own slots on a container mounted outside `body`, so the file-wide
1873
+ // palette is not inherited and a test can start from an undeclared one.
1874
+ describe( 'GlobalChartsProvider - palette resolution', () => {
1876
1875
  let originalGetComputedStyle: typeof window.getComputedStyle;
1876
+ let container: HTMLDivElement;
1877
1877
 
1878
1878
  beforeEach( () => {
1879
1879
  originalGetComputedStyle = window.getComputedStyle;
1880
+ container = document.createElement( 'div' );
1881
+ document.documentElement.appendChild( container );
1880
1882
  } );
1881
1883
 
1882
1884
  afterEach( () => {
1883
1885
  window.getComputedStyle = originalGetComputedStyle;
1886
+ document.documentElement.removeChild( container );
1884
1887
  } );
1885
1888
 
1886
- describe( 'CSS Variable Resolution', () => {
1887
- it( 'resolves CSS custom properties in theme colors', () => {
1888
- window.getComputedStyle = jest.fn( () => ( {
1889
- getPropertyValue: ( prop: string ) => {
1890
- if ( prop === '--primary-color' ) {
1891
- return '#00ff00';
1892
- }
1893
- return '';
1894
- },
1895
- } ) ) as unknown as typeof window.getComputedStyle;
1896
-
1897
- let contextValue: GlobalChartsContextValue;
1898
-
1899
- const TestComponent = () => {
1900
- contextValue = useGlobalChartsContext();
1901
- return <div>Test</div>;
1902
- };
1889
+ let contextValue: GlobalChartsContextValue;
1903
1890
 
1904
- const cssVarTheme: ChartTheme = {
1905
- colors: [ '--primary-color', '#ff0000' ],
1906
- } as ChartTheme;
1891
+ const TestComponent = () => {
1892
+ contextValue = useGlobalChartsContext();
1893
+ return <div>Test</div>;
1894
+ };
1907
1895
 
1908
- render(
1909
- <GlobalChartsProvider theme={ cssVarTheme }>
1910
- <TestComponent />
1911
- </GlobalChartsProvider>
1912
- );
1913
-
1914
- const color = contextValue.getElementStyles( {
1915
- data: undefined,
1916
- index: 0,
1917
- } ).color;
1918
-
1919
- expect( color ).toBe( '#00ff00' );
1896
+ const renderWithSlots = ( slots: Array< string | undefined > ) => {
1897
+ slots.forEach( ( color, slot ) => {
1898
+ if ( color !== undefined ) {
1899
+ container.style.setProperty( `--a8c-charts-color-series-${ slot + 1 }`, color );
1900
+ }
1920
1901
  } );
1921
1902
 
1922
- it( 'uses wrapper element for scoped CSS variable resolution', () => {
1923
- const mockGetComputedStyle = jest.fn( ( element: Element ) => {
1924
- // Scoped wrapper returns different value than root
1925
- if ( element !== document.documentElement ) {
1926
- return {
1927
- getPropertyValue: ( prop: string ) => {
1928
- if ( prop === '--scoped-color' ) {
1929
- return '#00ff00'; // Scoped value
1930
- }
1931
- return '';
1932
- },
1933
- };
1934
- }
1935
- // Root would return different value
1936
- return {
1937
- getPropertyValue: ( prop: string ) => {
1938
- if ( prop === '--scoped-color' ) {
1939
- return '#ff0000'; // Root value
1940
- }
1941
- return '';
1942
- },
1943
- };
1944
- } );
1945
- window.getComputedStyle = mockGetComputedStyle as unknown as typeof window.getComputedStyle;
1946
-
1947
- let contextValue: GlobalChartsContextValue;
1948
-
1949
- const TestComponent = () => {
1950
- contextValue = useGlobalChartsContext();
1951
- return <div>Test</div>;
1952
- };
1953
-
1954
- const cssVarTheme: ChartTheme = {
1955
- colors: [ '--scoped-color' ],
1956
- } as ChartTheme;
1903
+ return render(
1904
+ <GlobalChartsProvider>
1905
+ <TestComponent />
1906
+ </GlobalChartsProvider>,
1907
+ { container }
1908
+ );
1909
+ };
1957
1910
 
1958
- render(
1959
- <GlobalChartsProvider theme={ cssVarTheme }>
1960
- <TestComponent />
1961
- </GlobalChartsProvider>
1962
- );
1911
+ const colorAt = ( index: number ) =>
1912
+ contextValue.getElementStyles( { data: undefined, index } ).color;
1963
1913
 
1964
- const color = contextValue.getElementStyles( {
1965
- data: undefined,
1966
- index: 0,
1967
- } ).color;
1914
+ describe( 'Resolution', () => {
1915
+ it( 'resolves a slot into the palette', () => {
1916
+ renderWithSlots( [ '#00ff00' ] );
1968
1917
 
1969
- // Should use scoped value from wrapper element
1970
- expect( color ).toBe( '#00ff00' );
1918
+ expect( colorAt( 0 ) ).toBe( '#00ff00' );
1971
1919
  } );
1972
1920
 
1973
- it( 'resolves multiple CSS variables in color array', () => {
1974
- window.getComputedStyle = jest.fn( () => ( {
1975
- getPropertyValue: ( prop: string ) => {
1976
- if ( prop === '--color-1' ) {
1977
- return '#ff0000';
1978
- }
1979
- if ( prop === '--color-2' ) {
1980
- return '#00ff00';
1981
- }
1982
- return '';
1983
- },
1984
- } ) ) as unknown as typeof window.getComputedStyle;
1921
+ it( 'resolves every slot in order', () => {
1922
+ renderWithSlots( [ '#ff0000', '#00ff00', '#0000ff' ] );
1985
1923
 
1986
- let contextValue: GlobalChartsContextValue;
1987
-
1988
- const TestComponent = () => {
1989
- contextValue = useGlobalChartsContext();
1990
- return <div>Test</div>;
1991
- };
1924
+ expect( colorAt( 0 ) ).toBe( '#ff0000' );
1925
+ expect( colorAt( 1 ) ).toBe( '#00ff00' );
1926
+ expect( colorAt( 2 ) ).toBe( '#0000ff' );
1927
+ } );
1992
1928
 
1993
- const cssVarTheme: ChartTheme = {
1994
- colors: [ '--color-1', '--color-2', '#0000ff' ],
1995
- } as ChartTheme;
1929
+ it( 'reads the declaration closest to the wrapper', () => {
1930
+ const outer = document.createElement( 'div' );
1931
+ setPaletteRoles( outer, [ '#ff0000' ] );
1932
+ container.appendChild( outer );
1933
+ const inner = document.createElement( 'div' );
1934
+ setPaletteRoles( inner, [ '#00ff00' ] );
1935
+ outer.appendChild( inner );
1996
1936
 
1997
1937
  render(
1998
- <GlobalChartsProvider theme={ cssVarTheme }>
1938
+ <GlobalChartsProvider>
1999
1939
  <TestComponent />
2000
- </GlobalChartsProvider>
1940
+ </GlobalChartsProvider>,
1941
+ { container: inner }
2001
1942
  );
2002
1943
 
2003
- const color1 = contextValue.getElementStyles( {
2004
- data: undefined,
2005
- index: 0,
2006
- } ).color;
2007
- const color2 = contextValue.getElementStyles( {
2008
- data: undefined,
2009
- index: 1,
2010
- } ).color;
2011
- const color3 = contextValue.getElementStyles( {
2012
- data: undefined,
2013
- index: 2,
2014
- } ).color;
2015
-
2016
- expect( color1 ).toBe( '#ff0000' );
2017
- expect( color2 ).toBe( '#00ff00' );
2018
- expect( color3 ).toBe( '#0000ff' );
1944
+ expect( colorAt( 0 ) ).toBe( '#00ff00' );
2019
1945
  } );
2020
1946
  } );
2021
1947
 
2022
- describe( 'CSS Variable Edge Cases', () => {
2023
- it( 'skips CSS variables that are not defined', () => {
2024
- window.getComputedStyle = jest.fn( () => ( {
2025
- getPropertyValue: () => {
2026
- // Undefined var returns empty string
2027
- return '';
2028
- },
2029
- } ) ) as unknown as typeof window.getComputedStyle;
2030
-
2031
- let contextValue: GlobalChartsContextValue;
1948
+ describe( 'Gaps in the palette', () => {
1949
+ // Setting 1 and 3 gives a two-color palette, not a three-color one with a hole.
1950
+ it( 'compacts over a slot that resolves to nothing', () => {
1951
+ renderWithSlots( [ '#ff0000', undefined, '#0000ff' ] );
2032
1952
 
2033
- const TestComponent = () => {
2034
- contextValue = useGlobalChartsContext();
2035
- return <div>Test</div>;
2036
- };
2037
-
2038
- const cssVarTheme: ChartTheme = {
2039
- colors: [ '--undefined-color', '#ff0000' ],
2040
- } as ChartTheme;
2041
-
2042
- render(
2043
- <GlobalChartsProvider theme={ cssVarTheme }>
2044
- <TestComponent />
2045
- </GlobalChartsProvider>
2046
- );
2047
-
2048
- // Should skip undefined variable and use second color
2049
- const color = contextValue.getElementStyles( {
2050
- data: undefined,
2051
- index: 0,
2052
- } ).color;
2053
-
2054
- expect( color ).toBe( '#ff0000' );
1953
+ expect( colorAt( 0 ) ).toBe( '#ff0000' );
1954
+ expect( colorAt( 1 ) ).toBe( '#0000ff' );
2055
1955
  } );
2056
1956
 
2057
- it( 'skips CSS variables that resolve to empty string', () => {
2058
- window.getComputedStyle = jest.fn( () => ( {
2059
- getPropertyValue: () => '', // Returns empty string
2060
- } ) ) as unknown as typeof window.getComputedStyle;
2061
-
2062
- let contextValue: GlobalChartsContextValue;
2063
-
2064
- const TestComponent = () => {
2065
- contextValue = useGlobalChartsContext();
2066
- return <div>Test</div>;
2067
- };
2068
-
2069
- const cssVarTheme: ChartTheme = {
2070
- colors: [ '--undefined-color', '#00ff00' ],
2071
- } as ChartTheme;
2072
-
2073
- render(
2074
- <GlobalChartsProvider theme={ cssVarTheme }>
2075
- <TestComponent />
2076
- </GlobalChartsProvider>
2077
- );
2078
-
2079
- // Undefined variable should be skipped, second color becomes first
2080
- const color = contextValue.getElementStyles( {
2081
- data: undefined,
2082
- index: 0,
2083
- } ).color;
1957
+ it( 'falls back to the slot 1 seed when no role is declared', () => {
1958
+ renderWithSlots( [] );
2084
1959
 
2085
- expect( color ).toBe( '#00ff00' );
1960
+ expect( colorAt( 0 ) ).toBe( '#3858e9' );
2086
1961
  } );
1962
+ } );
2087
1963
 
2088
- it( 'handles CSS variables resolving to non-hex colors', () => {
2089
- window.getComputedStyle = jest.fn( () => ( {
2090
- getPropertyValue: ( prop: string ) => {
2091
- if ( prop === '--rgb-color' ) {
2092
- return 'rgb(255, 0, 0)'; // Non-hex format
2093
- }
2094
- return '';
2095
- },
2096
- } ) ) as unknown as typeof window.getComputedStyle;
2097
-
2098
- let contextValue: GlobalChartsContextValue;
2099
-
2100
- const TestComponent = () => {
2101
- contextValue = useGlobalChartsContext();
2102
- return <div>Test</div>;
2103
- };
2104
-
2105
- const cssVarTheme: ChartTheme = {
2106
- colors: [ '--rgb-color', '#00ff00' ],
2107
- } as ChartTheme;
2108
-
2109
- render(
2110
- <GlobalChartsProvider theme={ cssVarTheme }>
2111
- <TestComponent />
2112
- </GlobalChartsProvider>
2113
- );
2114
-
2115
- // RGB colors should now be converted to hex and used
2116
- const color = contextValue.getElementStyles( {
2117
- data: undefined,
2118
- index: 0,
2119
- } ).color;
1964
+ describe( 'Color formats', () => {
1965
+ it( 'converts an rgb() value to hex', () => {
1966
+ renderWithSlots( [ 'rgb(255, 0, 0)' ] );
2120
1967
 
2121
- expect( color ).toBe( '#ff0000' );
1968
+ expect( colorAt( 0 ) ).toBe( '#ff0000' );
2122
1969
  } );
2123
1970
 
2124
- it( 'handles CSS variables resolving to HSL colors', () => {
2125
- window.getComputedStyle = jest.fn( () => ( {
2126
- getPropertyValue: ( prop: string ) => {
2127
- if ( prop === '--hsl-color' ) {
2128
- return 'hsl(120, 100%, 50%)'; // HSL format (green)
2129
- }
2130
- return '';
2131
- },
2132
- } ) ) as unknown as typeof window.getComputedStyle;
2133
-
2134
- let contextValue: GlobalChartsContextValue;
2135
-
2136
- const TestComponent = () => {
2137
- contextValue = useGlobalChartsContext();
2138
- return <div>Test</div>;
2139
- };
1971
+ it( 'converts an hsl() value to hex', () => {
1972
+ renderWithSlots( [ 'hsl(120, 100%, 50%)' ] );
2140
1973
 
2141
- const cssVarTheme: ChartTheme = {
2142
- colors: [ '--hsl-color', '#ff0000' ],
2143
- } as ChartTheme;
2144
-
2145
- render(
2146
- <GlobalChartsProvider theme={ cssVarTheme }>
2147
- <TestComponent />
2148
- </GlobalChartsProvider>
2149
- );
2150
-
2151
- // HSL colors should be converted to hex and used
2152
- const color = contextValue.getElementStyles( {
2153
- data: undefined,
2154
- index: 0,
2155
- } ).color;
2156
-
2157
- expect( color ).toBe( '#00ff00' );
1974
+ expect( colorAt( 0 ) ).toBe( '#00ff00' );
2158
1975
  } );
2159
1976
 
2160
- it( 'handles CSS variables resolving to RGBA colors', () => {
2161
- window.getComputedStyle = jest.fn( () => ( {
2162
- getPropertyValue: ( prop: string ) => {
2163
- if ( prop === '--rgba-color' ) {
2164
- return 'rgba(0, 0, 255, 0.5)'; // RGBA format (blue with transparency)
2165
- }
2166
- return '';
2167
- },
2168
- } ) ) as unknown as typeof window.getComputedStyle;
2169
-
2170
- let contextValue: GlobalChartsContextValue;
2171
-
2172
- const TestComponent = () => {
2173
- contextValue = useGlobalChartsContext();
2174
- return <div>Test</div>;
2175
- };
2176
-
2177
- const cssVarTheme: ChartTheme = {
2178
- colors: [ '--rgba-color', '#ff0000' ],
2179
- } as ChartTheme;
2180
-
2181
- render(
2182
- <GlobalChartsProvider theme={ cssVarTheme }>
2183
- <TestComponent />
2184
- </GlobalChartsProvider>
2185
- );
2186
-
2187
- // RGBA colors are converted to hex (alpha is stripped)
2188
- const color = contextValue.getElementStyles( {
2189
- data: undefined,
2190
- index: 0,
2191
- } ).color;
1977
+ it( 'converts an rgba() value to hex, dropping the alpha', () => {
1978
+ renderWithSlots( [ 'rgba(0, 0, 255, 0.5)' ] );
2192
1979
 
2193
- expect( color ).toBe( '#0000ff' );
1980
+ expect( colorAt( 0 ) ).toBe( '#0000ff' );
2194
1981
  } );
2195
1982
 
2196
- it( 'handles mix of RGB, HSL, and hex in theme colors', () => {
2197
- window.getComputedStyle = jest.fn( () => ( {
2198
- getPropertyValue: ( prop: string ) => {
2199
- if ( prop === '--rgb-red' ) {
2200
- return 'rgb(255, 0, 0)';
2201
- }
2202
- if ( prop === '--hsl-green' ) {
2203
- return 'hsl(120, 100%, 50%)';
2204
- }
2205
- return '';
2206
- },
2207
- } ) ) as unknown as typeof window.getComputedStyle;
2208
-
2209
- let contextValue: GlobalChartsContextValue;
2210
-
2211
- const TestComponent = () => {
2212
- contextValue = useGlobalChartsContext();
2213
- return <div>Test</div>;
2214
- };
2215
-
2216
- const cssVarTheme: ChartTheme = {
2217
- colors: [ '--rgb-red', '--hsl-green', '#0000ff' ],
2218
- } as ChartTheme;
1983
+ it( 'handles a mix of formats across slots', () => {
1984
+ renderWithSlots( [ 'rgb(255, 0, 0)', 'hsl(120, 100%, 50%)', '#0000ff' ] );
2219
1985
 
2220
- render(
2221
- <GlobalChartsProvider theme={ cssVarTheme }>
2222
- <TestComponent />
2223
- </GlobalChartsProvider>
2224
- );
2225
-
2226
- // All color formats should be properly converted
2227
- const color1 = contextValue.getElementStyles( {
2228
- data: undefined,
2229
- index: 0,
2230
- } ).color;
2231
- const color2 = contextValue.getElementStyles( {
2232
- data: undefined,
2233
- index: 1,
2234
- } ).color;
2235
- const color3 = contextValue.getElementStyles( {
2236
- data: undefined,
2237
- index: 2,
2238
- } ).color;
2239
-
2240
- expect( color1 ).toBe( '#ff0000' ); // RGB red
2241
- expect( color2 ).toBe( '#00ff00' ); // HSL green
2242
- expect( color3 ).toBe( '#0000ff' ); // Hex blue
1986
+ expect( colorAt( 0 ) ).toBe( '#ff0000' );
1987
+ expect( colorAt( 1 ) ).toBe( '#00ff00' );
1988
+ expect( colorAt( 2 ) ).toBe( '#0000ff' );
2243
1989
  } );
2244
1990
  } );
2245
1991
 
2246
1992
  describe( 'Error Handling', () => {
2247
- it( 'handles invalid hex colors gracefully during HSL conversion', () => {
2248
- window.getComputedStyle = jest.fn( () => ( {
2249
- getPropertyValue: () => '',
2250
- } ) ) as unknown as typeof window.getComputedStyle;
2251
-
2252
- let contextValue: GlobalChartsContextValue;
2253
-
2254
- const TestComponent = () => {
2255
- contextValue = useGlobalChartsContext();
2256
- return <div>Test</div>;
2257
- };
2258
-
2259
- const invalidTheme: ChartTheme = {
2260
- colors: [ '#invalid', '#ff0000' ],
2261
- } as ChartTheme;
2262
-
2263
- // Should not crash
2264
- expect( () => {
2265
- render(
2266
- <GlobalChartsProvider theme={ invalidTheme }>
2267
- <TestComponent />
2268
- </GlobalChartsProvider>
2269
- );
2270
- } ).not.toThrow();
2271
-
2272
- // Invalid colors are still added to palette, just don't contribute to HSL calculations
2273
- const color1 = contextValue.getElementStyles( {
2274
- data: undefined,
2275
- index: 0,
2276
- } ).color;
2277
- const color2 = contextValue.getElementStyles( {
2278
- data: undefined,
2279
- index: 1,
2280
- } ).color;
2281
-
2282
- expect( color1 ).toBe( '#invalid' );
2283
- expect( color2 ).toBe( '#ff0000' );
2284
- } );
2285
-
2286
- it( 'handles malformed hex colors in color cache', () => {
2287
- window.getComputedStyle = jest.fn( () => ( {
2288
- getPropertyValue: () => '',
2289
- } ) ) as unknown as typeof window.getComputedStyle;
2290
-
2291
- let contextValue: GlobalChartsContextValue;
2292
-
2293
- const TestComponent = () => {
2294
- contextValue = useGlobalChartsContext();
2295
- return <div>Test</div>;
2296
- };
2297
-
2298
- const malformedTheme: ChartTheme = {
2299
- colors: [ '#ff', '#gggggg', '#00ff00' ],
2300
- } as ChartTheme;
2301
-
2302
- // Should not crash
2303
- expect( () => {
2304
- render(
2305
- <GlobalChartsProvider theme={ malformedTheme }>
2306
- <TestComponent />
2307
- </GlobalChartsProvider>
2308
- );
2309
- } ).not.toThrow();
2310
-
2311
- // Malformed colors are still in palette, just don't contribute to HSL calculations
2312
- const color1 = contextValue.getElementStyles( {
2313
- data: undefined,
2314
- index: 0,
2315
- } ).color;
2316
- const color2 = contextValue.getElementStyles( {
2317
- data: undefined,
2318
- index: 1,
2319
- } ).color;
2320
- const color3 = contextValue.getElementStyles( {
2321
- data: undefined,
2322
- index: 2,
2323
- } ).color;
1993
+ it( 'keeps an unparseable value without crashing', () => {
1994
+ expect( () => renderWithSlots( [ '#invalid', '#ff0000' ] ) ).not.toThrow();
2324
1995
 
2325
- expect( color1 ).toBe( '#ff' );
2326
- expect( color2 ).toBe( '#gggggg' );
2327
- expect( color3 ).toBe( '#00ff00' );
1996
+ expect( colorAt( 0 ) ).toBe( '#invalid' );
1997
+ expect( colorAt( 1 ) ).toBe( '#ff0000' );
2328
1998
  } );
2329
1999
 
2330
- it( 'continues processing colors after encountering an error', () => {
2331
- window.getComputedStyle = jest.fn( () => ( {
2332
- getPropertyValue: ( prop: string ) => {
2333
- if ( prop === '--valid-1' ) {
2334
- return '#ff0000';
2335
- }
2336
- if ( prop === '--invalid' ) {
2337
- return '#bad'; // Will fail HSL conversion
2338
- }
2339
- if ( prop === '--valid-2' ) {
2340
- return '#0000ff';
2341
- }
2342
- return '';
2343
- },
2344
- } ) ) as unknown as typeof window.getComputedStyle;
2345
-
2346
- let contextValue: GlobalChartsContextValue;
2347
-
2348
- const TestComponent = () => {
2349
- contextValue = useGlobalChartsContext();
2350
- return <div>Test</div>;
2351
- };
2352
-
2353
- const mixedTheme: ChartTheme = {
2354
- colors: [ '--valid-1', '--invalid', '--valid-2' ],
2355
- } as ChartTheme;
2000
+ it( 'keeps malformed hex values without crashing', () => {
2001
+ expect( () => renderWithSlots( [ '#ff', '#gggggg', '#00ff00' ] ) ).not.toThrow();
2356
2002
 
2357
- render(
2358
- <GlobalChartsProvider theme={ mixedTheme }>
2359
- <TestComponent />
2360
- </GlobalChartsProvider>
2361
- );
2362
-
2363
- // All colors are preserved in order, invalid ones just don't contribute to HSL
2364
- const color1 = contextValue.getElementStyles( {
2365
- data: undefined,
2366
- index: 0,
2367
- } ).color;
2368
- const color2 = contextValue.getElementStyles( {
2369
- data: undefined,
2370
- index: 1,
2371
- } ).color;
2372
- const color3 = contextValue.getElementStyles( {
2373
- data: undefined,
2374
- index: 2,
2375
- } ).color;
2376
-
2377
- expect( color1 ).toBe( '#ff0000' );
2378
- expect( color2 ).toBe( '#bbaadd' ); // #bad is expanded to #bbaadd by normalizeColorToHex
2379
- expect( color3 ).toBe( '#0000ff' );
2003
+ expect( colorAt( 0 ) ).toBe( '#ff' );
2004
+ expect( colorAt( 1 ) ).toBe( '#gggggg' );
2005
+ expect( colorAt( 2 ) ).toBe( '#00ff00' );
2380
2006
  } );
2381
- } );
2382
-
2383
- describe( 'Color Cache Timing', () => {
2384
- it( 'recomputes color cache when theme colors change with CSS variables', () => {
2385
- let mockColor = '#ff0000';
2386
- window.getComputedStyle = jest.fn( () => ( {
2387
- getPropertyValue: ( prop: string ) => {
2388
- if ( prop === '--dynamic-color' ) {
2389
- return mockColor;
2390
- }
2391
- return '';
2392
- },
2393
- } ) ) as unknown as typeof window.getComputedStyle;
2394
2007
 
2395
- let contextValue: GlobalChartsContextValue;
2008
+ it( 'keeps resolving the later slots after one fails to convert', () => {
2009
+ renderWithSlots( [ '#ff0000', '#bad', '#0000ff' ] );
2396
2010
 
2397
- const TestComponent = () => {
2398
- contextValue = useGlobalChartsContext();
2399
- return <div>Test</div>;
2400
- };
2401
-
2402
- const theme1: ChartTheme = {
2403
- colors: [ '--dynamic-color' ],
2404
- } as ChartTheme;
2405
-
2406
- const { rerender } = render(
2407
- <GlobalChartsProvider theme={ theme1 }>
2408
- <TestComponent />
2409
- </GlobalChartsProvider>
2410
- );
2411
-
2412
- const initialColor = contextValue.getElementStyles( {
2413
- data: undefined,
2414
- index: 0,
2415
- } ).color;
2416
-
2417
- // Change the mock to return different color
2418
- mockColor = '#00ff00';
2419
-
2420
- const theme2: ChartTheme = {
2421
- colors: [ '--dynamic-color' ],
2422
- } as ChartTheme;
2423
-
2424
- // Re-render with new theme (different object reference)
2425
- rerender(
2426
- <GlobalChartsProvider theme={ theme2 }>
2427
- <TestComponent />
2428
- </GlobalChartsProvider>
2429
- );
2430
-
2431
- const updatedColor = contextValue.getElementStyles( {
2432
- data: undefined,
2433
- index: 0,
2434
- } ).color;
2435
-
2436
- expect( initialColor ).toBe( '#ff0000' );
2437
- expect( updatedColor ).toBe( '#00ff00' );
2011
+ expect( colorAt( 0 ) ).toBe( '#ff0000' );
2012
+ // #bad is expanded to #bbaadd by normalizeColorToHex
2013
+ expect( colorAt( 1 ) ).toBe( '#bbaadd' );
2014
+ expect( colorAt( 2 ) ).toBe( '#0000ff' );
2438
2015
  } );
2439
2016
  } );
2440
2017
 
2441
- describe( 'Integration with CSS Variables', () => {
2442
- it( 'generates colors based on CSS variable hue range', () => {
2443
- window.getComputedStyle = jest.fn( () => ( {
2444
- getPropertyValue: ( prop: string ) => {
2445
- // Colors with specific hue range (red to orange)
2446
- if ( prop === '--base-1' ) {
2447
- return '#ff0000'; // Hue 0
2448
- }
2449
- if ( prop === '--base-2' ) {
2450
- return '#ff8800'; // Hue ~33
2451
- }
2452
- return '';
2453
- },
2454
- } ) ) as unknown as typeof window.getComputedStyle;
2455
-
2456
- let contextValue: GlobalChartsContextValue;
2457
-
2458
- const TestComponent = () => {
2459
- contextValue = useGlobalChartsContext();
2460
- return <div>Test</div>;
2461
- };
2462
-
2463
- const cssVarTheme: ChartTheme = {
2464
- colors: [ '--base-1', '--base-2' ],
2465
- } as ChartTheme;
2018
+ describe( 'Generated colors', () => {
2019
+ it( 'generates past the seeded slots', () => {
2020
+ renderWithSlots( [ '#ff0000', '#ff8800' ] );
2466
2021
 
2467
- render(
2468
- <GlobalChartsProvider theme={ cssVarTheme }>
2469
- <TestComponent />
2470
- </GlobalChartsProvider>
2471
- );
2472
-
2473
- // Request color beyond palette
2474
- const generatedColor = contextValue.getElementStyles( {
2475
- data: undefined,
2476
- index: 3,
2477
- } ).color;
2478
-
2479
- // Should be a hex color (generated)
2480
- expect( generatedColor ).toMatch( /^#[0-9a-f]{6}$/i );
2022
+ expect( colorAt( 3 ) ).toMatch( /^#[0-9a-f]{6}$/i );
2481
2023
  } );
2482
2024
 
2483
- it( 'mixed static and CSS variable colors work together', () => {
2484
- window.getComputedStyle = jest.fn( () => ( {
2485
- getPropertyValue: ( prop: string ) => {
2486
- if ( prop === '--custom' ) {
2487
- return '#00ff00';
2488
- }
2489
- return '';
2490
- },
2491
- } ) ) as unknown as typeof window.getComputedStyle;
2492
-
2493
- let contextValue: GlobalChartsContextValue;
2494
-
2495
- const TestComponent = () => {
2496
- contextValue = useGlobalChartsContext();
2497
- return <div>Test</div>;
2498
- };
2025
+ it( 'assigns a group one of the resolved palette colors', () => {
2026
+ renderWithSlots( [ '#ff0000', '#00ff00', '#0000ff' ] );
2499
2027
 
2500
- const mixedTheme: ChartTheme = {
2501
- colors: [ '#ff0000', '--custom', '#0000ff' ],
2502
- } as ChartTheme;
2503
-
2504
- render(
2505
- <GlobalChartsProvider theme={ mixedTheme }>
2506
- <TestComponent />
2507
- </GlobalChartsProvider>
2508
- );
2509
-
2510
- const color1 = contextValue.getElementStyles( {
2511
- data: undefined,
2512
- index: 0,
2513
- } ).color;
2514
- const color2 = contextValue.getElementStyles( {
2515
- data: undefined,
2516
- index: 1,
2517
- } ).color;
2518
- const color3 = contextValue.getElementStyles( {
2519
- data: undefined,
2520
- index: 2,
2521
- } ).color;
2522
-
2523
- expect( color1 ).toBe( '#ff0000' );
2524
- expect( color2 ).toBe( '#00ff00' ); // Resolved from CSS var
2525
- expect( color3 ).toBe( '#0000ff' );
2526
-
2527
- // Verify group assignments work
2528
2028
  const groupColor = contextValue.getElementStyles( {
2529
2029
  data: createMockDataWithGroup( 'test-group' ),
2530
2030
  index: 0,
@@ -2532,150 +2032,35 @@ describe( 'ChartContext', () => {
2532
2032
 
2533
2033
  expect( [ '#ff0000', '#00ff00', '#0000ff' ] ).toContain( groupColor );
2534
2034
  } );
2535
-
2536
- it( 'CSS variable changes reset group color mappings', () => {
2537
- let mockColor = '#ff0000';
2538
- window.getComputedStyle = jest.fn( () => ( {
2539
- getPropertyValue: ( prop: string ) => {
2540
- if ( prop === '--theme-color' ) {
2541
- return mockColor;
2542
- }
2543
- return '';
2544
- },
2545
- } ) ) as unknown as typeof window.getComputedStyle;
2546
-
2547
- let contextValue: GlobalChartsContextValue;
2548
-
2549
- const TestComponent = () => {
2550
- contextValue = useGlobalChartsContext();
2551
- return <div>Test</div>;
2552
- };
2553
-
2554
- const theme1: ChartTheme = {
2555
- colors: [ '--theme-color' ],
2556
- } as ChartTheme;
2557
-
2558
- const { rerender } = render(
2559
- <GlobalChartsProvider theme={ theme1 }>
2560
- <TestComponent />
2561
- </GlobalChartsProvider>
2562
- );
2563
-
2564
- // Create group mapping
2565
- const initialGroupColor = contextValue.getElementStyles( {
2566
- data: createMockDataWithGroup( 'test-group' ),
2567
- index: 0,
2568
- } ).color;
2569
-
2570
- expect( initialGroupColor ).toBe( '#ff0000' );
2571
-
2572
- // Change theme colors
2573
- mockColor = '#00ff00';
2574
- const theme2: ChartTheme = {
2575
- colors: [ '--theme-color' ],
2576
- } as ChartTheme;
2577
-
2578
- rerender(
2579
- <GlobalChartsProvider theme={ theme2 }>
2580
- <TestComponent />
2581
- </GlobalChartsProvider>
2582
- );
2583
-
2584
- // Group mapping should be reset and use new color
2585
- const updatedGroupColor = contextValue.getElementStyles( {
2586
- data: createMockDataWithGroup( 'test-group' ),
2587
- index: 0,
2588
- } ).color;
2589
-
2590
- expect( updatedGroupColor ).toBe( '#00ff00' );
2591
- } );
2592
2035
  } );
2593
2036
 
2594
2037
  describe( 'Server-Side Rendering', () => {
2595
- it( 'handles SSR environment where getComputedStyle unavailable', () => {
2596
- // Mock resolveCssVariable to return null (simulating SSR behavior)
2038
+ // Slot 1's terminal literal is the palette's only carrier under SSR.
2039
+ it( 'falls back to the seed when getComputedStyle is unavailable', () => {
2597
2040
  window.getComputedStyle = jest.fn( () => {
2598
2041
  throw new Error( 'window is not defined' );
2599
2042
  } ) as unknown as typeof window.getComputedStyle;
2600
2043
 
2601
- let contextValue: GlobalChartsContextValue;
2602
-
2603
- const TestComponent = () => {
2604
- contextValue = useGlobalChartsContext();
2605
- return <div>Test</div>;
2606
- };
2607
-
2608
- const cssVarTheme: ChartTheme = {
2609
- colors: [ '--ssr-color', '#ff0000' ],
2610
- } as ChartTheme;
2611
-
2612
- // Should not crash during SSR
2613
- expect( () => {
2614
- render(
2615
- <GlobalChartsProvider theme={ cssVarTheme }>
2616
- <TestComponent />
2617
- </GlobalChartsProvider>
2618
- );
2619
- } ).not.toThrow();
2620
-
2621
- // Should fall back to static colors
2622
- const color = contextValue.getElementStyles( {
2623
- data: undefined,
2624
- index: 0,
2625
- } ).color;
2044
+ expect( () => renderWithSlots( [ '#ff0000' ] ) ).not.toThrow();
2626
2045
 
2627
- expect( color ).toBe( '#ff0000' );
2046
+ expect( colorAt( 0 ) ).toBe( '#3858e9' );
2628
2047
  } );
2629
2048
  } );
2630
2049
 
2631
2050
  describe( 'Performance and Caching', () => {
2632
- it( 'color cache remains stable when theme unchanged with CSS vars', () => {
2633
- window.getComputedStyle = jest.fn( () => ( {
2634
- getPropertyValue: ( prop: string ) => {
2635
- if ( prop === '--stable-color' ) {
2636
- return '#ff0000';
2637
- }
2638
- return '';
2639
- },
2640
- } ) ) as unknown as typeof window.getComputedStyle;
2641
-
2642
- let contextValue: GlobalChartsContextValue;
2643
-
2644
- const TestComponent = () => {
2645
- contextValue = useGlobalChartsContext();
2646
- return <div>Test</div>;
2647
- };
2648
-
2649
- const cssVarTheme: ChartTheme = {
2650
- colors: [ '--stable-color' ],
2651
- } as ChartTheme;
2652
-
2653
- const { rerender } = render(
2654
- <GlobalChartsProvider theme={ cssVarTheme }>
2655
- <TestComponent />
2656
- </GlobalChartsProvider>
2657
- );
2051
+ it( 'keeps the palette stable across a re-render', () => {
2052
+ const { rerender } = renderWithSlots( [ '#ff0000' ] );
2658
2053
 
2659
- const initialColor = contextValue.getElementStyles( {
2660
- data: undefined,
2661
- index: 0,
2662
- } ).color;
2054
+ const initialColor = colorAt( 0 );
2663
2055
 
2664
- // Re-render with same theme
2665
2056
  rerender(
2666
- <GlobalChartsProvider theme={ cssVarTheme }>
2057
+ <GlobalChartsProvider>
2667
2058
  <TestComponent />
2668
2059
  </GlobalChartsProvider>
2669
2060
  );
2670
2061
 
2671
- const afterRerenderColor = contextValue.getElementStyles( {
2672
- data: undefined,
2673
- index: 0,
2674
- } ).color;
2675
-
2676
- // Colors should remain stable
2677
- expect( afterRerenderColor ).toBe( initialColor );
2678
- expect( afterRerenderColor ).toBe( '#ff0000' );
2062
+ expect( colorAt( 0 ) ).toBe( initialColor );
2063
+ expect( colorAt( 0 ) ).toBe( '#ff0000' );
2679
2064
  } );
2680
2065
  } );
2681
2066
  } );