@automattic/charts 4.4.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.
- package/CHANGELOG.md +12 -0
- package/TOKENS.md +2 -2
- package/dist/index.cjs +521 -203
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +4 -0
- package/dist/index.d.cts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +521 -203
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/charts/bar-chart/bar-chart.tsx +22 -10
- package/src/charts/bar-chart/private/band-tooltip.tsx +18 -12
- package/src/charts/geo-chart/geo-chart.tsx +4 -0
- package/src/charts/geo-chart/test/geo-chart.test.tsx +8 -0
- package/src/charts/geo-chart/types.ts +5 -0
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +1 -1
- package/src/charts/pie-chart/pie-chart.module.scss +4 -0
- package/src/charts/pie-chart/pie-chart.tsx +103 -7
- package/src/charts/pie-chart/test/pie-chart.test.tsx +149 -0
- package/src/charts/private/chart-layout/chart-layout.tsx +5 -1
- package/src/components/tooltip/accessible-tooltip.tsx +40 -4
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +82 -5
- package/src/providers/chart-context/global-charts-provider.tsx +57 -41
- package/src/providers/chart-context/private/catalog-pointers.ts +9 -1
- package/src/providers/chart-context/private/palette-generator.ts +305 -0
- package/src/providers/chart-context/private/perceptual-color.ts +219 -0
- package/src/providers/chart-context/private/series-palette.ts +1 -1
- package/src/providers/chart-context/test/chart-context.test.tsx +88 -8
- package/src/providers/chart-context/test/global-charts-provider-first-render.test.tsx +27 -0
- package/src/providers/chart-context/test/palette-generator.test.ts +181 -0
- package/src/providers/chart-context/test/perceptual-color.test.ts +75 -0
- package/src/test-utils/zoned-parsing-suite.ts +1 -1
- package/src/utils/color-utils.ts +5 -3
- package/src/utils/test/date-formatting.test.ts +1 -1
- 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
|
-
*
|
|
442
|
-
*
|
|
443
|
-
* @
|
|
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
807
|
/**
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
*
|
|
851
|
-
*
|
|
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
|
|
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
|
-
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
*
|
|
863
|
-
*
|
|
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
|
|
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
|
-
*
|
|
868
|
-
*
|
|
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
|
|
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
|
-
*
|
|
873
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
1023
|
+
const luminanceContrastRatio = (first, second) => (Math.max(first, second) + .05) / (Math.min(first, second) + .05);
|
|
884
1024
|
/**
|
|
885
|
-
*
|
|
1025
|
+
* WCAG contrast ratio between two hex colors.
|
|
886
1026
|
*
|
|
887
|
-
* @param
|
|
888
|
-
* @param
|
|
889
|
-
* @return
|
|
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
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1106
|
+
return {
|
|
1107
|
+
pool,
|
|
1108
|
+
nearest
|
|
1109
|
+
};
|
|
933
1110
|
};
|
|
934
1111
|
/**
|
|
935
|
-
*
|
|
1112
|
+
* Fold one newly placed color into a tracker's nearest distances.
|
|
936
1113
|
*
|
|
937
|
-
* @param
|
|
938
|
-
* @
|
|
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
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
const
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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.
|
|
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
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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 =
|
|
1340
|
+
const color = colorCache.colorAt(assignedCount);
|
|
1074
1341
|
groupToColorMap.set(group, color);
|
|
1075
1342
|
return color;
|
|
1076
1343
|
}
|
|
1077
|
-
return
|
|
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
|
|
2756
|
-
|
|
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
|
|
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,
|
|
@@ -5918,9 +6179,10 @@ function nearestBandIndex(data, accessor, scale, position) {
|
|
|
5918
6179
|
* @param root0.withTooltips - Whether pointer events update the tooltip.
|
|
5919
6180
|
* @param root0.onPointerDown - Receives the corrected pointer-down datum.
|
|
5920
6181
|
* @param root0.onPointerUp - Receives the corrected pointer-up datum.
|
|
6182
|
+
* @param root0.onPointerMove - Receives the corrected datum under a moving pointer.
|
|
5921
6183
|
* @return No visual content.
|
|
5922
6184
|
*/
|
|
5923
|
-
function BandTooltip({ keys, groupPadding, withTooltips, onPointerDown, onPointerUp }) {
|
|
6185
|
+
function BandTooltip({ keys, groupPadding, withTooltips, onPointerDown, onPointerUp, onPointerMove }) {
|
|
5924
6186
|
const { xScale, yScale, dataRegistry, horizontal } = (0, react.useContext)(_visx_xychart.DataContext);
|
|
5925
6187
|
const { showTooltip } = (0, react.useContext)(_visx_xychart.TooltipContext);
|
|
5926
6188
|
const scale = horizontal ? yScale : xScale;
|
|
@@ -5965,20 +6227,22 @@ function BandTooltip({ keys, groupPadding, withTooltips, onPointerDown, onPointe
|
|
|
5965
6227
|
]);
|
|
5966
6228
|
const handlePointer = (0, react.useCallback)((params) => {
|
|
5967
6229
|
const selections = getSelections(params);
|
|
5968
|
-
if (withTooltips && params?.event.type !== "pointerup") selections.forEach(showTooltip);
|
|
5969
|
-
const callback = params?.event.type === "pointerdown" ? onPointerDown : onPointerUp;
|
|
5970
|
-
if (params?.event.type === "pointermove" || !callback) return;
|
|
5971
6230
|
const nearest = selections.reduce((best, selection) => {
|
|
5972
6231
|
const distance = (value) => Math.hypot(value.distanceX, value.distanceY);
|
|
5973
6232
|
return !best || distance(selection) <= distance(best) ? selection : best;
|
|
5974
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;
|
|
5975
6238
|
if (nearest) callback(nearest);
|
|
5976
6239
|
}, [
|
|
5977
6240
|
getSelections,
|
|
5978
6241
|
withTooltips,
|
|
5979
6242
|
showTooltip,
|
|
5980
6243
|
onPointerDown,
|
|
5981
|
-
onPointerUp
|
|
6244
|
+
onPointerUp,
|
|
6245
|
+
onPointerMove
|
|
5982
6246
|
]);
|
|
5983
6247
|
(0, _visx_xychart.useEventEmitter)("pointermove", withTooltips ? handlePointer : void 0);
|
|
5984
6248
|
(0, _visx_xychart.useEventEmitter)("pointerdown", handlePointer);
|
|
@@ -6061,7 +6325,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
|
|
|
6061
6325
|
});
|
|
6062
6326
|
}, [primaryEntries, onDatumActivate]);
|
|
6063
6327
|
const visibleSeriesKey = (0, react.useMemo)(() => JSON.stringify(primaryKeys), [primaryKeys]);
|
|
6064
|
-
const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation({
|
|
6328
|
+
const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown, onChartPointerMove } = useKeyboardNavigation({
|
|
6065
6329
|
selectedIndex,
|
|
6066
6330
|
setSelectedIndex,
|
|
6067
6331
|
isNavigating,
|
|
@@ -6071,6 +6335,10 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
|
|
|
6071
6335
|
onActivate: activateSelectedBar,
|
|
6072
6336
|
visibleSeriesKey
|
|
6073
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]);
|
|
6074
6342
|
const comparisonEntries = (0, react.useMemo)(() => {
|
|
6075
6343
|
const primaryByGroup = new Map(primaryEntries.map(({ series, index }) => [series.group, {
|
|
6076
6344
|
label: series.label,
|
|
@@ -6394,7 +6662,8 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
|
|
|
6394
6662
|
groupPadding,
|
|
6395
6663
|
withTooltips,
|
|
6396
6664
|
onPointerDown,
|
|
6397
|
-
onPointerUp
|
|
6665
|
+
onPointerUp,
|
|
6666
|
+
onPointerMove: handlePointerMove
|
|
6398
6667
|
}),
|
|
6399
6668
|
!allSeriesHidden && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WholeNumberTicks, {
|
|
6400
6669
|
...wholeNumberTicksProps,
|
|
@@ -7076,12 +7345,13 @@ function normalizeGeoChartError(eventArgs) {
|
|
|
7076
7345
|
* @param props.height - Height of the chart in pixels
|
|
7077
7346
|
* @param props.region - Region to display ('world', 'US', or ISO 3166-1 alpha-2 code)
|
|
7078
7347
|
* @param props.resolution - Resolution level ('countries', 'provinces', or 'metros')
|
|
7348
|
+
* @param props.domain - Country code whose viewpoint sets the disputed borders
|
|
7079
7349
|
* @param props.onError - Optional callback for Google Charts errors
|
|
7080
7350
|
* @param props.className - Additional CSS class name for the chart container
|
|
7081
7351
|
* @param props.renderPlaceholder - Optional render function for the loading placeholder
|
|
7082
7352
|
* @return A React component displaying an interactive map with data visualization
|
|
7083
7353
|
*/
|
|
7084
|
-
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 }) => {
|
|
7085
7355
|
const { getElementStyles } = useGlobalChartsContext();
|
|
7086
7356
|
const scopeElement = useChartScopeElement();
|
|
7087
7357
|
const containerRef = (0, react.useRef)(null);
|
|
@@ -7145,6 +7415,7 @@ const GeoChartInternal = ({ className, data, width, height, region = "world", re
|
|
|
7145
7415
|
const options = (0, react.useMemo)(() => ({
|
|
7146
7416
|
...region !== "world" && { region },
|
|
7147
7417
|
...resolution !== "countries" && { resolution },
|
|
7418
|
+
...domain && { domain },
|
|
7148
7419
|
colorAxis: { colors: [lightColorHex, fullColorHex] },
|
|
7149
7420
|
backgroundColor: backgroundColorHex,
|
|
7150
7421
|
datalessRegionColor: defaultFillColorHex,
|
|
@@ -7158,6 +7429,7 @@ const GeoChartInternal = ({ className, data, width, height, region = "world", re
|
|
|
7158
7429
|
}), [
|
|
7159
7430
|
region,
|
|
7160
7431
|
resolution,
|
|
7432
|
+
domain,
|
|
7161
7433
|
lightColorHex,
|
|
7162
7434
|
fullColorHex,
|
|
7163
7435
|
backgroundColorHex,
|
|
@@ -11451,6 +11723,7 @@ function RadialWipeAnimation({ id, radius, innerRadius = 0, durationMs = 1e3, wi
|
|
|
11451
11723
|
var pie_chart_module_default = {
|
|
11452
11724
|
"pie-chart__label-plate": "a8ccharts-gnszbG-pie-chart__label-plate",
|
|
11453
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",
|
|
11454
11727
|
"pie-chart__plot": "a8ccharts-gnszbG-pie-chart__plot",
|
|
11455
11728
|
"pie-chart--responsive": "a8ccharts-gnszbG-pie-chart--responsive"
|
|
11456
11729
|
};
|
|
@@ -11495,6 +11768,25 @@ const validateData$1 = (data) => {
|
|
|
11495
11768
|
};
|
|
11496
11769
|
};
|
|
11497
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
|
+
/**
|
|
11498
11790
|
* Renders a pie or donut chart using the provided data.
|
|
11499
11791
|
*
|
|
11500
11792
|
* @param {PieChartProps} props - Component props
|
|
@@ -11508,11 +11800,36 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
|
|
|
11508
11800
|
const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } = (0, _visx_tooltip.useTooltip)();
|
|
11509
11801
|
const standaloneScopeClass = useStandaloneScopeClass();
|
|
11510
11802
|
const containerRef = (0, react.useRef)(null);
|
|
11803
|
+
const rootRef = (0, react.useRef)(null);
|
|
11804
|
+
const [labelPointers, setLabelPointers] = (0, react.useState)(null);
|
|
11511
11805
|
const onMouseLeave = (0, react.useCallback)(() => {
|
|
11512
11806
|
if (!withTooltips) return;
|
|
11513
11807
|
hideTooltip();
|
|
11514
11808
|
}, [withTooltips, hideTooltip]);
|
|
11515
|
-
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
|
+
]);
|
|
11516
11833
|
const dataWithPercentages = useDataWithPercentages(data);
|
|
11517
11834
|
const { visibleData, allSegmentsHidden, legendData } = useLegendVisibilityData({
|
|
11518
11835
|
data: dataWithPercentages,
|
|
@@ -11523,7 +11840,6 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
|
|
|
11523
11840
|
showValues: true,
|
|
11524
11841
|
legendValueDisplay
|
|
11525
11842
|
}), [legendValueDisplay]));
|
|
11526
|
-
const { isValid, message } = validateData$1(data);
|
|
11527
11843
|
const { svgChildren, htmlChildren, legendChildren, otherChildren } = useChartChildren(children, "PieChart");
|
|
11528
11844
|
const chartMetadata = (0, react.useMemo)(() => ({
|
|
11529
11845
|
thickness,
|
|
@@ -11585,6 +11901,7 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
|
|
|
11585
11901
|
legendElement,
|
|
11586
11902
|
legendChildren,
|
|
11587
11903
|
gap,
|
|
11904
|
+
rootRef,
|
|
11588
11905
|
className: (0, clsx.default)("pie-chart", pie_chart_module_default["pie-chart"], { [pie_chart_module_default["pie-chart--responsive"]]: !propWidth && !propHeight }, className),
|
|
11589
11906
|
style: {
|
|
11590
11907
|
width: propWidth || void 0,
|
|
@@ -11646,9 +11963,10 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
|
|
|
11646
11963
|
tooltipTop: event.clientY - bounds.top + tooltipOffsetY
|
|
11647
11964
|
});
|
|
11648
11965
|
};
|
|
11966
|
+
const fill = accessors.fill(arc.data);
|
|
11649
11967
|
const pathProps = {
|
|
11650
11968
|
d: pie.path(arc) || "",
|
|
11651
|
-
fill
|
|
11969
|
+
fill,
|
|
11652
11970
|
"data-testid": "pie-segment"
|
|
11653
11971
|
};
|
|
11654
11972
|
const groupProps = {};
|
|
@@ -11676,7 +11994,7 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
|
|
|
11676
11994
|
ry: 4,
|
|
11677
11995
|
pointerEvents: "none"
|
|
11678
11996
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
11679
|
-
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) }),
|
|
11680
11998
|
x: centroidX,
|
|
11681
11999
|
y: centroidY,
|
|
11682
12000
|
dy: ".33em",
|