@automattic/charts 4.3.0 → 4.5.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 (41) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/TOKENS.md +2 -2
  3. package/dist/index.cjs +584 -209
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +4 -0
  6. package/dist/index.d.cts +14 -3
  7. package/dist/index.d.ts +14 -3
  8. package/dist/index.js +585 -210
  9. package/dist/index.js.map +1 -1
  10. package/package.json +2 -2
  11. package/src/charts/bar-chart/bar-chart.tsx +57 -23
  12. package/src/charts/bar-chart/private/band-tooltip.tsx +18 -12
  13. package/src/charts/bar-chart/private/classified-bar-series.tsx +85 -0
  14. package/src/charts/bar-chart/private/comparison-bars-geometry.ts +3 -6
  15. package/src/charts/bar-chart/private/index.ts +1 -0
  16. package/src/charts/bar-chart/private/test/comparison-bars-geometry.test.ts +2 -3
  17. package/src/charts/bar-chart/test/bar-chart.test.tsx +130 -1
  18. package/src/charts/bar-chart/types.ts +5 -1
  19. package/src/charts/geo-chart/geo-chart.tsx +4 -0
  20. package/src/charts/geo-chart/test/geo-chart.test.tsx +8 -0
  21. package/src/charts/geo-chart/types.ts +5 -0
  22. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +1 -1
  23. package/src/charts/pie-chart/pie-chart.module.scss +4 -0
  24. package/src/charts/pie-chart/pie-chart.tsx +103 -7
  25. package/src/charts/pie-chart/test/pie-chart.test.tsx +149 -0
  26. package/src/charts/private/chart-layout/chart-layout.tsx +5 -1
  27. package/src/components/tooltip/accessible-tooltip.tsx +40 -4
  28. package/src/components/tooltip/test/accessible-tooltip.test.tsx +82 -5
  29. package/src/providers/chart-context/global-charts-provider.tsx +57 -41
  30. package/src/providers/chart-context/private/catalog-pointers.ts +9 -1
  31. package/src/providers/chart-context/private/palette-generator.ts +305 -0
  32. package/src/providers/chart-context/private/perceptual-color.ts +219 -0
  33. package/src/providers/chart-context/private/series-palette.ts +1 -1
  34. package/src/providers/chart-context/test/chart-context.test.tsx +88 -8
  35. package/src/providers/chart-context/test/global-charts-provider-first-render.test.tsx +27 -0
  36. package/src/providers/chart-context/test/palette-generator.test.ts +181 -0
  37. package/src/providers/chart-context/test/perceptual-color.test.ts +75 -0
  38. package/src/test-utils/zoned-parsing-suite.ts +1 -1
  39. package/src/utils/color-utils.ts +5 -3
  40. package/src/utils/test/date-formatting.test.ts +1 -1
  41. package/src/providers/chart-context/private/get-chart-color.ts +0 -223
package/dist/index.cjs CHANGED
@@ -438,9 +438,11 @@ const hexToRgba = (hex, alpha) => {
438
438
  return (0, _visx_vendor_d3_color.color)(hex).copy({ opacity: alpha }).formatRgb();
439
439
  };
440
440
  /**
441
- * Calculate the perceptual distance between two HSL colors
442
- * @param hsl1 - first color in HSL format [h, s, l]
443
- * @param hsl2 - second color in HSL format [h, s, l]
441
+ * Weighted Euclidean distance between two HSL colors.
442
+ *
443
+ * @deprecated Not perceptual and blind to color vision deficiency, so charts no longer use it; it will be removed in a future major version.
444
+ * @param hsl1 - first color in HSL format [h, s, l]
445
+ * @param hsl2 - second color in HSL format [h, s, l]
444
446
  * @return distance value (0-100+, lower means more similar)
445
447
  */
446
448
  const getColorDistance = (hsl1, hsl2) => {
@@ -802,145 +804,421 @@ const createZonedClock = (timeZone) => {
802
804
  * All JS token resolution reads this element rather than `document.documentElement`, so a `getComputedStyle` call sees the same `--a8c-charts-*` values — including any override set inside the provider tree — that a CSS-painted sibling element does.
803
805
  */
804
806
  const ChartScopeContext = (0, react.createContext)(null);
805
- //#endregion
806
- //#region src/providers/chart-context/private/get-chart-color.ts
807
- /**
808
- * Golden ratio for mathematically pleasing color distribution
809
- * Used to generate evenly spaced hues that are visually distinct
810
- */
811
- const GOLDEN_RATIO = .618033988749;
812
- /**
813
- * Minimum perceptual distance between colors to ensure visual distinction
814
- * Based on weighted HSL distance calculation optimized for chart readability
815
- */
816
- const MIN_COLOR_DISTANCE = 25;
817
- /**
818
- * Maximum attempts to find a sufficiently different color
819
- * Prevents infinite loops while allowing reasonable search space
820
- */
821
- const MAX_COLOR_GENERATION_ATTEMPTS = 50;
822
- /**
823
- * Color variation attempt offset
824
- * Small increment to explore slightly different color variations per attempt
825
- */
826
- const VARIATION_ATTEMPT_OFFSET = .1;
827
- /**
828
- * Base saturation percentage for generated colors
829
- * 45% provides muted, professional colors without being washed out
830
- */
831
- const BASE_SATURATION = 45;
832
- /**
833
- * Number of saturation variation steps
834
- * Creates 3 different saturation levels for variety
835
- */
836
- const SATURATION_VARIATION_STEPS = 3;
837
807
  /**
838
- * Saturation increment per variation step
839
- * 10% increments provide subtle variation while keeping colors muted
840
- * Results in saturation levels: 45%, 55%, 65%
841
- */
842
- const SATURATION_INCREMENT = 10;
843
- /**
844
- * Base lightness percentage for generated colors
845
- * 35% ensures sufficient contrast against white backgrounds for WCAG AA compliance
846
- * WCAG AA requires 4.5:1 contrast ratio for normal text
808
+ * The catalog role holding one series-palette slot.
809
+ *
810
+ * @param slot - The one-based slot number.
811
+ * @return The role name.
847
812
  */
848
- const BASE_LIGHTNESS = 35;
813
+ const seriesRole = (slot) => `--a8c-charts-color-series-${slot}`;
814
+ const SERIES_SLOT_1_FALLBACK = "#3858e9";
815
+ /** The catalog pointer for every slot, in slot order. */
816
+ const SERIES_PALETTE_POINTERS = Array.from({ length: 5 }, (_, index) => index === 0 ? `var(${seriesRole(1)}, ${SERIES_SLOT_1_FALLBACK})` : `var(${seriesRole(index + 1)})`);
817
+ //#endregion
818
+ //#region src/providers/chart-context/private/catalog-pointers.ts
819
+ /** Terminal literals the provider also generates from before its first read, so SSR and the first client render agree. */
820
+ const BACKGROUND_FALLBACK = "#ffffff";
821
+ const LABEL_FALLBACK = "#1e1e1e";
822
+ const LABEL_INVERSE_FALLBACK = "#f0f0f0";
823
+ const CATALOG_POINTERS = {
824
+ background: `var(--a8c-charts-color-background, ${BACKGROUND_FALLBACK})`,
825
+ label: `var(--a8c-charts-color-label, ${LABEL_FALLBACK})`,
826
+ labelAxis: "var(--a8c-charts-color-label-axis, #1e1e1e)",
827
+ labelInverse: `var(--a8c-charts-color-label-inverse, ${LABEL_INVERSE_FALLBACK})`,
828
+ labelBackground: "var(--a8c-charts-color-label-background, transparent)",
829
+ grid: "var(--a8c-charts-color-grid, #dbdbdb)",
830
+ axisX: "var(--a8c-charts-color-axis-x, #dbdbdb)",
831
+ tickX: "var(--a8c-charts-color-tick-x, #dbdbdb)",
832
+ axisY: "var(--a8c-charts-color-axis-y, none)",
833
+ tickY: "var(--a8c-charts-color-tick-y, none)",
834
+ annotation: "var(--a8c-charts-color-annotation, #1e1e1e)",
835
+ surface: "var(--a8c-charts-color-surface, #fff)",
836
+ surfaceSecondary: "var(--a8c-charts-color-surface-secondary, #f4f4f4)",
837
+ trendUp: "var(--a8c-charts-color-trend-up, #008030)",
838
+ trendDown: "var(--a8c-charts-color-trend-down, #cc1818)",
839
+ series: SERIES_PALETTE_POINTERS
840
+ };
849
841
  /**
850
- * Number of lightness variation steps
851
- * Creates 4 different lightness levels for variety
842
+ * The annotation parts visx paints, in the shape `@visx/annotation` takes. `radius`
843
+ * rides along as the base the theme and the per-datum styles merge onto.
852
844
  */
853
- const LIGHTNESS_VARIATION_STEPS = 4;
845
+ const ANNOTATION_POINTERS = {
846
+ label: {
847
+ anchorLineStroke: CATALOG_POINTERS.annotation,
848
+ backgroundFill: CATALOG_POINTERS.surface
849
+ },
850
+ connector: { stroke: CATALOG_POINTERS.annotation },
851
+ circleSubject: {
852
+ stroke: "transparent",
853
+ fill: CATALOG_POINTERS.annotation,
854
+ radius: 5
855
+ }
856
+ };
857
+ //#endregion
858
+ //#region src/providers/chart-context/private/perceptual-color.ts
859
+ const DEUTERANOPIA = [
860
+ .367322,
861
+ .860646,
862
+ -.227968,
863
+ .280085,
864
+ .672501,
865
+ .047413,
866
+ -.01182,
867
+ .04294,
868
+ .968881
869
+ ];
870
+ const PROTANOPIA = [
871
+ .152286,
872
+ 1.052583,
873
+ -.204868,
874
+ .114503,
875
+ .786281,
876
+ .099216,
877
+ -.003882,
878
+ -.048116,
879
+ 1.051998
880
+ ];
881
+ const DEGREES = Math.PI / 180;
882
+ const toLinear = (channel) => channel <= .04045 ? channel / 12.92 : Math.pow((channel + .055) / 1.055, 2.4);
883
+ const fromLinear = (channel) => channel <= .0031308 ? 12.92 * channel : 1.055 * Math.pow(channel, 1 / 2.4) - .055;
884
+ const hexToLinear = (hex) => {
885
+ validateHexColor(hex);
886
+ return [
887
+ 1,
888
+ 3,
889
+ 5
890
+ ].map((start) => toLinear(parseInt(hex.slice(start, start + 2), 16) / 255));
891
+ };
892
+ const linearToHex = (linear) => "#" + linear.map((channel) => Math.round(Math.min(1, Math.max(0, fromLinear(channel))) * 255).toString(16).padStart(2, "0")).join("");
893
+ const simulate = (matrix, [r, g, b]) => [
894
+ 0,
895
+ 3,
896
+ 6
897
+ ].map((row) => Math.min(1, Math.max(0, matrix[row] * r + matrix[row + 1] * g + matrix[row + 2] * b)));
898
+ const linearToLab = ([r, g, b]) => {
899
+ const x = (.4124564 * r + .3575761 * g + .1804375 * b) / .95047;
900
+ const y = .2126729 * r + .7151522 * g + .072175 * b;
901
+ const z = (.0193339 * r + .119192 * g + .9503041 * b) / 1.08883;
902
+ const f = (t) => t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116;
903
+ return [
904
+ 116 * f(y) - 16,
905
+ 500 * (f(x) - f(y)),
906
+ 200 * (f(y) - f(z))
907
+ ];
908
+ };
854
909
  /**
855
- * Lightness increment per variation step
856
- * 8% increments provide subtle lightness variation while maintaining accessibility
857
- * Results in lightness levels: 35%, 43%, 51%, 59%
858
- * All levels maintain WCAG AA compliance against white backgrounds
859
- */
860
- const LIGHTNESS_INCREMENT = 8;
910
+ * CIEDE2000 color difference between two CIELAB colors.
911
+ *
912
+ * @param first - First color in CIELAB.
913
+ * @param second - Second color in CIELAB.
914
+ * @return ΔE00; about 10 is where two chart categories stop being reliably distinguishable.
915
+ */
916
+ const deltaE2000 = (first, second) => {
917
+ const [l1, a1, b1] = first;
918
+ const [l2, a2, b2] = second;
919
+ const chromaMean = (Math.hypot(a1, b1) + Math.hypot(a2, b2)) / 2;
920
+ const g = .5 * (1 - Math.sqrt(chromaMean ** 7 / (chromaMean ** 7 + 25 ** 7)));
921
+ const a1p = a1 * (1 + g);
922
+ const a2p = a2 * (1 + g);
923
+ const c1p = Math.hypot(a1p, b1);
924
+ const c2p = Math.hypot(a2p, b2);
925
+ const hueOf = (a, b) => a === 0 && b === 0 ? 0 : (Math.atan2(b, a) / DEGREES + 360) % 360;
926
+ const h1p = hueOf(a1p, b1);
927
+ const h2p = hueOf(a2p, b2);
928
+ const chromatic = c1p * c2p !== 0;
929
+ let hueDelta = chromatic ? h2p - h1p : 0;
930
+ if (hueDelta > 180) hueDelta -= 360;
931
+ else if (hueDelta < -180) hueDelta += 360;
932
+ const lightnessDelta = l2 - l1;
933
+ const chromaDelta = c2p - c1p;
934
+ const hueDifference = 2 * Math.sqrt(c1p * c2p) * Math.sin(hueDelta * DEGREES / 2);
935
+ const lightnessMean = (l1 + l2) / 2;
936
+ const chromaMeanPrime = (c1p + c2p) / 2;
937
+ let hueMean = h1p + h2p;
938
+ if (chromatic) {
939
+ if (Math.abs(h1p - h2p) > 180) hueMean += hueMean < 360 ? 360 : -360;
940
+ hueMean /= 2;
941
+ }
942
+ const t = 1 - .17 * Math.cos((hueMean - 30) * DEGREES) + .24 * Math.cos(2 * hueMean * DEGREES) + .32 * Math.cos((3 * hueMean + 6) * DEGREES) - .2 * Math.cos((4 * hueMean - 63) * DEGREES);
943
+ const lightnessScale = 1 + .015 * (lightnessMean - 50) ** 2 / Math.sqrt(20 + (lightnessMean - 50) ** 2);
944
+ const chromaScale = 1 + .045 * chromaMeanPrime;
945
+ const hueScale = 1 + .015 * chromaMeanPrime * t;
946
+ const rotation = -2 * Math.sqrt(chromaMeanPrime ** 7 / (chromaMeanPrime ** 7 + 25 ** 7)) * Math.sin(60 * Math.exp(-(((hueMean - 275) / 25) ** 2)) * DEGREES);
947
+ const lightnessTerm = lightnessDelta / lightnessScale;
948
+ const chromaTerm = chromaDelta / chromaScale;
949
+ const hueTerm = hueDifference / hueScale;
950
+ return Math.sqrt(lightnessTerm ** 2 + chromaTerm ** 2 + hueTerm ** 2 + rotation * chromaTerm * hueTerm);
951
+ };
861
952
  /**
862
- * Minimum hue range in degrees to ensure sufficient color variety
863
- * 60 degrees provides reasonable color spread even for narrow palettes
953
+ * A hex color in CIELAB as seen in normal vision, under deuteranopia and under protanopia.
954
+ *
955
+ * @param hex - Six-digit hex color.
956
+ * @return The three views.
957
+ * @throws {Error} if hex string is malformed
864
958
  */
865
- const MIN_HUE_RANGE_DEGREES = 60;
959
+ const hexToViews = (hex) => {
960
+ const linear = hexToLinear(hex);
961
+ return [
962
+ linearToLab(linear),
963
+ linearToLab(simulate(DEUTERANOPIA, linear)),
964
+ linearToLab(simulate(PROTANOPIA, linear))
965
+ ];
966
+ };
866
967
  /**
867
- * Hue range expansion factor
868
- * 1.3x expansion provides slightly more variety than the original palette
968
+ * The smallest ΔE00 between two colors across the three views.
969
+ *
970
+ * @param first - First color's views.
971
+ * @param second - Second color's views.
972
+ * @return The distance in the view where the two are hardest to tell apart.
869
973
  */
870
- const HUE_RANGE_EXPANSION_FACTOR = 1.3;
974
+ const viewDistance = (first, second) => Math.min(deltaE2000(first[0], second[0]), deltaE2000(first[1], second[1]), deltaE2000(first[2], second[2]));
871
975
  /**
872
- * Threshold for detecting hue wrap-around (color wheel boundary crossing)
873
- * 180 degrees indicates the colors span more than half the color wheel
976
+ * Convert an OKLCH color to sRGB hex.
977
+ *
978
+ * @param lightness - OKLab L, 0 to 1.
979
+ * @param chroma - OKLCH C.
980
+ * @param hue - Hue in degrees.
981
+ * @return Hex color, or null when the color is outside the sRGB gamut.
874
982
  */
875
- const HUE_WRAP_THRESHOLD_DEGREES = 180;
983
+ const oklchToHex = (lightness, chroma, hue) => {
984
+ const a = chroma * Math.cos(hue * DEGREES);
985
+ const b = chroma * Math.sin(hue * DEGREES);
986
+ const l = (lightness + .3963377774 * a + .2158037573 * b) ** 3;
987
+ const m = (lightness - .1055613458 * a - .0638541728 * b) ** 3;
988
+ const s = (lightness - .0894841775 * a - 1.291485548 * b) ** 3;
989
+ const linear = [
990
+ 4.0767416621 * l - 3.3077633306 * m + .2309645873 * s,
991
+ -1.2684380046 * l + 2.6097574011 * m - .3413193965 * s,
992
+ -.0041960863 * l - .7034186147 * m + 1.707614701 * s
993
+ ];
994
+ if (linear.some((channel) => channel < -1e-4 || channel > 1.0001)) return null;
995
+ return linearToHex(linear);
996
+ };
876
997
  /**
877
- * Full color wheel rotation in degrees
998
+ * OKLCH chroma and hue of a hex color.
999
+ *
1000
+ * @param hex - Six-digit hex color.
1001
+ * @return Chroma, and hue in degrees from 0 to 360.
1002
+ * @throws {Error} if hex string is malformed
878
1003
  */
879
- const FULL_HUE_ROTATION_DEGREES = 360;
1004
+ const hexToOklch = (hex) => {
1005
+ const [r, g, b] = hexToLinear(hex);
1006
+ const l = Math.cbrt(.4122214708 * r + .5363325363 * g + .0514459929 * b);
1007
+ const m = Math.cbrt(.2119034982 * r + .6806995451 * g + .1073969566 * b);
1008
+ const s = Math.cbrt(.0883024619 * r + .2817188376 * g + .6299787005 * b);
1009
+ const a = 1.9779984951 * l - 2.428592205 * m + .4505937099 * s;
1010
+ const bAxis = .0259040371 * l + .7827717662 * m - .808675766 * s;
1011
+ return {
1012
+ chroma: Math.hypot(a, bAxis),
1013
+ hue: (Math.atan2(bAxis, a) / DEGREES + 360) % 360
1014
+ };
1015
+ };
880
1016
  /**
881
- * Factor for single color hue range
1017
+ * WCAG contrast ratio between two relative luminances.
1018
+ *
1019
+ * @param first - First relative luminance.
1020
+ * @param second - Second relative luminance.
1021
+ * @return Ratio from 1 to 21.
882
1022
  */
883
- const SINGLE_COLOR_HUE_RANGE_FACTOR = .33;
1023
+ const luminanceContrastRatio = (first, second) => (Math.max(first, second) + .05) / (Math.min(first, second) + .05);
884
1024
  /**
885
- * Get a color from the colors array or generate a new color using the golden ratio
1025
+ * WCAG contrast ratio between two hex colors.
886
1026
  *
887
- * @param index - the index of the color to get
888
- * @param colorCache - pre-computed color data for performance
889
- * @return a color from the colors array or a new color using the golden ratio
1027
+ * @param first - First hex color.
1028
+ * @param second - Second hex color.
1029
+ * @return Ratio from 1 to 21.
1030
+ * @throws {Error} if either hex string is malformed
890
1031
  */
891
- const getChartColor = (index, colorCache) => {
892
- const { colors, hues, existingHslColors, minHue: cachedMinHue, maxHue: cachedMaxHue } = colorCache;
893
- if (index < colors.length) return colors[index];
894
- let minHue = cachedMinHue;
895
- let maxHue = cachedMaxHue;
896
- for (let attempt = 0; attempt < MAX_COLOR_GENERATION_ATTEMPTS; attempt++) {
897
- let hue = (index - colors.length + attempt * VARIATION_ATTEMPT_OFFSET) * GOLDEN_RATIO * FULL_HUE_ROTATION_DEGREES % FULL_HUE_ROTATION_DEGREES;
898
- if (hues.length > 0) {
899
- let hueRange = maxHue - minHue;
900
- if (hues.length === 1) hueRange = FULL_HUE_ROTATION_DEGREES * SINGLE_COLOR_HUE_RANGE_FACTOR;
901
- else if (hueRange > HUE_WRAP_THRESHOLD_DEGREES) {
902
- const altMinHue = Math.min(...hues.filter((h) => h > HUE_WRAP_THRESHOLD_DEGREES));
903
- const altMaxHue = Math.max(...hues.filter((h) => h < HUE_WRAP_THRESHOLD_DEGREES)) + FULL_HUE_ROTATION_DEGREES;
904
- const altRange = altMaxHue - altMinHue;
905
- if (altRange < hueRange) {
906
- minHue = altMinHue;
907
- maxHue = altMaxHue;
908
- hueRange = altRange;
909
- }
1032
+ const contrastRatio = (first, second) => luminanceContrastRatio(relativeLuminance(first), relativeLuminance(second));
1033
+ /** WCAG 1.4.3 text contrast, which a label drawn on a fill needs from at least one label color. */
1034
+ const MIN_LABEL_CONTRAST = 4.5;
1035
+ /** Degrees of hue one unit of OKLCH chroma is worth when ranking candidates by hue. */
1036
+ const CHROMA_WEIGHT = 300;
1037
+ /** ΔE00 one unit of OKLCH chroma is worth when ranking candidates by distance. */
1038
+ const FAR_CHROMA_WEIGHT = 100;
1039
+ const toCandidate = (hex, oklch = hexToOklch(hex)) => ({
1040
+ hex,
1041
+ views: hexToViews(hex),
1042
+ luminance: relativeLuminance(hex),
1043
+ hue: oklch.hue,
1044
+ chroma: oklch.chroma
1045
+ });
1046
+ const rank = (candidate, distance, anchorHue) => anchorHue === null ? -(distance + candidate.chroma * FAR_CHROMA_WEIGHT) : (candidate.hue - anchorHue + 360) % 360 - candidate.chroma * CHROMA_WEIGHT;
1047
+ const LIGHTNESS_STEPS = Array.from({ length: 13 }, (_, step) => .45 + step * .025);
1048
+ const CHROMA_STEPS = [
1049
+ .12,
1050
+ .15,
1051
+ .18,
1052
+ .21
1053
+ ];
1054
+ const HUE_STEPS = Array.from({ length: 72 }, (_, step) => step * 5);
1055
+ let candidateGrid = null;
1056
+ const getCandidateGrid = () => {
1057
+ if (!candidateGrid) {
1058
+ const seen = /* @__PURE__ */ new Set();
1059
+ candidateGrid = [];
1060
+ for (const lightness of LIGHTNESS_STEPS) for (const chroma of CHROMA_STEPS) for (const hue of HUE_STEPS) {
1061
+ const hex = oklchToHex(lightness, chroma, hue);
1062
+ if (hex && !seen.has(hex)) {
1063
+ seen.add(hex);
1064
+ candidateGrid.push(toCandidate(hex, {
1065
+ hue,
1066
+ chroma
1067
+ }));
910
1068
  }
911
- const expandedRange = Math.max(hueRange * HUE_RANGE_EXPANSION_FACTOR, MIN_HUE_RANGE_DEGREES);
912
- hue = (minHue + maxHue) / 2 - expandedRange / 2 + hue / FULL_HUE_ROTATION_DEGREES * expandedRange;
913
- hue = (hue % FULL_HUE_ROTATION_DEGREES + FULL_HUE_ROTATION_DEGREES) % FULL_HUE_ROTATION_DEGREES;
914
1069
  }
915
- const saturation = BASE_SATURATION + (index + attempt) % SATURATION_VARIATION_STEPS * SATURATION_INCREMENT;
916
- const lightness = BASE_LIGHTNESS + (index + attempt) % LIGHTNESS_VARIATION_STEPS * LIGHTNESS_INCREMENT;
917
- const candidateHsl = [
918
- hue,
919
- saturation,
920
- lightness
921
- ];
922
- let isSufficientlyDifferent = true;
923
- for (const existingHsl of existingHslColors) if (getColorDistance(candidateHsl, existingHsl) < MIN_COLOR_DISTANCE) {
924
- isSufficientlyDifferent = false;
925
- break;
1070
+ }
1071
+ return candidateGrid;
1072
+ };
1073
+ const legiblePools = /* @__PURE__ */ new Map();
1074
+ const legibleCandidatesFor = (rawBackground, labelColors) => {
1075
+ const background = rawBackground.toLowerCase();
1076
+ const key = [background, ...labelColors.map((hex) => hex.toLowerCase())].join("|");
1077
+ let pool = legiblePools.get(key);
1078
+ if (!pool) {
1079
+ const backgroundLuminance = relativeLuminance(background);
1080
+ const labelLuminances = labelColors.map(relativeLuminance);
1081
+ const onBackground = getCandidateGrid().filter((candidate) => luminanceContrastRatio(candidate.luminance, backgroundLuminance) >= 3);
1082
+ const underLabels = onBackground.filter((candidate) => labelLuminances.some((labelLuminance) => luminanceContrastRatio(candidate.luminance, labelLuminance) >= MIN_LABEL_CONTRAST));
1083
+ pool = underLabels.length > 0 ? underLabels : onBackground;
1084
+ legiblePools.set(key, pool);
1085
+ }
1086
+ return pool;
1087
+ };
1088
+ /**
1089
+ * Build a tracker for a candidate pool, seeding each unused candidate's nearest distance against
1090
+ * every color already in the palette.
1091
+ *
1092
+ * @param pool - Candidates to track, in selection-priority order.
1093
+ * @param palette - Colors already placed.
1094
+ * @param usedHexes - Hex values already placed; a used candidate's distance is never read.
1095
+ * @return The tracker.
1096
+ */
1097
+ const buildTracker = (pool, palette, usedHexes) => {
1098
+ const nearest = new Float64Array(pool.length).fill(Infinity);
1099
+ for (let i = 0; i < pool.length; i++) {
1100
+ if (usedHexes.has(pool[i].hex)) continue;
1101
+ for (const color of palette) {
1102
+ const distance = viewDistance(pool[i].views, color.views);
1103
+ if (distance < nearest[i]) nearest[i] = distance;
926
1104
  }
927
- if (isSufficientlyDifferent) return (0, _visx_vendor_d3_color.hsl)(Math.round(hue), saturation / 100, lightness / 100).formatHex();
928
1105
  }
929
- const fallbackHue = (index - colors.length) * GOLDEN_RATIO * FULL_HUE_ROTATION_DEGREES % FULL_HUE_ROTATION_DEGREES;
930
- const fallbackSaturation = BASE_SATURATION + index % SATURATION_VARIATION_STEPS * SATURATION_INCREMENT;
931
- const fallbackLightness = BASE_LIGHTNESS + index % LIGHTNESS_VARIATION_STEPS * LIGHTNESS_INCREMENT;
932
- return (0, _visx_vendor_d3_color.hsl)(Math.round(fallbackHue), fallbackSaturation / 100, fallbackLightness / 100).formatHex();
1106
+ return {
1107
+ pool,
1108
+ nearest
1109
+ };
933
1110
  };
934
1111
  /**
935
- * The catalog role holding one series-palette slot.
1112
+ * Fold one newly placed color into a tracker's nearest distances.
936
1113
  *
937
- * @param slot - The one-based slot number.
938
- * @return The role name.
1114
+ * @param tracker - Tracker to update.
1115
+ * @param addedColor - Color just added to the palette.
1116
+ * @param usedHexes - Hex values already placed; a used candidate's distance is never read.
939
1117
  */
940
- const seriesRole = (slot) => `--a8c-charts-color-series-${slot}`;
941
- const SERIES_SLOT_1_FALLBACK = "#3858e9";
942
- /** The catalog pointer for every slot, in slot order. */
943
- const SERIES_PALETTE_POINTERS = Array.from({ length: 5 }, (_, index) => index === 0 ? `var(${seriesRole(1)}, ${SERIES_SLOT_1_FALLBACK})` : `var(${seriesRole(index + 1)})`);
1118
+ const updateTracker = (tracker, addedColor, usedHexes) => {
1119
+ for (let i = 0; i < tracker.pool.length; i++) {
1120
+ if (usedHexes.has(tracker.pool[i].hex)) continue;
1121
+ const distance = viewDistance(tracker.pool[i].views, addedColor.views);
1122
+ if (distance < tracker.nearest[i]) tracker.nearest[i] = distance;
1123
+ }
1124
+ };
1125
+ /**
1126
+ * Pick the next color: the best-ranked unused candidate that clears `PREFERRED_SEPARATION` from
1127
+ * every earlier color; otherwise the farthest candidate.
1128
+ *
1129
+ * @param tracker - Tracker to read.
1130
+ * @param usedHexes - Hex values already placed; a used candidate is never picked.
1131
+ * @param anchorHue - Hue to walk the wheel from, or null to rank by distance.
1132
+ * @return The picked candidate, or null if every candidate in the pool is used.
1133
+ */
1134
+ const pickNext = (tracker, usedHexes, anchorHue) => {
1135
+ let farthestIndex = -1;
1136
+ let farthestDistance = -Infinity;
1137
+ let preferredIndex = -1;
1138
+ let preferredRank = Infinity;
1139
+ for (let i = 0; i < tracker.pool.length; i++) {
1140
+ if (usedHexes.has(tracker.pool[i].hex)) continue;
1141
+ const distance = tracker.nearest[i];
1142
+ if (distance > farthestDistance) {
1143
+ farthestDistance = distance;
1144
+ farthestIndex = i;
1145
+ }
1146
+ if (distance >= 12) {
1147
+ const candidateRank = rank(tracker.pool[i], distance, anchorHue);
1148
+ if (candidateRank < preferredRank || candidateRank === preferredRank && distance > tracker.nearest[preferredIndex]) {
1149
+ preferredRank = candidateRank;
1150
+ preferredIndex = i;
1151
+ }
1152
+ }
1153
+ }
1154
+ const index = preferredIndex === -1 ? farthestIndex : preferredIndex;
1155
+ return index === -1 ? null : tracker.pool[index];
1156
+ };
1157
+ const generators = /* @__PURE__ */ new Map();
1158
+ const MAX_CACHED_GENERATORS = 32;
1159
+ /**
1160
+ * Build the series palette: the seeds, then colors that stay apart from every earlier color in
1161
+ * normal vision and under deuteranopia and protanopia.
1162
+ *
1163
+ * @param seeds - Resolved hex palette slots, in slot order.
1164
+ * @param background - Resolved hex chart background.
1165
+ * @param labelColors - Resolved hex label colors that may be drawn on a fill; each generated color reaches `MIN_LABEL_CONTRAST` with one of them.
1166
+ * @return The color at a palette index.
1167
+ */
1168
+ const createPaletteGenerator = (seeds, background, labelColors = []) => {
1169
+ const normalizedSeeds = seeds.map((hex) => hex.toLowerCase());
1170
+ const normalizedBackground = background.toLowerCase();
1171
+ const normalizedLabels = labelColors.map((hex) => hex.toLowerCase());
1172
+ const key = `${normalizedSeeds.join(",")}|${normalizedBackground}|${normalizedLabels.join(",")}`;
1173
+ let colorAt = generators.get(key);
1174
+ if (!colorAt) {
1175
+ if (generators.size >= MAX_CACHED_GENERATORS) generators.delete(generators.keys().next().value);
1176
+ colorAt = buildPaletteGenerator(normalizedSeeds, normalizedBackground, normalizedLabels);
1177
+ generators.set(key, colorAt);
1178
+ }
1179
+ return colorAt;
1180
+ };
1181
+ const buildPaletteGenerator = (normalizedSeeds, background, labelColors) => {
1182
+ const palette = normalizedSeeds.map((hex) => toCandidate(hex));
1183
+ const usedHexes = new Set(normalizedSeeds);
1184
+ const anchorHue = palette.length > 0 ? palette[0].hue : null;
1185
+ let legibleTracker = null;
1186
+ let legibleExhausted = false;
1187
+ let gridTracker = null;
1188
+ let gridExhausted = false;
1189
+ let placedBeforeExhaustion = 0;
1190
+ let repeatCount = 0;
1191
+ const nextColor = () => {
1192
+ const walkHue = (palette.length - normalizedSeeds.length) % 2 === 1 ? anchorHue : null;
1193
+ if (!legibleExhausted) {
1194
+ legibleTracker ??= buildTracker(legibleCandidatesFor(background, labelColors), palette, usedHexes);
1195
+ const picked = pickNext(legibleTracker, usedHexes, walkHue);
1196
+ if (picked) return picked;
1197
+ legibleExhausted = true;
1198
+ }
1199
+ if (!gridExhausted) {
1200
+ gridTracker ??= buildTracker(getCandidateGrid(), palette, usedHexes);
1201
+ const picked = pickNext(gridTracker, usedHexes, walkHue);
1202
+ if (picked) return picked;
1203
+ gridExhausted = true;
1204
+ placedBeforeExhaustion = palette.length;
1205
+ }
1206
+ const repeated = palette[repeatCount % placedBeforeExhaustion];
1207
+ repeatCount++;
1208
+ return repeated;
1209
+ };
1210
+ return (index) => {
1211
+ if (index < palette.length) return palette[index].hex;
1212
+ while (palette.length <= index) {
1213
+ const next = nextColor();
1214
+ usedHexes.add(next.hex);
1215
+ palette.push(next);
1216
+ if (legibleTracker && !legibleExhausted) updateTracker(legibleTracker, next, usedHexes);
1217
+ if (gridTracker && !gridExhausted) updateTracker(gridTracker, next, usedHexes);
1218
+ }
1219
+ return palette[index].hex;
1220
+ };
1221
+ };
944
1222
  //#endregion
945
1223
  //#region src/providers/chart-context/themes.ts
946
1224
  /**
@@ -990,6 +1268,13 @@ const defaultTheme = {
990
1268
  };
991
1269
  //#endregion
992
1270
  //#region src/providers/chart-context/global-charts-provider.tsx
1271
+ const resolveOpaqueHex = (pointer, element) => {
1272
+ const raw = resolveCssVariable(pointer, element);
1273
+ if (!raw || (0, _visx_vendor_d3_color.color)(raw)?.opacity !== 1) return null;
1274
+ const hex = normalizeColorToHex(pointer, element, resolveCssVariable);
1275
+ return isValidHexColor(hex) ? hex : null;
1276
+ };
1277
+ const PLACEHOLDER_LABEL_COLORS = [LABEL_FALLBACK, LABEL_INVERSE_FALLBACK];
993
1278
  const GlobalChartsContext = (0, react.createContext)(null);
994
1279
  const GlobalChartsProvider = ({ children, theme, locale, timeZone }) => {
995
1280
  const [charts, setCharts] = (0, react.useState)(() => /* @__PURE__ */ new Map());
@@ -1004,50 +1289,32 @@ const GlobalChartsProvider = ({ children, theme, locale, timeZone }) => {
1004
1289
  const providerTheme = (0, react.useMemo)(() => theme ? mergeThemes(defaultTheme, theme) : defaultTheme, [theme]);
1005
1290
  const [colorCache, setColorCache] = (0, react.useState)(() => ({
1006
1291
  colors: [],
1007
- hues: [],
1008
- existingHslColors: [],
1009
- minHue: 360,
1010
- maxHue: 0
1292
+ background: BACKGROUND_FALLBACK,
1293
+ labelColors: PLACEHOLDER_LABEL_COLORS,
1294
+ colorAt: createPaletteGenerator([SERIES_SLOT_1_FALLBACK], BACKGROUND_FALLBACK, PLACEHOLDER_LABEL_COLORS)
1011
1295
  }));
1012
1296
  const [isColorPaletteResolved, setIsColorPaletteResolved] = (0, react.useState)(false);
1013
1297
  (0, react.useLayoutEffect)(() => {
1014
1298
  setIsColorPaletteResolved(false);
1015
1299
  const resolvedColors = [];
1016
- const hues = [];
1017
- const existingHslColors = [];
1018
- let minHue = 360;
1019
- let maxHue = 0;
1020
1300
  for (const color of SERIES_PALETTE_POINTERS) {
1021
1301
  const normalizedColor = normalizeColorToHex(color, wrapperRef.current, resolveCssVariable);
1022
- if (normalizedColor.startsWith("#")) {
1023
- resolvedColors.push(normalizedColor);
1024
- const hslColor = (0, _visx_vendor_d3_color.hsl)(normalizedColor);
1025
- if (!isNaN(hslColor.h)) {
1026
- const hslTuple = [
1027
- hslColor.h,
1028
- hslColor.s * 100,
1029
- hslColor.l * 100
1030
- ];
1031
- hues.push(hslTuple[0]);
1032
- existingHslColors.push(hslTuple);
1033
- minHue = Math.min(minHue, hslTuple[0]);
1034
- maxHue = Math.max(maxHue, hslTuple[0]);
1035
- }
1036
- }
1302
+ if (isValidHexColor(normalizedColor)) resolvedColors.push(normalizedColor);
1037
1303
  }
1304
+ const backgroundHex = resolveOpaqueHex(CATALOG_POINTERS.background, wrapperRef.current) ?? "#ffffff";
1305
+ const labelColors = [CATALOG_POINTERS.label, CATALOG_POINTERS.labelInverse].map((pointer) => resolveOpaqueHex(pointer, wrapperRef.current)).filter((hex) => hex !== null);
1038
1306
  setColorCache({
1039
1307
  colors: resolvedColors,
1040
- hues,
1041
- existingHslColors,
1042
- minHue,
1043
- maxHue
1308
+ background: backgroundHex,
1309
+ labelColors,
1310
+ colorAt: createPaletteGenerator(resolvedColors, backgroundHex, labelColors)
1044
1311
  });
1045
1312
  }, []);
1046
1313
  (0, react.useEffect)(() => {
1047
1314
  if (colorCache.colors.length > 0) setIsColorPaletteResolved(true);
1048
1315
  }, [colorCache]);
1049
1316
  const [groupToColorMap, setGroupToColorMap] = (0, react.useState)(() => /* @__PURE__ */ new Map());
1050
- const paletteKey = colorCache.colors.join(",");
1317
+ const paletteKey = `${colorCache.colors.join(",")}|${colorCache.background}|${colorCache.labelColors.join(",")}`;
1051
1318
  (0, react.useEffect)(() => {
1052
1319
  setGroupToColorMap(/* @__PURE__ */ new Map());
1053
1320
  }, [paletteKey]);
@@ -1070,11 +1337,11 @@ const GlobalChartsProvider = ({ children, theme, locale, timeZone }) => {
1070
1337
  const existing = groupToColorMap.get(group);
1071
1338
  if (existing) return existing;
1072
1339
  const assignedCount = groupToColorMap.size;
1073
- const color = getChartColor(assignedCount, colorCache);
1340
+ const color = colorCache.colorAt(assignedCount);
1074
1341
  groupToColorMap.set(group, color);
1075
1342
  return color;
1076
1343
  }
1077
- return getChartColor(index, colorCache);
1344
+ return colorCache.colorAt(index);
1078
1345
  }, [colorCache, groupToColorMap]);
1079
1346
  const getElementStyles = (0, react.useCallback)(({ data, index, overrideColor, legendShape }) => {
1080
1347
  const isSeriesData = data && typeof data === "object" && "data" in data;
@@ -1240,39 +1507,6 @@ const useDeepMemo = (value) => {
1240
1507
  return ref.current;
1241
1508
  };
1242
1509
  //#endregion
1243
- //#region src/providers/chart-context/private/catalog-pointers.ts
1244
- const CATALOG_POINTERS = {
1245
- background: "var(--a8c-charts-color-background, #fff)",
1246
- labelAxis: "var(--a8c-charts-color-label-axis, #1e1e1e)",
1247
- grid: "var(--a8c-charts-color-grid, #dbdbdb)",
1248
- axisX: "var(--a8c-charts-color-axis-x, #dbdbdb)",
1249
- tickX: "var(--a8c-charts-color-tick-x, #dbdbdb)",
1250
- axisY: "var(--a8c-charts-color-axis-y, none)",
1251
- tickY: "var(--a8c-charts-color-tick-y, none)",
1252
- annotation: "var(--a8c-charts-color-annotation, #1e1e1e)",
1253
- surface: "var(--a8c-charts-color-surface, #fff)",
1254
- surfaceSecondary: "var(--a8c-charts-color-surface-secondary, #f4f4f4)",
1255
- trendUp: "var(--a8c-charts-color-trend-up, #008030)",
1256
- trendDown: "var(--a8c-charts-color-trend-down, #cc1818)",
1257
- series: SERIES_PALETTE_POINTERS
1258
- };
1259
- /**
1260
- * The annotation parts visx paints, in the shape `@visx/annotation` takes. `radius`
1261
- * rides along as the base the theme and the per-datum styles merge onto.
1262
- */
1263
- const ANNOTATION_POINTERS = {
1264
- label: {
1265
- anchorLineStroke: CATALOG_POINTERS.annotation,
1266
- backgroundFill: CATALOG_POINTERS.surface
1267
- },
1268
- connector: { stroke: CATALOG_POINTERS.annotation },
1269
- circleSubject: {
1270
- stroke: "transparent",
1271
- fill: CATALOG_POINTERS.annotation,
1272
- radius: 5
1273
- }
1274
- };
1275
- //#endregion
1276
1510
  //#region src/providers/chart-scope/use-chart-scope-element.ts
1277
1511
  /**
1278
1512
  * Returns the nearest chart scope element, or `null` when rendered outside any scope (and during SSR, before the ref is attached).
@@ -2750,18 +2984,41 @@ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating,
2750
2984
  getChartRoot,
2751
2985
  focusWithoutScrollIfNeeded
2752
2986
  ]);
2987
+ const onChartFocus = (0, react.useCallback)((event) => {
2988
+ if (event.currentTarget.contains(event.relatedTarget)) return;
2989
+ if (!isNavigating && selectedIndex !== void 0) setSelectedIndex(0);
2990
+ }, [
2991
+ isNavigating,
2992
+ selectedIndex,
2993
+ setSelectedIndex
2994
+ ]);
2995
+ const pointerIndex = (0, react.useRef)(void 0);
2996
+ const onChartPointerMove = (0, react.useCallback)((index) => {
2997
+ if (selectedIndex === void 0) {
2998
+ if (pointerIndex.current !== void 0) pointerIndex.current = index;
2999
+ return;
3000
+ }
3001
+ const root = getChartRoot();
3002
+ const activeElement = root?.ownerDocument.activeElement;
3003
+ if (activeElement && root.contains(activeElement)) {
3004
+ pointerIndex.current = index;
3005
+ focusWithoutScrollIfNeeded(root);
3006
+ }
3007
+ setSelectedIndex(void 0);
3008
+ setIsNavigating(false);
3009
+ }, [
3010
+ selectedIndex,
3011
+ setSelectedIndex,
3012
+ setIsNavigating,
3013
+ getChartRoot,
3014
+ focusWithoutScrollIfNeeded
3015
+ ]);
2753
3016
  return {
2754
3017
  tooltipRef,
2755
- onChartFocus: (0, react.useCallback)((event) => {
2756
- if (event.currentTarget.contains(event.relatedTarget)) return;
2757
- if (!isNavigating && selectedIndex !== void 0) setSelectedIndex(0);
2758
- }, [
2759
- isNavigating,
2760
- selectedIndex,
2761
- setSelectedIndex
2762
- ]),
2763
- onChartBlur: (0, react.useCallback)(() => {
3018
+ onChartFocus,
3019
+ onChartBlur: (0, react.useCallback)((event) => {
2764
3020
  setIsNavigating(false);
3021
+ if (!event.currentTarget.contains(event.relatedTarget)) pointerIndex.current = void 0;
2765
3022
  }, [setIsNavigating]),
2766
3023
  onChartKeyDown: (0, react.useCallback)((event) => {
2767
3024
  if (event.key === "Tab" || event.key === "Escape") {
@@ -2769,10 +3026,12 @@ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating,
2769
3026
  focusWithoutScrollIfNeeded(getChartRoot());
2770
3027
  setSelectedIndex(void 0);
2771
3028
  setIsNavigating(false);
3029
+ pointerIndex.current = void 0;
2772
3030
  return;
2773
3031
  }
2774
3032
  if (totalPoints === 0) return;
2775
- const currentSelectedIndex = selectedIndex === void 0 ? -1 : selectedIndex;
3033
+ const currentSelectedIndex = selectedIndex ?? pointerIndex.current ?? -1;
3034
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft") pointerIndex.current = void 0;
2776
3035
  if (event.key === "ArrowRight") {
2777
3036
  event.preventDefault();
2778
3037
  setIsNavigating(true);
@@ -2793,7 +3052,8 @@ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating,
2793
3052
  getChartRoot,
2794
3053
  focusWithoutScrollIfNeeded,
2795
3054
  onActivate
2796
- ])
3055
+ ]),
3056
+ onChartPointerMove
2797
3057
  };
2798
3058
  };
2799
3059
  //#endregion
@@ -2932,7 +3192,7 @@ function useChartChildren(children, chartType) {
2932
3192
  var chart_layout_module_default = { "chart-layout__content": "a8ccharts-fpNVAq-chart-layout__content" };
2933
3193
  //#endregion
2934
3194
  //#region src/charts/private/chart-layout/chart-layout.tsx
2935
- const ChartLayout = ({ legendPosition, legendElement, legendChildren, children, trailingContent, onContentHeightChange, gap, className, style, "data-testid": dataTestId, "data-chart-id": dataChartId }) => {
3195
+ const ChartLayout = ({ legendPosition, legendElement, legendChildren, children, trailingContent, onContentHeightChange, gap, className, rootRef, style, "data-testid": dataTestId, "data-chart-id": dataChartId }) => {
2936
3196
  const [contentRef, contentWidth, contentHeight] = useElementSize();
2937
3197
  const isRenderProp = typeof children === "function";
2938
3198
  const isMeasured = contentHeight > 0;
@@ -2951,6 +3211,7 @@ const ChartLayout = ({ legendPosition, legendElement, legendChildren, children,
2951
3211
  isMeasured
2952
3212
  }) : children;
2953
3213
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_wordpress_ui.Stack, {
3214
+ ref: rootRef,
2954
3215
  direction: "column",
2955
3216
  gap,
2956
3217
  className,
@@ -5661,9 +5922,8 @@ const createGroupScale = (keys, bandwidth, padding) => (0, _visx_scale.scaleBand
5661
5922
  //#endregion
5662
5923
  //#region src/charts/bar-chart/private/comparison-bars-geometry.ts
5663
5924
  /**
5664
- * Output position of a value scale's baseline: zero if in-domain, else the
5665
- * nearest range edge. Mirrors visx's getScaleBaseline so comparison shadows
5666
- * sit on the same baseline as primary bars.
5925
+ * Mirrors visx's getScaleBaseline so comparison shadows and primary bars
5926
+ * share the same baseline, including out-of-domain zero values.
5667
5927
  *
5668
5928
  * @param {ValueScale} scale - The continuous value scale.
5669
5929
  * @return {number} The baseline output position in pixels.
@@ -5674,7 +5934,7 @@ function getValueScaleBaseline(scale) {
5674
5934
  const maybeZero = scale(0);
5675
5935
  const [minOutput, maxOutput] = isDescending ? [b, a] : [a, b];
5676
5936
  if (isDescending) return Number.isFinite(maybeZero) ? Math.min(Math.max(minOutput, maybeZero), maxOutput) : maxOutput;
5677
- return Number.isFinite(maybeZero) ? Math.min(Math.max(maybeZero, minOutput), maxOutput) : minOutput;
5937
+ return Number.isFinite(maybeZero) ? Math.max(maybeZero, minOutput) : minOutput;
5678
5938
  }
5679
5939
  /**
5680
5940
  * Compute the rect for a comparison "shadow" bar, centered on the paired
@@ -5710,6 +5970,51 @@ function computeComparisonRect(params) {
5710
5970
  height: valueLength
5711
5971
  };
5712
5972
  }
5973
+ //#endregion
5974
+ //#region src/charts/bar-chart/private/classified-bar-series.tsx
5975
+ const ClassifiedBarSeries = ({ barClassName, primaryKeys, groupPadding, ...props }) => {
5976
+ const { xScale, yScale, horizontal } = (0, react.useContext)(_visx_xychart.DataContext);
5977
+ const bandScale = horizontal ? yScale : xScale;
5978
+ const valueScale = horizontal ? xScale : yScale;
5979
+ const groupScale = (0, react.useMemo)(() => createGroupScale(primaryKeys, bandScale?.bandwidth?.() ?? 0, groupPadding), [
5980
+ primaryKeys,
5981
+ bandScale,
5982
+ groupPadding
5983
+ ]);
5984
+ const renderGlyph = (0, react.useCallback)(({ datum, x, y, color, onBlur, onFocus, onPointerMove, onPointerOut, onPointerUp }) => {
5985
+ if (!bandScale?.bandwidth || !valueScale) return null;
5986
+ const baseline = getValueScaleBaseline(valueScale);
5987
+ const position = Number(bandScale((horizontal ? props.yAccessor : props.xAccessor)(datum))) + (groupScale(props.dataKey) ?? 0);
5988
+ const value = horizontal ? x : y;
5989
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
5990
+ className: (0, clsx.default)("visx-bar", barClassName(datum)),
5991
+ tabIndex: onFocus || onBlur ? 0 : void 0,
5992
+ onBlur,
5993
+ onFocus,
5994
+ onPointerMove,
5995
+ onPointerOut,
5996
+ onPointerUp,
5997
+ x: horizontal ? baseline + Math.min(0, value - baseline) : position,
5998
+ y: horizontal ? position : baseline + Math.min(0, value - baseline),
5999
+ width: horizontal ? Math.abs(value - baseline) : groupScale.bandwidth(),
6000
+ height: horizontal ? groupScale.bandwidth() : Math.abs(value - baseline),
6001
+ fill: color
6002
+ });
6003
+ }, [
6004
+ bandScale,
6005
+ valueScale,
6006
+ horizontal,
6007
+ groupScale,
6008
+ props.dataKey,
6009
+ props.xAccessor,
6010
+ props.yAccessor,
6011
+ barClassName
6012
+ ]);
6013
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.GlyphSeries, {
6014
+ ...props,
6015
+ renderGlyph
6016
+ });
6017
+ };
5713
6018
  /**
5714
6019
  * Upper clamp on the computed group padding, so bars can never collapse to zero width
5715
6020
  * even at very large `widthFactor` values.
@@ -5874,9 +6179,10 @@ function nearestBandIndex(data, accessor, scale, position) {
5874
6179
  * @param root0.withTooltips - Whether pointer events update the tooltip.
5875
6180
  * @param root0.onPointerDown - Receives the corrected pointer-down datum.
5876
6181
  * @param root0.onPointerUp - Receives the corrected pointer-up datum.
6182
+ * @param root0.onPointerMove - Receives the corrected datum under a moving pointer.
5877
6183
  * @return No visual content.
5878
6184
  */
5879
- function BandTooltip({ keys, groupPadding, withTooltips, onPointerDown, onPointerUp }) {
6185
+ function BandTooltip({ keys, groupPadding, withTooltips, onPointerDown, onPointerUp, onPointerMove }) {
5880
6186
  const { xScale, yScale, dataRegistry, horizontal } = (0, react.useContext)(_visx_xychart.DataContext);
5881
6187
  const { showTooltip } = (0, react.useContext)(_visx_xychart.TooltipContext);
5882
6188
  const scale = horizontal ? yScale : xScale;
@@ -5921,20 +6227,22 @@ function BandTooltip({ keys, groupPadding, withTooltips, onPointerDown, onPointe
5921
6227
  ]);
5922
6228
  const handlePointer = (0, react.useCallback)((params) => {
5923
6229
  const selections = getSelections(params);
5924
- if (withTooltips && params?.event.type !== "pointerup") selections.forEach(showTooltip);
5925
- const callback = params?.event.type === "pointerdown" ? onPointerDown : onPointerUp;
5926
- if (params?.event.type === "pointermove" || !callback) return;
5927
6230
  const nearest = selections.reduce((best, selection) => {
5928
6231
  const distance = (value) => Math.hypot(value.distanceX, value.distanceY);
5929
6232
  return !best || distance(selection) <= distance(best) ? selection : best;
5930
6233
  }, void 0);
6234
+ if (params?.event.type === "pointermove" && nearest) onPointerMove?.(nearest);
6235
+ if (withTooltips && params?.event.type !== "pointerup") selections.forEach(showTooltip);
6236
+ const callback = params?.event.type === "pointerdown" ? onPointerDown : onPointerUp;
6237
+ if (params?.event.type === "pointermove" || !callback) return;
5931
6238
  if (nearest) callback(nearest);
5932
6239
  }, [
5933
6240
  getSelections,
5934
6241
  withTooltips,
5935
6242
  showTooltip,
5936
6243
  onPointerDown,
5937
- onPointerUp
6244
+ onPointerUp,
6245
+ onPointerMove
5938
6246
  ]);
5939
6247
  (0, _visx_xychart.useEventEmitter)("pointermove", withTooltips ? handlePointer : void 0);
5940
6248
  (0, _visx_xychart.useEventEmitter)("pointerdown", handlePointer);
@@ -5953,7 +6261,7 @@ const renderTooltipRow = (label, value) => /* @__PURE__ */ (0, react_jsx_runtime
5953
6261
  className: bar_chart_module_default["bar-chart__tooltip-row"],
5954
6262
  children: (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n.__)("%1$s: %2$s", "jetpack-charts"), label, value)
5955
6263
  });
5956
- const BarChartInternal = ({ data, chartId: providedChartId, width, height, className, margin, withTooltips = false, showLegend = false, legend = {}, gridVisibility: gridVisibilityProp, renderTooltip, tooltipPlacement, tooltipAnchorTop, options = {}, orientation = "vertical", withPatterns = false, showZeroValues = false, withBandHighlight = false, onBandHighlightChange, defaultHiddenSeries, animation, children, gap = "md", onPointerDown, onPointerUp, onDatumActivate }) => {
6264
+ const BarChartInternal = ({ data, chartId: providedChartId, width, height, className, margin, withTooltips = false, showLegend = false, legend = {}, gridVisibility: gridVisibilityProp, renderTooltip, tooltipPlacement, tooltipAnchorTop, tooltipStyle, barClassName, options = {}, orientation = "vertical", withPatterns = false, showZeroValues = false, withBandHighlight = false, onBandHighlightChange, defaultHiddenSeries, animation, children, gap = "md", onPointerDown, onPointerUp, onDatumActivate }) => {
5957
6265
  if (!withTooltips && (withBandHighlight || onBandHighlightChange)) warnOnce("bar-chart-band-highlight-without-tooltips", "BarChart: withBandHighlight and onBandHighlightChange require withTooltips.");
5958
6266
  const legendInteractive = legend.interactive ?? false;
5959
6267
  const legendCollapseGroups = legend.collapseGroups ?? false;
@@ -6017,7 +6325,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
6017
6325
  });
6018
6326
  }, [primaryEntries, onDatumActivate]);
6019
6327
  const visibleSeriesKey = (0, react.useMemo)(() => JSON.stringify(primaryKeys), [primaryKeys]);
6020
- const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation({
6328
+ const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown, onChartPointerMove } = useKeyboardNavigation({
6021
6329
  selectedIndex,
6022
6330
  setSelectedIndex,
6023
6331
  isNavigating,
@@ -6027,6 +6335,10 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
6027
6335
  onActivate: activateSelectedBar,
6028
6336
  visibleSeriesKey
6029
6337
  });
6338
+ const handlePointerMove = (0, react.useCallback)(({ key, index }) => {
6339
+ const seriesIndex = primaryKeys.indexOf(key);
6340
+ if (seriesIndex >= 0) onChartPointerMove(index * primaryKeys.length + seriesIndex);
6341
+ }, [primaryKeys, onChartPointerMove]);
6030
6342
  const comparisonEntries = (0, react.useMemo)(() => {
6031
6343
  const primaryByGroup = new Map(primaryEntries.map(({ series, index }) => [series.group, {
6032
6344
  label: series.label,
@@ -6323,7 +6635,19 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
6323
6635
  getElementStyles,
6324
6636
  resolveFill: resolveComparisonFill
6325
6637
  }),
6326
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.BarGroup, {
6638
+ barClassName ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", {
6639
+ className: "visx-bar-group",
6640
+ children: primaryEntries.map(({ series: seriesData, index }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClassifiedBarSeries, {
6641
+ dataKey: seriesData.label,
6642
+ data: seriesData.data,
6643
+ xAccessor: chartOptions.accessors.xAccessor,
6644
+ yAccessor: chartOptions.accessors.yAccessor,
6645
+ colorAccessor: getBarBackground(index),
6646
+ barClassName,
6647
+ primaryKeys,
6648
+ groupPadding
6649
+ }, seriesData.label))
6650
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.BarGroup, {
6327
6651
  padding: groupPadding,
6328
6652
  children: primaryEntries.map(({ series: seriesData, index }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.BarSeries, {
6329
6653
  dataKey: seriesData?.label,
@@ -6338,7 +6662,8 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
6338
6662
  groupPadding,
6339
6663
  withTooltips,
6340
6664
  onPointerDown,
6341
- onPointerUp
6665
+ onPointerUp,
6666
+ onPointerMove: handlePointerMove
6342
6667
  }),
6343
6668
  !allSeriesHidden && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WholeNumberTicks, {
6344
6669
  ...wholeNumberTicksProps,
@@ -6353,6 +6678,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
6353
6678
  withTooltips && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessibleTooltip, {
6354
6679
  tooltipPlacement,
6355
6680
  tooltipAnchorTop,
6681
+ style: tooltipStyle,
6356
6682
  detectBounds: true,
6357
6683
  snapTooltipToDatumX: true,
6358
6684
  snapTooltipToDatumY: true,
@@ -7019,12 +7345,13 @@ function normalizeGeoChartError(eventArgs) {
7019
7345
  * @param props.height - Height of the chart in pixels
7020
7346
  * @param props.region - Region to display ('world', 'US', or ISO 3166-1 alpha-2 code)
7021
7347
  * @param props.resolution - Resolution level ('countries', 'provinces', or 'metros')
7348
+ * @param props.domain - Country code whose viewpoint sets the disputed borders
7022
7349
  * @param props.onError - Optional callback for Google Charts errors
7023
7350
  * @param props.className - Additional CSS class name for the chart container
7024
7351
  * @param props.renderPlaceholder - Optional render function for the loading placeholder
7025
7352
  * @return A React component displaying an interactive map with data visualization
7026
7353
  */
7027
- const GeoChartInternal = ({ className, data, width, height, region = "world", resolution = "countries", onError, renderPlaceholder }) => {
7354
+ const GeoChartInternal = ({ className, data, width, height, region = "world", resolution = "countries", domain, onError, renderPlaceholder }) => {
7028
7355
  const { getElementStyles } = useGlobalChartsContext();
7029
7356
  const scopeElement = useChartScopeElement();
7030
7357
  const containerRef = (0, react.useRef)(null);
@@ -7088,6 +7415,7 @@ const GeoChartInternal = ({ className, data, width, height, region = "world", re
7088
7415
  const options = (0, react.useMemo)(() => ({
7089
7416
  ...region !== "world" && { region },
7090
7417
  ...resolution !== "countries" && { resolution },
7418
+ ...domain && { domain },
7091
7419
  colorAxis: { colors: [lightColorHex, fullColorHex] },
7092
7420
  backgroundColor: backgroundColorHex,
7093
7421
  datalessRegionColor: defaultFillColorHex,
@@ -7101,6 +7429,7 @@ const GeoChartInternal = ({ className, data, width, height, region = "world", re
7101
7429
  }), [
7102
7430
  region,
7103
7431
  resolution,
7432
+ domain,
7104
7433
  lightColorHex,
7105
7434
  fullColorHex,
7106
7435
  backgroundColorHex,
@@ -11394,6 +11723,7 @@ function RadialWipeAnimation({ id, radius, innerRadius = 0, durationMs = 1e3, wi
11394
11723
  var pie_chart_module_default = {
11395
11724
  "pie-chart__label-plate": "a8ccharts-gnszbG-pie-chart__label-plate",
11396
11725
  "pie-chart__label-text": "a8ccharts-gnszbG-pie-chart__label-text",
11726
+ "pie-chart__label-text--on-light": "a8ccharts-gnszbG-pie-chart__label-text--on-light",
11397
11727
  "pie-chart__plot": "a8ccharts-gnszbG-pie-chart__plot",
11398
11728
  "pie-chart--responsive": "a8ccharts-gnszbG-pie-chart--responsive"
11399
11729
  };
@@ -11438,6 +11768,25 @@ const validateData$1 = (data) => {
11438
11768
  };
11439
11769
  };
11440
11770
  /**
11771
+ * Whether a pie slice's label should use the dark `label` role instead of the default `label-inverse` role.
11772
+ *
11773
+ * A label plate always wins: the text sits on the plate, not the slice fill, so it never flips.
11774
+ * Otherwise the role that contrasts more with the resolved slice fill wins. `null` pointers (not
11775
+ * yet resolved, or labels off) keep the default inverse role.
11776
+ *
11777
+ * @param fill - The slice's resolved fill color.
11778
+ * @param pointers - The label pointers, resolved at the chart's own root element.
11779
+ * @return Whether the label needs dark text.
11780
+ */
11781
+ const labelNeedsDarkText = (fill, pointers) => {
11782
+ if (!pointers || pointers.hasPlate) return false;
11783
+ const fillHex = normalizeColorToHex(fill);
11784
+ if (!isValidHexColor(fillHex) || !isValidHexColor(pointers.labelHex)) return false;
11785
+ if (pointers.isInverseSeeThrough) return true;
11786
+ if (!isValidHexColor(pointers.labelInverseHex)) return false;
11787
+ return contrastRatio(fillHex, pointers.labelHex) > contrastRatio(fillHex, pointers.labelInverseHex);
11788
+ };
11789
+ /**
11441
11790
  * Renders a pie or donut chart using the provided data.
11442
11791
  *
11443
11792
  * @param {PieChartProps} props - Component props
@@ -11451,11 +11800,36 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
11451
11800
  const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } = (0, _visx_tooltip.useTooltip)();
11452
11801
  const standaloneScopeClass = useStandaloneScopeClass();
11453
11802
  const containerRef = (0, react.useRef)(null);
11803
+ const rootRef = (0, react.useRef)(null);
11804
+ const [labelPointers, setLabelPointers] = (0, react.useState)(null);
11454
11805
  const onMouseLeave = (0, react.useCallback)(() => {
11455
11806
  if (!withTooltips) return;
11456
11807
  hideTooltip();
11457
11808
  }, [withTooltips, hideTooltip]);
11458
- const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
11809
+ const { getElementStyles, isSeriesVisible, isColorPaletteResolved } = useGlobalChartsContext();
11810
+ const { isValid, message } = validateData$1(data);
11811
+ (0, react.useLayoutEffect)(() => {
11812
+ if (!showLabels || !rootRef.current) return;
11813
+ const resolve = createCssVariableResolver(rootRef.current);
11814
+ const rawLabelBackground = resolve(CATALOG_POINTERS.labelBackground);
11815
+ const rawLabel = resolve(CATALOG_POINTERS.label);
11816
+ const isLabelOpaque = rawLabel ? (0, _visx_vendor_d3_color.color)(rawLabel)?.opacity === 1 : false;
11817
+ const rawLabelInverse = resolve(CATALOG_POINTERS.labelInverse);
11818
+ const labelInverseColor = rawLabelInverse ? (0, _visx_vendor_d3_color.color)(rawLabelInverse) : null;
11819
+ const plateColor = rawLabelBackground ? (0, _visx_vendor_d3_color.color)(rawLabelBackground) : null;
11820
+ const next = {
11821
+ hasPlate: rawLabelBackground ? !plateColor || plateColor.opacity > 0 : false,
11822
+ labelHex: isLabelOpaque ? normalizeColorToHex(CATALOG_POINTERS.label, null, resolve) : "",
11823
+ labelInverseHex: normalizeColorToHex(CATALOG_POINTERS.labelInverse, null, resolve),
11824
+ isInverseSeeThrough: labelInverseColor ? labelInverseColor.opacity < 1 : false
11825
+ };
11826
+ setLabelPointers((previous) => previous && previous.hasPlate === next.hasPlate && previous.labelHex === next.labelHex && previous.labelInverseHex === next.labelInverseHex && previous.isInverseSeeThrough === next.isInverseSeeThrough ? previous : next);
11827
+ }, [
11828
+ showLabels,
11829
+ className,
11830
+ isColorPaletteResolved,
11831
+ isValid
11832
+ ]);
11459
11833
  const dataWithPercentages = useDataWithPercentages(data);
11460
11834
  const { visibleData, allSegmentsHidden, legendData } = useLegendVisibilityData({
11461
11835
  data: dataWithPercentages,
@@ -11466,7 +11840,6 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
11466
11840
  showValues: true,
11467
11841
  legendValueDisplay
11468
11842
  }), [legendValueDisplay]));
11469
- const { isValid, message } = validateData$1(data);
11470
11843
  const { svgChildren, htmlChildren, legendChildren, otherChildren } = useChartChildren(children, "PieChart");
11471
11844
  const chartMetadata = (0, react.useMemo)(() => ({
11472
11845
  thickness,
@@ -11528,6 +11901,7 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
11528
11901
  legendElement,
11529
11902
  legendChildren,
11530
11903
  gap,
11904
+ rootRef,
11531
11905
  className: (0, clsx.default)("pie-chart", pie_chart_module_default["pie-chart"], { [pie_chart_module_default["pie-chart--responsive"]]: !propWidth && !propHeight }, className),
11532
11906
  style: {
11533
11907
  width: propWidth || void 0,
@@ -11589,9 +11963,10 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
11589
11963
  tooltipTop: event.clientY - bounds.top + tooltipOffsetY
11590
11964
  });
11591
11965
  };
11966
+ const fill = accessors.fill(arc.data);
11592
11967
  const pathProps = {
11593
11968
  d: pie.path(arc) || "",
11594
- fill: accessors.fill(arc.data),
11969
+ fill,
11595
11970
  "data-testid": "pie-segment"
11596
11971
  };
11597
11972
  const groupProps = {};
@@ -11619,7 +11994,7 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
11619
11994
  ry: 4,
11620
11995
  pointerEvents: "none"
11621
11996
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
11622
- className: pie_chart_module_default["pie-chart__label-text"],
11997
+ className: (0, clsx.default)(pie_chart_module_default["pie-chart__label-text"], { [pie_chart_module_default["pie-chart__label-text--on-light"]]: labelNeedsDarkText(fill, labelPointers) }),
11623
11998
  x: centroidX,
11624
11999
  y: centroidY,
11625
12000
  dy: ".33em",