@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.js
CHANGED
|
@@ -436,9 +436,11 @@ const hexToRgba = (hex, alpha) => {
|
|
|
436
436
|
return color(hex).copy({ opacity: alpha }).formatRgb();
|
|
437
437
|
};
|
|
438
438
|
/**
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
* @
|
|
439
|
+
* Weighted Euclidean distance between two HSL colors.
|
|
440
|
+
*
|
|
441
|
+
* @deprecated Not perceptual and blind to color vision deficiency, so charts no longer use it; it will be removed in a future major version.
|
|
442
|
+
* @param hsl1 - first color in HSL format [h, s, l]
|
|
443
|
+
* @param hsl2 - second color in HSL format [h, s, l]
|
|
442
444
|
* @return distance value (0-100+, lower means more similar)
|
|
443
445
|
*/
|
|
444
446
|
const getColorDistance = (hsl1, hsl2) => {
|
|
@@ -800,145 +802,421 @@ const createZonedClock = (timeZone) => {
|
|
|
800
802
|
* 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.
|
|
801
803
|
*/
|
|
802
804
|
const ChartScopeContext = createContext(null);
|
|
803
|
-
//#endregion
|
|
804
|
-
//#region src/providers/chart-context/private/get-chart-color.ts
|
|
805
|
-
/**
|
|
806
|
-
* Golden ratio for mathematically pleasing color distribution
|
|
807
|
-
* Used to generate evenly spaced hues that are visually distinct
|
|
808
|
-
*/
|
|
809
|
-
const GOLDEN_RATIO = .618033988749;
|
|
810
805
|
/**
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
/**
|
|
816
|
-
* Maximum attempts to find a sufficiently different color
|
|
817
|
-
* Prevents infinite loops while allowing reasonable search space
|
|
818
|
-
*/
|
|
819
|
-
const MAX_COLOR_GENERATION_ATTEMPTS = 50;
|
|
820
|
-
/**
|
|
821
|
-
* Color variation attempt offset
|
|
822
|
-
* Small increment to explore slightly different color variations per attempt
|
|
823
|
-
*/
|
|
824
|
-
const VARIATION_ATTEMPT_OFFSET = .1;
|
|
825
|
-
/**
|
|
826
|
-
* Base saturation percentage for generated colors
|
|
827
|
-
* 45% provides muted, professional colors without being washed out
|
|
828
|
-
*/
|
|
829
|
-
const BASE_SATURATION = 45;
|
|
830
|
-
/**
|
|
831
|
-
* Number of saturation variation steps
|
|
832
|
-
* Creates 3 different saturation levels for variety
|
|
833
|
-
*/
|
|
834
|
-
const SATURATION_VARIATION_STEPS = 3;
|
|
835
|
-
/**
|
|
836
|
-
* Saturation increment per variation step
|
|
837
|
-
* 10% increments provide subtle variation while keeping colors muted
|
|
838
|
-
* Results in saturation levels: 45%, 55%, 65%
|
|
839
|
-
*/
|
|
840
|
-
const SATURATION_INCREMENT = 10;
|
|
841
|
-
/**
|
|
842
|
-
* Base lightness percentage for generated colors
|
|
843
|
-
* 35% ensures sufficient contrast against white backgrounds for WCAG AA compliance
|
|
844
|
-
* WCAG AA requires 4.5:1 contrast ratio for normal text
|
|
806
|
+
* The catalog role holding one series-palette slot.
|
|
807
|
+
*
|
|
808
|
+
* @param slot - The one-based slot number.
|
|
809
|
+
* @return The role name.
|
|
845
810
|
*/
|
|
846
|
-
const
|
|
811
|
+
const seriesRole = (slot) => `--a8c-charts-color-series-${slot}`;
|
|
812
|
+
const SERIES_SLOT_1_FALLBACK = "#3858e9";
|
|
813
|
+
/** The catalog pointer for every slot, in slot order. */
|
|
814
|
+
const SERIES_PALETTE_POINTERS = Array.from({ length: 5 }, (_, index) => index === 0 ? `var(${seriesRole(1)}, ${SERIES_SLOT_1_FALLBACK})` : `var(${seriesRole(index + 1)})`);
|
|
815
|
+
//#endregion
|
|
816
|
+
//#region src/providers/chart-context/private/catalog-pointers.ts
|
|
817
|
+
/** Terminal literals the provider also generates from before its first read, so SSR and the first client render agree. */
|
|
818
|
+
const BACKGROUND_FALLBACK = "#ffffff";
|
|
819
|
+
const LABEL_FALLBACK = "#1e1e1e";
|
|
820
|
+
const LABEL_INVERSE_FALLBACK = "#f0f0f0";
|
|
821
|
+
const CATALOG_POINTERS = {
|
|
822
|
+
background: `var(--a8c-charts-color-background, ${BACKGROUND_FALLBACK})`,
|
|
823
|
+
label: `var(--a8c-charts-color-label, ${LABEL_FALLBACK})`,
|
|
824
|
+
labelAxis: "var(--a8c-charts-color-label-axis, #1e1e1e)",
|
|
825
|
+
labelInverse: `var(--a8c-charts-color-label-inverse, ${LABEL_INVERSE_FALLBACK})`,
|
|
826
|
+
labelBackground: "var(--a8c-charts-color-label-background, transparent)",
|
|
827
|
+
grid: "var(--a8c-charts-color-grid, #dbdbdb)",
|
|
828
|
+
axisX: "var(--a8c-charts-color-axis-x, #dbdbdb)",
|
|
829
|
+
tickX: "var(--a8c-charts-color-tick-x, #dbdbdb)",
|
|
830
|
+
axisY: "var(--a8c-charts-color-axis-y, none)",
|
|
831
|
+
tickY: "var(--a8c-charts-color-tick-y, none)",
|
|
832
|
+
annotation: "var(--a8c-charts-color-annotation, #1e1e1e)",
|
|
833
|
+
surface: "var(--a8c-charts-color-surface, #fff)",
|
|
834
|
+
surfaceSecondary: "var(--a8c-charts-color-surface-secondary, #f4f4f4)",
|
|
835
|
+
trendUp: "var(--a8c-charts-color-trend-up, #008030)",
|
|
836
|
+
trendDown: "var(--a8c-charts-color-trend-down, #cc1818)",
|
|
837
|
+
series: SERIES_PALETTE_POINTERS
|
|
838
|
+
};
|
|
847
839
|
/**
|
|
848
|
-
*
|
|
849
|
-
*
|
|
840
|
+
* The annotation parts visx paints, in the shape `@visx/annotation` takes. `radius`
|
|
841
|
+
* rides along as the base the theme and the per-datum styles merge onto.
|
|
850
842
|
*/
|
|
851
|
-
const
|
|
843
|
+
const ANNOTATION_POINTERS = {
|
|
844
|
+
label: {
|
|
845
|
+
anchorLineStroke: CATALOG_POINTERS.annotation,
|
|
846
|
+
backgroundFill: CATALOG_POINTERS.surface
|
|
847
|
+
},
|
|
848
|
+
connector: { stroke: CATALOG_POINTERS.annotation },
|
|
849
|
+
circleSubject: {
|
|
850
|
+
stroke: "transparent",
|
|
851
|
+
fill: CATALOG_POINTERS.annotation,
|
|
852
|
+
radius: 5
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
//#endregion
|
|
856
|
+
//#region src/providers/chart-context/private/perceptual-color.ts
|
|
857
|
+
const DEUTERANOPIA = [
|
|
858
|
+
.367322,
|
|
859
|
+
.860646,
|
|
860
|
+
-.227968,
|
|
861
|
+
.280085,
|
|
862
|
+
.672501,
|
|
863
|
+
.047413,
|
|
864
|
+
-.01182,
|
|
865
|
+
.04294,
|
|
866
|
+
.968881
|
|
867
|
+
];
|
|
868
|
+
const PROTANOPIA = [
|
|
869
|
+
.152286,
|
|
870
|
+
1.052583,
|
|
871
|
+
-.204868,
|
|
872
|
+
.114503,
|
|
873
|
+
.786281,
|
|
874
|
+
.099216,
|
|
875
|
+
-.003882,
|
|
876
|
+
-.048116,
|
|
877
|
+
1.051998
|
|
878
|
+
];
|
|
879
|
+
const DEGREES = Math.PI / 180;
|
|
880
|
+
const toLinear = (channel) => channel <= .04045 ? channel / 12.92 : Math.pow((channel + .055) / 1.055, 2.4);
|
|
881
|
+
const fromLinear = (channel) => channel <= .0031308 ? 12.92 * channel : 1.055 * Math.pow(channel, 1 / 2.4) - .055;
|
|
882
|
+
const hexToLinear = (hex) => {
|
|
883
|
+
validateHexColor(hex);
|
|
884
|
+
return [
|
|
885
|
+
1,
|
|
886
|
+
3,
|
|
887
|
+
5
|
|
888
|
+
].map((start) => toLinear(parseInt(hex.slice(start, start + 2), 16) / 255));
|
|
889
|
+
};
|
|
890
|
+
const linearToHex = (linear) => "#" + linear.map((channel) => Math.round(Math.min(1, Math.max(0, fromLinear(channel))) * 255).toString(16).padStart(2, "0")).join("");
|
|
891
|
+
const simulate = (matrix, [r, g, b]) => [
|
|
892
|
+
0,
|
|
893
|
+
3,
|
|
894
|
+
6
|
|
895
|
+
].map((row) => Math.min(1, Math.max(0, matrix[row] * r + matrix[row + 1] * g + matrix[row + 2] * b)));
|
|
896
|
+
const linearToLab = ([r, g, b]) => {
|
|
897
|
+
const x = (.4124564 * r + .3575761 * g + .1804375 * b) / .95047;
|
|
898
|
+
const y = .2126729 * r + .7151522 * g + .072175 * b;
|
|
899
|
+
const z = (.0193339 * r + .119192 * g + .9503041 * b) / 1.08883;
|
|
900
|
+
const f = (t) => t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116;
|
|
901
|
+
return [
|
|
902
|
+
116 * f(y) - 16,
|
|
903
|
+
500 * (f(x) - f(y)),
|
|
904
|
+
200 * (f(y) - f(z))
|
|
905
|
+
];
|
|
906
|
+
};
|
|
852
907
|
/**
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
|
|
858
|
-
|
|
908
|
+
* CIEDE2000 color difference between two CIELAB colors.
|
|
909
|
+
*
|
|
910
|
+
* @param first - First color in CIELAB.
|
|
911
|
+
* @param second - Second color in CIELAB.
|
|
912
|
+
* @return ΔE00; about 10 is where two chart categories stop being reliably distinguishable.
|
|
913
|
+
*/
|
|
914
|
+
const deltaE2000 = (first, second) => {
|
|
915
|
+
const [l1, a1, b1] = first;
|
|
916
|
+
const [l2, a2, b2] = second;
|
|
917
|
+
const chromaMean = (Math.hypot(a1, b1) + Math.hypot(a2, b2)) / 2;
|
|
918
|
+
const g = .5 * (1 - Math.sqrt(chromaMean ** 7 / (chromaMean ** 7 + 25 ** 7)));
|
|
919
|
+
const a1p = a1 * (1 + g);
|
|
920
|
+
const a2p = a2 * (1 + g);
|
|
921
|
+
const c1p = Math.hypot(a1p, b1);
|
|
922
|
+
const c2p = Math.hypot(a2p, b2);
|
|
923
|
+
const hueOf = (a, b) => a === 0 && b === 0 ? 0 : (Math.atan2(b, a) / DEGREES + 360) % 360;
|
|
924
|
+
const h1p = hueOf(a1p, b1);
|
|
925
|
+
const h2p = hueOf(a2p, b2);
|
|
926
|
+
const chromatic = c1p * c2p !== 0;
|
|
927
|
+
let hueDelta = chromatic ? h2p - h1p : 0;
|
|
928
|
+
if (hueDelta > 180) hueDelta -= 360;
|
|
929
|
+
else if (hueDelta < -180) hueDelta += 360;
|
|
930
|
+
const lightnessDelta = l2 - l1;
|
|
931
|
+
const chromaDelta = c2p - c1p;
|
|
932
|
+
const hueDifference = 2 * Math.sqrt(c1p * c2p) * Math.sin(hueDelta * DEGREES / 2);
|
|
933
|
+
const lightnessMean = (l1 + l2) / 2;
|
|
934
|
+
const chromaMeanPrime = (c1p + c2p) / 2;
|
|
935
|
+
let hueMean = h1p + h2p;
|
|
936
|
+
if (chromatic) {
|
|
937
|
+
if (Math.abs(h1p - h2p) > 180) hueMean += hueMean < 360 ? 360 : -360;
|
|
938
|
+
hueMean /= 2;
|
|
939
|
+
}
|
|
940
|
+
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);
|
|
941
|
+
const lightnessScale = 1 + .015 * (lightnessMean - 50) ** 2 / Math.sqrt(20 + (lightnessMean - 50) ** 2);
|
|
942
|
+
const chromaScale = 1 + .045 * chromaMeanPrime;
|
|
943
|
+
const hueScale = 1 + .015 * chromaMeanPrime * t;
|
|
944
|
+
const rotation = -2 * Math.sqrt(chromaMeanPrime ** 7 / (chromaMeanPrime ** 7 + 25 ** 7)) * Math.sin(60 * Math.exp(-(((hueMean - 275) / 25) ** 2)) * DEGREES);
|
|
945
|
+
const lightnessTerm = lightnessDelta / lightnessScale;
|
|
946
|
+
const chromaTerm = chromaDelta / chromaScale;
|
|
947
|
+
const hueTerm = hueDifference / hueScale;
|
|
948
|
+
return Math.sqrt(lightnessTerm ** 2 + chromaTerm ** 2 + hueTerm ** 2 + rotation * chromaTerm * hueTerm);
|
|
949
|
+
};
|
|
859
950
|
/**
|
|
860
|
-
*
|
|
861
|
-
*
|
|
951
|
+
* A hex color in CIELAB as seen in normal vision, under deuteranopia and under protanopia.
|
|
952
|
+
*
|
|
953
|
+
* @param hex - Six-digit hex color.
|
|
954
|
+
* @return The three views.
|
|
955
|
+
* @throws {Error} if hex string is malformed
|
|
862
956
|
*/
|
|
863
|
-
const
|
|
957
|
+
const hexToViews = (hex) => {
|
|
958
|
+
const linear = hexToLinear(hex);
|
|
959
|
+
return [
|
|
960
|
+
linearToLab(linear),
|
|
961
|
+
linearToLab(simulate(DEUTERANOPIA, linear)),
|
|
962
|
+
linearToLab(simulate(PROTANOPIA, linear))
|
|
963
|
+
];
|
|
964
|
+
};
|
|
864
965
|
/**
|
|
865
|
-
*
|
|
866
|
-
*
|
|
966
|
+
* The smallest ΔE00 between two colors across the three views.
|
|
967
|
+
*
|
|
968
|
+
* @param first - First color's views.
|
|
969
|
+
* @param second - Second color's views.
|
|
970
|
+
* @return The distance in the view where the two are hardest to tell apart.
|
|
867
971
|
*/
|
|
868
|
-
const
|
|
972
|
+
const viewDistance = (first, second) => Math.min(deltaE2000(first[0], second[0]), deltaE2000(first[1], second[1]), deltaE2000(first[2], second[2]));
|
|
869
973
|
/**
|
|
870
|
-
*
|
|
871
|
-
*
|
|
974
|
+
* Convert an OKLCH color to sRGB hex.
|
|
975
|
+
*
|
|
976
|
+
* @param lightness - OKLab L, 0 to 1.
|
|
977
|
+
* @param chroma - OKLCH C.
|
|
978
|
+
* @param hue - Hue in degrees.
|
|
979
|
+
* @return Hex color, or null when the color is outside the sRGB gamut.
|
|
872
980
|
*/
|
|
873
|
-
const
|
|
981
|
+
const oklchToHex = (lightness, chroma, hue) => {
|
|
982
|
+
const a = chroma * Math.cos(hue * DEGREES);
|
|
983
|
+
const b = chroma * Math.sin(hue * DEGREES);
|
|
984
|
+
const l = (lightness + .3963377774 * a + .2158037573 * b) ** 3;
|
|
985
|
+
const m = (lightness - .1055613458 * a - .0638541728 * b) ** 3;
|
|
986
|
+
const s = (lightness - .0894841775 * a - 1.291485548 * b) ** 3;
|
|
987
|
+
const linear = [
|
|
988
|
+
4.0767416621 * l - 3.3077633306 * m + .2309645873 * s,
|
|
989
|
+
-1.2684380046 * l + 2.6097574011 * m - .3413193965 * s,
|
|
990
|
+
-.0041960863 * l - .7034186147 * m + 1.707614701 * s
|
|
991
|
+
];
|
|
992
|
+
if (linear.some((channel) => channel < -1e-4 || channel > 1.0001)) return null;
|
|
993
|
+
return linearToHex(linear);
|
|
994
|
+
};
|
|
874
995
|
/**
|
|
875
|
-
*
|
|
996
|
+
* OKLCH chroma and hue of a hex color.
|
|
997
|
+
*
|
|
998
|
+
* @param hex - Six-digit hex color.
|
|
999
|
+
* @return Chroma, and hue in degrees from 0 to 360.
|
|
1000
|
+
* @throws {Error} if hex string is malformed
|
|
876
1001
|
*/
|
|
877
|
-
const
|
|
1002
|
+
const hexToOklch = (hex) => {
|
|
1003
|
+
const [r, g, b] = hexToLinear(hex);
|
|
1004
|
+
const l = Math.cbrt(.4122214708 * r + .5363325363 * g + .0514459929 * b);
|
|
1005
|
+
const m = Math.cbrt(.2119034982 * r + .6806995451 * g + .1073969566 * b);
|
|
1006
|
+
const s = Math.cbrt(.0883024619 * r + .2817188376 * g + .6299787005 * b);
|
|
1007
|
+
const a = 1.9779984951 * l - 2.428592205 * m + .4505937099 * s;
|
|
1008
|
+
const bAxis = .0259040371 * l + .7827717662 * m - .808675766 * s;
|
|
1009
|
+
return {
|
|
1010
|
+
chroma: Math.hypot(a, bAxis),
|
|
1011
|
+
hue: (Math.atan2(bAxis, a) / DEGREES + 360) % 360
|
|
1012
|
+
};
|
|
1013
|
+
};
|
|
878
1014
|
/**
|
|
879
|
-
*
|
|
1015
|
+
* WCAG contrast ratio between two relative luminances.
|
|
1016
|
+
*
|
|
1017
|
+
* @param first - First relative luminance.
|
|
1018
|
+
* @param second - Second relative luminance.
|
|
1019
|
+
* @return Ratio from 1 to 21.
|
|
880
1020
|
*/
|
|
881
|
-
const
|
|
1021
|
+
const luminanceContrastRatio = (first, second) => (Math.max(first, second) + .05) / (Math.min(first, second) + .05);
|
|
882
1022
|
/**
|
|
883
|
-
*
|
|
1023
|
+
* WCAG contrast ratio between two hex colors.
|
|
884
1024
|
*
|
|
885
|
-
* @param
|
|
886
|
-
* @param
|
|
887
|
-
* @return
|
|
1025
|
+
* @param first - First hex color.
|
|
1026
|
+
* @param second - Second hex color.
|
|
1027
|
+
* @return Ratio from 1 to 21.
|
|
1028
|
+
* @throws {Error} if either hex string is malformed
|
|
888
1029
|
*/
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1030
|
+
const contrastRatio = (first, second) => luminanceContrastRatio(relativeLuminance(first), relativeLuminance(second));
|
|
1031
|
+
/** WCAG 1.4.3 text contrast, which a label drawn on a fill needs from at least one label color. */
|
|
1032
|
+
const MIN_LABEL_CONTRAST = 4.5;
|
|
1033
|
+
/** Degrees of hue one unit of OKLCH chroma is worth when ranking candidates by hue. */
|
|
1034
|
+
const CHROMA_WEIGHT = 300;
|
|
1035
|
+
/** ΔE00 one unit of OKLCH chroma is worth when ranking candidates by distance. */
|
|
1036
|
+
const FAR_CHROMA_WEIGHT = 100;
|
|
1037
|
+
const toCandidate = (hex, oklch = hexToOklch(hex)) => ({
|
|
1038
|
+
hex,
|
|
1039
|
+
views: hexToViews(hex),
|
|
1040
|
+
luminance: relativeLuminance(hex),
|
|
1041
|
+
hue: oklch.hue,
|
|
1042
|
+
chroma: oklch.chroma
|
|
1043
|
+
});
|
|
1044
|
+
const rank = (candidate, distance, anchorHue) => anchorHue === null ? -(distance + candidate.chroma * FAR_CHROMA_WEIGHT) : (candidate.hue - anchorHue + 360) % 360 - candidate.chroma * CHROMA_WEIGHT;
|
|
1045
|
+
const LIGHTNESS_STEPS = Array.from({ length: 13 }, (_, step) => .45 + step * .025);
|
|
1046
|
+
const CHROMA_STEPS = [
|
|
1047
|
+
.12,
|
|
1048
|
+
.15,
|
|
1049
|
+
.18,
|
|
1050
|
+
.21
|
|
1051
|
+
];
|
|
1052
|
+
const HUE_STEPS = Array.from({ length: 72 }, (_, step) => step * 5);
|
|
1053
|
+
let candidateGrid = null;
|
|
1054
|
+
const getCandidateGrid = () => {
|
|
1055
|
+
if (!candidateGrid) {
|
|
1056
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1057
|
+
candidateGrid = [];
|
|
1058
|
+
for (const lightness of LIGHTNESS_STEPS) for (const chroma of CHROMA_STEPS) for (const hue of HUE_STEPS) {
|
|
1059
|
+
const hex = oklchToHex(lightness, chroma, hue);
|
|
1060
|
+
if (hex && !seen.has(hex)) {
|
|
1061
|
+
seen.add(hex);
|
|
1062
|
+
candidateGrid.push(toCandidate(hex, {
|
|
1063
|
+
hue,
|
|
1064
|
+
chroma
|
|
1065
|
+
}));
|
|
908
1066
|
}
|
|
909
|
-
const expandedRange = Math.max(hueRange * HUE_RANGE_EXPANSION_FACTOR, MIN_HUE_RANGE_DEGREES);
|
|
910
|
-
hue = (minHue + maxHue) / 2 - expandedRange / 2 + hue / FULL_HUE_ROTATION_DEGREES * expandedRange;
|
|
911
|
-
hue = (hue % FULL_HUE_ROTATION_DEGREES + FULL_HUE_ROTATION_DEGREES) % FULL_HUE_ROTATION_DEGREES;
|
|
912
1067
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1068
|
+
}
|
|
1069
|
+
return candidateGrid;
|
|
1070
|
+
};
|
|
1071
|
+
const legiblePools = /* @__PURE__ */ new Map();
|
|
1072
|
+
const legibleCandidatesFor = (rawBackground, labelColors) => {
|
|
1073
|
+
const background = rawBackground.toLowerCase();
|
|
1074
|
+
const key = [background, ...labelColors.map((hex) => hex.toLowerCase())].join("|");
|
|
1075
|
+
let pool = legiblePools.get(key);
|
|
1076
|
+
if (!pool) {
|
|
1077
|
+
const backgroundLuminance = relativeLuminance(background);
|
|
1078
|
+
const labelLuminances = labelColors.map(relativeLuminance);
|
|
1079
|
+
const onBackground = getCandidateGrid().filter((candidate) => luminanceContrastRatio(candidate.luminance, backgroundLuminance) >= 3);
|
|
1080
|
+
const underLabels = onBackground.filter((candidate) => labelLuminances.some((labelLuminance) => luminanceContrastRatio(candidate.luminance, labelLuminance) >= MIN_LABEL_CONTRAST));
|
|
1081
|
+
pool = underLabels.length > 0 ? underLabels : onBackground;
|
|
1082
|
+
legiblePools.set(key, pool);
|
|
1083
|
+
}
|
|
1084
|
+
return pool;
|
|
1085
|
+
};
|
|
1086
|
+
/**
|
|
1087
|
+
* Build a tracker for a candidate pool, seeding each unused candidate's nearest distance against
|
|
1088
|
+
* every color already in the palette.
|
|
1089
|
+
*
|
|
1090
|
+
* @param pool - Candidates to track, in selection-priority order.
|
|
1091
|
+
* @param palette - Colors already placed.
|
|
1092
|
+
* @param usedHexes - Hex values already placed; a used candidate's distance is never read.
|
|
1093
|
+
* @return The tracker.
|
|
1094
|
+
*/
|
|
1095
|
+
const buildTracker = (pool, palette, usedHexes) => {
|
|
1096
|
+
const nearest = new Float64Array(pool.length).fill(Infinity);
|
|
1097
|
+
for (let i = 0; i < pool.length; i++) {
|
|
1098
|
+
if (usedHexes.has(pool[i].hex)) continue;
|
|
1099
|
+
for (const color of palette) {
|
|
1100
|
+
const distance = viewDistance(pool[i].views, color.views);
|
|
1101
|
+
if (distance < nearest[i]) nearest[i] = distance;
|
|
924
1102
|
}
|
|
925
|
-
if (isSufficientlyDifferent) return hsl(Math.round(hue), saturation / 100, lightness / 100).formatHex();
|
|
926
1103
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1104
|
+
return {
|
|
1105
|
+
pool,
|
|
1106
|
+
nearest
|
|
1107
|
+
};
|
|
931
1108
|
};
|
|
932
1109
|
/**
|
|
933
|
-
*
|
|
1110
|
+
* Fold one newly placed color into a tracker's nearest distances.
|
|
934
1111
|
*
|
|
935
|
-
* @param
|
|
936
|
-
* @
|
|
1112
|
+
* @param tracker - Tracker to update.
|
|
1113
|
+
* @param addedColor - Color just added to the palette.
|
|
1114
|
+
* @param usedHexes - Hex values already placed; a used candidate's distance is never read.
|
|
937
1115
|
*/
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
const
|
|
1116
|
+
const updateTracker = (tracker, addedColor, usedHexes) => {
|
|
1117
|
+
for (let i = 0; i < tracker.pool.length; i++) {
|
|
1118
|
+
if (usedHexes.has(tracker.pool[i].hex)) continue;
|
|
1119
|
+
const distance = viewDistance(tracker.pool[i].views, addedColor.views);
|
|
1120
|
+
if (distance < tracker.nearest[i]) tracker.nearest[i] = distance;
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
/**
|
|
1124
|
+
* Pick the next color: the best-ranked unused candidate that clears `PREFERRED_SEPARATION` from
|
|
1125
|
+
* every earlier color; otherwise the farthest candidate.
|
|
1126
|
+
*
|
|
1127
|
+
* @param tracker - Tracker to read.
|
|
1128
|
+
* @param usedHexes - Hex values already placed; a used candidate is never picked.
|
|
1129
|
+
* @param anchorHue - Hue to walk the wheel from, or null to rank by distance.
|
|
1130
|
+
* @return The picked candidate, or null if every candidate in the pool is used.
|
|
1131
|
+
*/
|
|
1132
|
+
const pickNext = (tracker, usedHexes, anchorHue) => {
|
|
1133
|
+
let farthestIndex = -1;
|
|
1134
|
+
let farthestDistance = -Infinity;
|
|
1135
|
+
let preferredIndex = -1;
|
|
1136
|
+
let preferredRank = Infinity;
|
|
1137
|
+
for (let i = 0; i < tracker.pool.length; i++) {
|
|
1138
|
+
if (usedHexes.has(tracker.pool[i].hex)) continue;
|
|
1139
|
+
const distance = tracker.nearest[i];
|
|
1140
|
+
if (distance > farthestDistance) {
|
|
1141
|
+
farthestDistance = distance;
|
|
1142
|
+
farthestIndex = i;
|
|
1143
|
+
}
|
|
1144
|
+
if (distance >= 12) {
|
|
1145
|
+
const candidateRank = rank(tracker.pool[i], distance, anchorHue);
|
|
1146
|
+
if (candidateRank < preferredRank || candidateRank === preferredRank && distance > tracker.nearest[preferredIndex]) {
|
|
1147
|
+
preferredRank = candidateRank;
|
|
1148
|
+
preferredIndex = i;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
const index = preferredIndex === -1 ? farthestIndex : preferredIndex;
|
|
1153
|
+
return index === -1 ? null : tracker.pool[index];
|
|
1154
|
+
};
|
|
1155
|
+
const generators = /* @__PURE__ */ new Map();
|
|
1156
|
+
const MAX_CACHED_GENERATORS = 32;
|
|
1157
|
+
/**
|
|
1158
|
+
* Build the series palette: the seeds, then colors that stay apart from every earlier color in
|
|
1159
|
+
* normal vision and under deuteranopia and protanopia.
|
|
1160
|
+
*
|
|
1161
|
+
* @param seeds - Resolved hex palette slots, in slot order.
|
|
1162
|
+
* @param background - Resolved hex chart background.
|
|
1163
|
+
* @param labelColors - Resolved hex label colors that may be drawn on a fill; each generated color reaches `MIN_LABEL_CONTRAST` with one of them.
|
|
1164
|
+
* @return The color at a palette index.
|
|
1165
|
+
*/
|
|
1166
|
+
const createPaletteGenerator = (seeds, background, labelColors = []) => {
|
|
1167
|
+
const normalizedSeeds = seeds.map((hex) => hex.toLowerCase());
|
|
1168
|
+
const normalizedBackground = background.toLowerCase();
|
|
1169
|
+
const normalizedLabels = labelColors.map((hex) => hex.toLowerCase());
|
|
1170
|
+
const key = `${normalizedSeeds.join(",")}|${normalizedBackground}|${normalizedLabels.join(",")}`;
|
|
1171
|
+
let colorAt = generators.get(key);
|
|
1172
|
+
if (!colorAt) {
|
|
1173
|
+
if (generators.size >= MAX_CACHED_GENERATORS) generators.delete(generators.keys().next().value);
|
|
1174
|
+
colorAt = buildPaletteGenerator(normalizedSeeds, normalizedBackground, normalizedLabels);
|
|
1175
|
+
generators.set(key, colorAt);
|
|
1176
|
+
}
|
|
1177
|
+
return colorAt;
|
|
1178
|
+
};
|
|
1179
|
+
const buildPaletteGenerator = (normalizedSeeds, background, labelColors) => {
|
|
1180
|
+
const palette = normalizedSeeds.map((hex) => toCandidate(hex));
|
|
1181
|
+
const usedHexes = new Set(normalizedSeeds);
|
|
1182
|
+
const anchorHue = palette.length > 0 ? palette[0].hue : null;
|
|
1183
|
+
let legibleTracker = null;
|
|
1184
|
+
let legibleExhausted = false;
|
|
1185
|
+
let gridTracker = null;
|
|
1186
|
+
let gridExhausted = false;
|
|
1187
|
+
let placedBeforeExhaustion = 0;
|
|
1188
|
+
let repeatCount = 0;
|
|
1189
|
+
const nextColor = () => {
|
|
1190
|
+
const walkHue = (palette.length - normalizedSeeds.length) % 2 === 1 ? anchorHue : null;
|
|
1191
|
+
if (!legibleExhausted) {
|
|
1192
|
+
legibleTracker ??= buildTracker(legibleCandidatesFor(background, labelColors), palette, usedHexes);
|
|
1193
|
+
const picked = pickNext(legibleTracker, usedHexes, walkHue);
|
|
1194
|
+
if (picked) return picked;
|
|
1195
|
+
legibleExhausted = true;
|
|
1196
|
+
}
|
|
1197
|
+
if (!gridExhausted) {
|
|
1198
|
+
gridTracker ??= buildTracker(getCandidateGrid(), palette, usedHexes);
|
|
1199
|
+
const picked = pickNext(gridTracker, usedHexes, walkHue);
|
|
1200
|
+
if (picked) return picked;
|
|
1201
|
+
gridExhausted = true;
|
|
1202
|
+
placedBeforeExhaustion = palette.length;
|
|
1203
|
+
}
|
|
1204
|
+
const repeated = palette[repeatCount % placedBeforeExhaustion];
|
|
1205
|
+
repeatCount++;
|
|
1206
|
+
return repeated;
|
|
1207
|
+
};
|
|
1208
|
+
return (index) => {
|
|
1209
|
+
if (index < palette.length) return palette[index].hex;
|
|
1210
|
+
while (palette.length <= index) {
|
|
1211
|
+
const next = nextColor();
|
|
1212
|
+
usedHexes.add(next.hex);
|
|
1213
|
+
palette.push(next);
|
|
1214
|
+
if (legibleTracker && !legibleExhausted) updateTracker(legibleTracker, next, usedHexes);
|
|
1215
|
+
if (gridTracker && !gridExhausted) updateTracker(gridTracker, next, usedHexes);
|
|
1216
|
+
}
|
|
1217
|
+
return palette[index].hex;
|
|
1218
|
+
};
|
|
1219
|
+
};
|
|
942
1220
|
//#endregion
|
|
943
1221
|
//#region src/providers/chart-context/themes.ts
|
|
944
1222
|
/**
|
|
@@ -988,6 +1266,13 @@ const defaultTheme = {
|
|
|
988
1266
|
};
|
|
989
1267
|
//#endregion
|
|
990
1268
|
//#region src/providers/chart-context/global-charts-provider.tsx
|
|
1269
|
+
const resolveOpaqueHex = (pointer, element) => {
|
|
1270
|
+
const raw = resolveCssVariable(pointer, element);
|
|
1271
|
+
if (!raw || color(raw)?.opacity !== 1) return null;
|
|
1272
|
+
const hex = normalizeColorToHex(pointer, element, resolveCssVariable);
|
|
1273
|
+
return isValidHexColor(hex) ? hex : null;
|
|
1274
|
+
};
|
|
1275
|
+
const PLACEHOLDER_LABEL_COLORS = [LABEL_FALLBACK, LABEL_INVERSE_FALLBACK];
|
|
991
1276
|
const GlobalChartsContext = createContext(null);
|
|
992
1277
|
const GlobalChartsProvider = ({ children, theme, locale, timeZone }) => {
|
|
993
1278
|
const [charts, setCharts] = useState(() => /* @__PURE__ */ new Map());
|
|
@@ -1002,50 +1287,32 @@ const GlobalChartsProvider = ({ children, theme, locale, timeZone }) => {
|
|
|
1002
1287
|
const providerTheme = useMemo(() => theme ? mergeThemes(defaultTheme, theme) : defaultTheme, [theme]);
|
|
1003
1288
|
const [colorCache, setColorCache] = useState(() => ({
|
|
1004
1289
|
colors: [],
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
maxHue: 0
|
|
1290
|
+
background: BACKGROUND_FALLBACK,
|
|
1291
|
+
labelColors: PLACEHOLDER_LABEL_COLORS,
|
|
1292
|
+
colorAt: createPaletteGenerator([SERIES_SLOT_1_FALLBACK], BACKGROUND_FALLBACK, PLACEHOLDER_LABEL_COLORS)
|
|
1009
1293
|
}));
|
|
1010
1294
|
const [isColorPaletteResolved, setIsColorPaletteResolved] = useState(false);
|
|
1011
1295
|
useLayoutEffect(() => {
|
|
1012
1296
|
setIsColorPaletteResolved(false);
|
|
1013
1297
|
const resolvedColors = [];
|
|
1014
|
-
const hues = [];
|
|
1015
|
-
const existingHslColors = [];
|
|
1016
|
-
let minHue = 360;
|
|
1017
|
-
let maxHue = 0;
|
|
1018
1298
|
for (const color of SERIES_PALETTE_POINTERS) {
|
|
1019
1299
|
const normalizedColor = normalizeColorToHex(color, wrapperRef.current, resolveCssVariable);
|
|
1020
|
-
if (normalizedColor.
|
|
1021
|
-
resolvedColors.push(normalizedColor);
|
|
1022
|
-
const hslColor = hsl(normalizedColor);
|
|
1023
|
-
if (!isNaN(hslColor.h)) {
|
|
1024
|
-
const hslTuple = [
|
|
1025
|
-
hslColor.h,
|
|
1026
|
-
hslColor.s * 100,
|
|
1027
|
-
hslColor.l * 100
|
|
1028
|
-
];
|
|
1029
|
-
hues.push(hslTuple[0]);
|
|
1030
|
-
existingHslColors.push(hslTuple);
|
|
1031
|
-
minHue = Math.min(minHue, hslTuple[0]);
|
|
1032
|
-
maxHue = Math.max(maxHue, hslTuple[0]);
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1300
|
+
if (isValidHexColor(normalizedColor)) resolvedColors.push(normalizedColor);
|
|
1035
1301
|
}
|
|
1302
|
+
const backgroundHex = resolveOpaqueHex(CATALOG_POINTERS.background, wrapperRef.current) ?? "#ffffff";
|
|
1303
|
+
const labelColors = [CATALOG_POINTERS.label, CATALOG_POINTERS.labelInverse].map((pointer) => resolveOpaqueHex(pointer, wrapperRef.current)).filter((hex) => hex !== null);
|
|
1036
1304
|
setColorCache({
|
|
1037
1305
|
colors: resolvedColors,
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
maxHue
|
|
1306
|
+
background: backgroundHex,
|
|
1307
|
+
labelColors,
|
|
1308
|
+
colorAt: createPaletteGenerator(resolvedColors, backgroundHex, labelColors)
|
|
1042
1309
|
});
|
|
1043
1310
|
}, []);
|
|
1044
1311
|
useEffect(() => {
|
|
1045
1312
|
if (colorCache.colors.length > 0) setIsColorPaletteResolved(true);
|
|
1046
1313
|
}, [colorCache]);
|
|
1047
1314
|
const [groupToColorMap, setGroupToColorMap] = useState(() => /* @__PURE__ */ new Map());
|
|
1048
|
-
const paletteKey = colorCache.colors.join(",")
|
|
1315
|
+
const paletteKey = `${colorCache.colors.join(",")}|${colorCache.background}|${colorCache.labelColors.join(",")}`;
|
|
1049
1316
|
useEffect(() => {
|
|
1050
1317
|
setGroupToColorMap(/* @__PURE__ */ new Map());
|
|
1051
1318
|
}, [paletteKey]);
|
|
@@ -1068,11 +1335,11 @@ const GlobalChartsProvider = ({ children, theme, locale, timeZone }) => {
|
|
|
1068
1335
|
const existing = groupToColorMap.get(group);
|
|
1069
1336
|
if (existing) return existing;
|
|
1070
1337
|
const assignedCount = groupToColorMap.size;
|
|
1071
|
-
const color =
|
|
1338
|
+
const color = colorCache.colorAt(assignedCount);
|
|
1072
1339
|
groupToColorMap.set(group, color);
|
|
1073
1340
|
return color;
|
|
1074
1341
|
}
|
|
1075
|
-
return
|
|
1342
|
+
return colorCache.colorAt(index);
|
|
1076
1343
|
}, [colorCache, groupToColorMap]);
|
|
1077
1344
|
const getElementStyles = useCallback(({ data, index, overrideColor, legendShape }) => {
|
|
1078
1345
|
const isSeriesData = data && typeof data === "object" && "data" in data;
|
|
@@ -1238,39 +1505,6 @@ const useDeepMemo = (value) => {
|
|
|
1238
1505
|
return ref.current;
|
|
1239
1506
|
};
|
|
1240
1507
|
//#endregion
|
|
1241
|
-
//#region src/providers/chart-context/private/catalog-pointers.ts
|
|
1242
|
-
const CATALOG_POINTERS = {
|
|
1243
|
-
background: "var(--a8c-charts-color-background, #fff)",
|
|
1244
|
-
labelAxis: "var(--a8c-charts-color-label-axis, #1e1e1e)",
|
|
1245
|
-
grid: "var(--a8c-charts-color-grid, #dbdbdb)",
|
|
1246
|
-
axisX: "var(--a8c-charts-color-axis-x, #dbdbdb)",
|
|
1247
|
-
tickX: "var(--a8c-charts-color-tick-x, #dbdbdb)",
|
|
1248
|
-
axisY: "var(--a8c-charts-color-axis-y, none)",
|
|
1249
|
-
tickY: "var(--a8c-charts-color-tick-y, none)",
|
|
1250
|
-
annotation: "var(--a8c-charts-color-annotation, #1e1e1e)",
|
|
1251
|
-
surface: "var(--a8c-charts-color-surface, #fff)",
|
|
1252
|
-
surfaceSecondary: "var(--a8c-charts-color-surface-secondary, #f4f4f4)",
|
|
1253
|
-
trendUp: "var(--a8c-charts-color-trend-up, #008030)",
|
|
1254
|
-
trendDown: "var(--a8c-charts-color-trend-down, #cc1818)",
|
|
1255
|
-
series: SERIES_PALETTE_POINTERS
|
|
1256
|
-
};
|
|
1257
|
-
/**
|
|
1258
|
-
* The annotation parts visx paints, in the shape `@visx/annotation` takes. `radius`
|
|
1259
|
-
* rides along as the base the theme and the per-datum styles merge onto.
|
|
1260
|
-
*/
|
|
1261
|
-
const ANNOTATION_POINTERS = {
|
|
1262
|
-
label: {
|
|
1263
|
-
anchorLineStroke: CATALOG_POINTERS.annotation,
|
|
1264
|
-
backgroundFill: CATALOG_POINTERS.surface
|
|
1265
|
-
},
|
|
1266
|
-
connector: { stroke: CATALOG_POINTERS.annotation },
|
|
1267
|
-
circleSubject: {
|
|
1268
|
-
stroke: "transparent",
|
|
1269
|
-
fill: CATALOG_POINTERS.annotation,
|
|
1270
|
-
radius: 5
|
|
1271
|
-
}
|
|
1272
|
-
};
|
|
1273
|
-
//#endregion
|
|
1274
1508
|
//#region src/providers/chart-scope/use-chart-scope-element.ts
|
|
1275
1509
|
/**
|
|
1276
1510
|
* Returns the nearest chart scope element, or `null` when rendered outside any scope (and during SSR, before the ref is attached).
|
|
@@ -2748,18 +2982,41 @@ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating,
|
|
|
2748
2982
|
getChartRoot,
|
|
2749
2983
|
focusWithoutScrollIfNeeded
|
|
2750
2984
|
]);
|
|
2985
|
+
const onChartFocus = useCallback((event) => {
|
|
2986
|
+
if (event.currentTarget.contains(event.relatedTarget)) return;
|
|
2987
|
+
if (!isNavigating && selectedIndex !== void 0) setSelectedIndex(0);
|
|
2988
|
+
}, [
|
|
2989
|
+
isNavigating,
|
|
2990
|
+
selectedIndex,
|
|
2991
|
+
setSelectedIndex
|
|
2992
|
+
]);
|
|
2993
|
+
const pointerIndex = useRef(void 0);
|
|
2994
|
+
const onChartPointerMove = useCallback((index) => {
|
|
2995
|
+
if (selectedIndex === void 0) {
|
|
2996
|
+
if (pointerIndex.current !== void 0) pointerIndex.current = index;
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
const root = getChartRoot();
|
|
3000
|
+
const activeElement = root?.ownerDocument.activeElement;
|
|
3001
|
+
if (activeElement && root.contains(activeElement)) {
|
|
3002
|
+
pointerIndex.current = index;
|
|
3003
|
+
focusWithoutScrollIfNeeded(root);
|
|
3004
|
+
}
|
|
3005
|
+
setSelectedIndex(void 0);
|
|
3006
|
+
setIsNavigating(false);
|
|
3007
|
+
}, [
|
|
3008
|
+
selectedIndex,
|
|
3009
|
+
setSelectedIndex,
|
|
3010
|
+
setIsNavigating,
|
|
3011
|
+
getChartRoot,
|
|
3012
|
+
focusWithoutScrollIfNeeded
|
|
3013
|
+
]);
|
|
2751
3014
|
return {
|
|
2752
3015
|
tooltipRef,
|
|
2753
|
-
onChartFocus
|
|
2754
|
-
|
|
2755
|
-
if (!isNavigating && selectedIndex !== void 0) setSelectedIndex(0);
|
|
2756
|
-
}, [
|
|
2757
|
-
isNavigating,
|
|
2758
|
-
selectedIndex,
|
|
2759
|
-
setSelectedIndex
|
|
2760
|
-
]),
|
|
2761
|
-
onChartBlur: useCallback(() => {
|
|
3016
|
+
onChartFocus,
|
|
3017
|
+
onChartBlur: useCallback((event) => {
|
|
2762
3018
|
setIsNavigating(false);
|
|
3019
|
+
if (!event.currentTarget.contains(event.relatedTarget)) pointerIndex.current = void 0;
|
|
2763
3020
|
}, [setIsNavigating]),
|
|
2764
3021
|
onChartKeyDown: useCallback((event) => {
|
|
2765
3022
|
if (event.key === "Tab" || event.key === "Escape") {
|
|
@@ -2767,10 +3024,12 @@ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating,
|
|
|
2767
3024
|
focusWithoutScrollIfNeeded(getChartRoot());
|
|
2768
3025
|
setSelectedIndex(void 0);
|
|
2769
3026
|
setIsNavigating(false);
|
|
3027
|
+
pointerIndex.current = void 0;
|
|
2770
3028
|
return;
|
|
2771
3029
|
}
|
|
2772
3030
|
if (totalPoints === 0) return;
|
|
2773
|
-
const currentSelectedIndex = selectedIndex
|
|
3031
|
+
const currentSelectedIndex = selectedIndex ?? pointerIndex.current ?? -1;
|
|
3032
|
+
if (event.key === "ArrowRight" || event.key === "ArrowLeft") pointerIndex.current = void 0;
|
|
2774
3033
|
if (event.key === "ArrowRight") {
|
|
2775
3034
|
event.preventDefault();
|
|
2776
3035
|
setIsNavigating(true);
|
|
@@ -2791,7 +3050,8 @@ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating,
|
|
|
2791
3050
|
getChartRoot,
|
|
2792
3051
|
focusWithoutScrollIfNeeded,
|
|
2793
3052
|
onActivate
|
|
2794
|
-
])
|
|
3053
|
+
]),
|
|
3054
|
+
onChartPointerMove
|
|
2795
3055
|
};
|
|
2796
3056
|
};
|
|
2797
3057
|
//#endregion
|
|
@@ -2930,7 +3190,7 @@ function useChartChildren(children, chartType) {
|
|
|
2930
3190
|
var chart_layout_module_default = { "chart-layout__content": "a8ccharts-fpNVAq-chart-layout__content" };
|
|
2931
3191
|
//#endregion
|
|
2932
3192
|
//#region src/charts/private/chart-layout/chart-layout.tsx
|
|
2933
|
-
const ChartLayout = ({ legendPosition, legendElement, legendChildren, children, trailingContent, onContentHeightChange, gap, className, style, "data-testid": dataTestId, "data-chart-id": dataChartId }) => {
|
|
3193
|
+
const ChartLayout = ({ legendPosition, legendElement, legendChildren, children, trailingContent, onContentHeightChange, gap, className, rootRef, style, "data-testid": dataTestId, "data-chart-id": dataChartId }) => {
|
|
2934
3194
|
const [contentRef, contentWidth, contentHeight] = useElementSize();
|
|
2935
3195
|
const isRenderProp = typeof children === "function";
|
|
2936
3196
|
const isMeasured = contentHeight > 0;
|
|
@@ -2949,6 +3209,7 @@ const ChartLayout = ({ legendPosition, legendElement, legendChildren, children,
|
|
|
2949
3209
|
isMeasured
|
|
2950
3210
|
}) : children;
|
|
2951
3211
|
return /* @__PURE__ */ jsxs(Stack, {
|
|
3212
|
+
ref: rootRef,
|
|
2952
3213
|
direction: "column",
|
|
2953
3214
|
gap,
|
|
2954
3215
|
className,
|
|
@@ -5916,9 +6177,10 @@ function nearestBandIndex(data, accessor, scale, position) {
|
|
|
5916
6177
|
* @param root0.withTooltips - Whether pointer events update the tooltip.
|
|
5917
6178
|
* @param root0.onPointerDown - Receives the corrected pointer-down datum.
|
|
5918
6179
|
* @param root0.onPointerUp - Receives the corrected pointer-up datum.
|
|
6180
|
+
* @param root0.onPointerMove - Receives the corrected datum under a moving pointer.
|
|
5919
6181
|
* @return No visual content.
|
|
5920
6182
|
*/
|
|
5921
|
-
function BandTooltip({ keys, groupPadding, withTooltips, onPointerDown, onPointerUp }) {
|
|
6183
|
+
function BandTooltip({ keys, groupPadding, withTooltips, onPointerDown, onPointerUp, onPointerMove }) {
|
|
5922
6184
|
const { xScale, yScale, dataRegistry, horizontal } = useContext(DataContext);
|
|
5923
6185
|
const { showTooltip } = useContext(TooltipContext);
|
|
5924
6186
|
const scale = horizontal ? yScale : xScale;
|
|
@@ -5963,20 +6225,22 @@ function BandTooltip({ keys, groupPadding, withTooltips, onPointerDown, onPointe
|
|
|
5963
6225
|
]);
|
|
5964
6226
|
const handlePointer = useCallback((params) => {
|
|
5965
6227
|
const selections = getSelections(params);
|
|
5966
|
-
if (withTooltips && params?.event.type !== "pointerup") selections.forEach(showTooltip);
|
|
5967
|
-
const callback = params?.event.type === "pointerdown" ? onPointerDown : onPointerUp;
|
|
5968
|
-
if (params?.event.type === "pointermove" || !callback) return;
|
|
5969
6228
|
const nearest = selections.reduce((best, selection) => {
|
|
5970
6229
|
const distance = (value) => Math.hypot(value.distanceX, value.distanceY);
|
|
5971
6230
|
return !best || distance(selection) <= distance(best) ? selection : best;
|
|
5972
6231
|
}, void 0);
|
|
6232
|
+
if (params?.event.type === "pointermove" && nearest) onPointerMove?.(nearest);
|
|
6233
|
+
if (withTooltips && params?.event.type !== "pointerup") selections.forEach(showTooltip);
|
|
6234
|
+
const callback = params?.event.type === "pointerdown" ? onPointerDown : onPointerUp;
|
|
6235
|
+
if (params?.event.type === "pointermove" || !callback) return;
|
|
5973
6236
|
if (nearest) callback(nearest);
|
|
5974
6237
|
}, [
|
|
5975
6238
|
getSelections,
|
|
5976
6239
|
withTooltips,
|
|
5977
6240
|
showTooltip,
|
|
5978
6241
|
onPointerDown,
|
|
5979
|
-
onPointerUp
|
|
6242
|
+
onPointerUp,
|
|
6243
|
+
onPointerMove
|
|
5980
6244
|
]);
|
|
5981
6245
|
useEventEmitter("pointermove", withTooltips ? handlePointer : void 0);
|
|
5982
6246
|
useEventEmitter("pointerdown", handlePointer);
|
|
@@ -6059,7 +6323,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
|
|
|
6059
6323
|
});
|
|
6060
6324
|
}, [primaryEntries, onDatumActivate]);
|
|
6061
6325
|
const visibleSeriesKey = useMemo(() => JSON.stringify(primaryKeys), [primaryKeys]);
|
|
6062
|
-
const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation({
|
|
6326
|
+
const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown, onChartPointerMove } = useKeyboardNavigation({
|
|
6063
6327
|
selectedIndex,
|
|
6064
6328
|
setSelectedIndex,
|
|
6065
6329
|
isNavigating,
|
|
@@ -6069,6 +6333,10 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
|
|
|
6069
6333
|
onActivate: activateSelectedBar,
|
|
6070
6334
|
visibleSeriesKey
|
|
6071
6335
|
});
|
|
6336
|
+
const handlePointerMove = useCallback(({ key, index }) => {
|
|
6337
|
+
const seriesIndex = primaryKeys.indexOf(key);
|
|
6338
|
+
if (seriesIndex >= 0) onChartPointerMove(index * primaryKeys.length + seriesIndex);
|
|
6339
|
+
}, [primaryKeys, onChartPointerMove]);
|
|
6072
6340
|
const comparisonEntries = useMemo(() => {
|
|
6073
6341
|
const primaryByGroup = new Map(primaryEntries.map(({ series, index }) => [series.group, {
|
|
6074
6342
|
label: series.label,
|
|
@@ -6392,7 +6660,8 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
|
|
|
6392
6660
|
groupPadding,
|
|
6393
6661
|
withTooltips,
|
|
6394
6662
|
onPointerDown,
|
|
6395
|
-
onPointerUp
|
|
6663
|
+
onPointerUp,
|
|
6664
|
+
onPointerMove: handlePointerMove
|
|
6396
6665
|
}),
|
|
6397
6666
|
!allSeriesHidden && /* @__PURE__ */ jsx(WholeNumberTicks, {
|
|
6398
6667
|
...wholeNumberTicksProps,
|
|
@@ -7074,12 +7343,13 @@ function normalizeGeoChartError(eventArgs) {
|
|
|
7074
7343
|
* @param props.height - Height of the chart in pixels
|
|
7075
7344
|
* @param props.region - Region to display ('world', 'US', or ISO 3166-1 alpha-2 code)
|
|
7076
7345
|
* @param props.resolution - Resolution level ('countries', 'provinces', or 'metros')
|
|
7346
|
+
* @param props.domain - Country code whose viewpoint sets the disputed borders
|
|
7077
7347
|
* @param props.onError - Optional callback for Google Charts errors
|
|
7078
7348
|
* @param props.className - Additional CSS class name for the chart container
|
|
7079
7349
|
* @param props.renderPlaceholder - Optional render function for the loading placeholder
|
|
7080
7350
|
* @return A React component displaying an interactive map with data visualization
|
|
7081
7351
|
*/
|
|
7082
|
-
const GeoChartInternal = ({ className, data, width, height, region = "world", resolution = "countries", onError, renderPlaceholder }) => {
|
|
7352
|
+
const GeoChartInternal = ({ className, data, width, height, region = "world", resolution = "countries", domain, onError, renderPlaceholder }) => {
|
|
7083
7353
|
const { getElementStyles } = useGlobalChartsContext();
|
|
7084
7354
|
const scopeElement = useChartScopeElement();
|
|
7085
7355
|
const containerRef = useRef(null);
|
|
@@ -7143,6 +7413,7 @@ const GeoChartInternal = ({ className, data, width, height, region = "world", re
|
|
|
7143
7413
|
const options = useMemo(() => ({
|
|
7144
7414
|
...region !== "world" && { region },
|
|
7145
7415
|
...resolution !== "countries" && { resolution },
|
|
7416
|
+
...domain && { domain },
|
|
7146
7417
|
colorAxis: { colors: [lightColorHex, fullColorHex] },
|
|
7147
7418
|
backgroundColor: backgroundColorHex,
|
|
7148
7419
|
datalessRegionColor: defaultFillColorHex,
|
|
@@ -7156,6 +7427,7 @@ const GeoChartInternal = ({ className, data, width, height, region = "world", re
|
|
|
7156
7427
|
}), [
|
|
7157
7428
|
region,
|
|
7158
7429
|
resolution,
|
|
7430
|
+
domain,
|
|
7159
7431
|
lightColorHex,
|
|
7160
7432
|
fullColorHex,
|
|
7161
7433
|
backgroundColorHex,
|
|
@@ -11084,6 +11356,7 @@ function RadialWipeAnimation({ id, radius, innerRadius = 0, durationMs = 1e3, wi
|
|
|
11084
11356
|
var pie_chart_module_default = {
|
|
11085
11357
|
"pie-chart__label-plate": "a8ccharts-gnszbG-pie-chart__label-plate",
|
|
11086
11358
|
"pie-chart__label-text": "a8ccharts-gnszbG-pie-chart__label-text",
|
|
11359
|
+
"pie-chart__label-text--on-light": "a8ccharts-gnszbG-pie-chart__label-text--on-light",
|
|
11087
11360
|
"pie-chart__plot": "a8ccharts-gnszbG-pie-chart__plot",
|
|
11088
11361
|
"pie-chart--responsive": "a8ccharts-gnszbG-pie-chart--responsive"
|
|
11089
11362
|
};
|
|
@@ -11128,6 +11401,25 @@ const validateData$1 = (data) => {
|
|
|
11128
11401
|
};
|
|
11129
11402
|
};
|
|
11130
11403
|
/**
|
|
11404
|
+
* Whether a pie slice's label should use the dark `label` role instead of the default `label-inverse` role.
|
|
11405
|
+
*
|
|
11406
|
+
* A label plate always wins: the text sits on the plate, not the slice fill, so it never flips.
|
|
11407
|
+
* Otherwise the role that contrasts more with the resolved slice fill wins. `null` pointers (not
|
|
11408
|
+
* yet resolved, or labels off) keep the default inverse role.
|
|
11409
|
+
*
|
|
11410
|
+
* @param fill - The slice's resolved fill color.
|
|
11411
|
+
* @param pointers - The label pointers, resolved at the chart's own root element.
|
|
11412
|
+
* @return Whether the label needs dark text.
|
|
11413
|
+
*/
|
|
11414
|
+
const labelNeedsDarkText = (fill, pointers) => {
|
|
11415
|
+
if (!pointers || pointers.hasPlate) return false;
|
|
11416
|
+
const fillHex = normalizeColorToHex(fill);
|
|
11417
|
+
if (!isValidHexColor(fillHex) || !isValidHexColor(pointers.labelHex)) return false;
|
|
11418
|
+
if (pointers.isInverseSeeThrough) return true;
|
|
11419
|
+
if (!isValidHexColor(pointers.labelInverseHex)) return false;
|
|
11420
|
+
return contrastRatio(fillHex, pointers.labelHex) > contrastRatio(fillHex, pointers.labelInverseHex);
|
|
11421
|
+
};
|
|
11422
|
+
/**
|
|
11131
11423
|
* Renders a pie or donut chart using the provided data.
|
|
11132
11424
|
*
|
|
11133
11425
|
* @param {PieChartProps} props - Component props
|
|
@@ -11141,11 +11433,36 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
|
|
|
11141
11433
|
const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } = useTooltip();
|
|
11142
11434
|
const standaloneScopeClass = useStandaloneScopeClass();
|
|
11143
11435
|
const containerRef = useRef(null);
|
|
11436
|
+
const rootRef = useRef(null);
|
|
11437
|
+
const [labelPointers, setLabelPointers] = useState(null);
|
|
11144
11438
|
const onMouseLeave = useCallback(() => {
|
|
11145
11439
|
if (!withTooltips) return;
|
|
11146
11440
|
hideTooltip();
|
|
11147
11441
|
}, [withTooltips, hideTooltip]);
|
|
11148
|
-
const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
|
|
11442
|
+
const { getElementStyles, isSeriesVisible, isColorPaletteResolved } = useGlobalChartsContext();
|
|
11443
|
+
const { isValid, message } = validateData$1(data);
|
|
11444
|
+
useLayoutEffect(() => {
|
|
11445
|
+
if (!showLabels || !rootRef.current) return;
|
|
11446
|
+
const resolve = createCssVariableResolver(rootRef.current);
|
|
11447
|
+
const rawLabelBackground = resolve(CATALOG_POINTERS.labelBackground);
|
|
11448
|
+
const rawLabel = resolve(CATALOG_POINTERS.label);
|
|
11449
|
+
const isLabelOpaque = rawLabel ? color(rawLabel)?.opacity === 1 : false;
|
|
11450
|
+
const rawLabelInverse = resolve(CATALOG_POINTERS.labelInverse);
|
|
11451
|
+
const labelInverseColor = rawLabelInverse ? color(rawLabelInverse) : null;
|
|
11452
|
+
const plateColor = rawLabelBackground ? color(rawLabelBackground) : null;
|
|
11453
|
+
const next = {
|
|
11454
|
+
hasPlate: rawLabelBackground ? !plateColor || plateColor.opacity > 0 : false,
|
|
11455
|
+
labelHex: isLabelOpaque ? normalizeColorToHex(CATALOG_POINTERS.label, null, resolve) : "",
|
|
11456
|
+
labelInverseHex: normalizeColorToHex(CATALOG_POINTERS.labelInverse, null, resolve),
|
|
11457
|
+
isInverseSeeThrough: labelInverseColor ? labelInverseColor.opacity < 1 : false
|
|
11458
|
+
};
|
|
11459
|
+
setLabelPointers((previous) => previous && previous.hasPlate === next.hasPlate && previous.labelHex === next.labelHex && previous.labelInverseHex === next.labelInverseHex && previous.isInverseSeeThrough === next.isInverseSeeThrough ? previous : next);
|
|
11460
|
+
}, [
|
|
11461
|
+
showLabels,
|
|
11462
|
+
className,
|
|
11463
|
+
isColorPaletteResolved,
|
|
11464
|
+
isValid
|
|
11465
|
+
]);
|
|
11149
11466
|
const dataWithPercentages = useDataWithPercentages(data);
|
|
11150
11467
|
const { visibleData, allSegmentsHidden, legendData } = useLegendVisibilityData({
|
|
11151
11468
|
data: dataWithPercentages,
|
|
@@ -11156,7 +11473,6 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
|
|
|
11156
11473
|
showValues: true,
|
|
11157
11474
|
legendValueDisplay
|
|
11158
11475
|
}), [legendValueDisplay]));
|
|
11159
|
-
const { isValid, message } = validateData$1(data);
|
|
11160
11476
|
const { svgChildren, htmlChildren, legendChildren, otherChildren } = useChartChildren(children, "PieChart");
|
|
11161
11477
|
const chartMetadata = useMemo(() => ({
|
|
11162
11478
|
thickness,
|
|
@@ -11218,6 +11534,7 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
|
|
|
11218
11534
|
legendElement,
|
|
11219
11535
|
legendChildren,
|
|
11220
11536
|
gap,
|
|
11537
|
+
rootRef,
|
|
11221
11538
|
className: clsx("pie-chart", pie_chart_module_default["pie-chart"], { [pie_chart_module_default["pie-chart--responsive"]]: !propWidth && !propHeight }, className),
|
|
11222
11539
|
style: {
|
|
11223
11540
|
width: propWidth || void 0,
|
|
@@ -11279,9 +11596,10 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
|
|
|
11279
11596
|
tooltipTop: event.clientY - bounds.top + tooltipOffsetY
|
|
11280
11597
|
});
|
|
11281
11598
|
};
|
|
11599
|
+
const fill = accessors.fill(arc.data);
|
|
11282
11600
|
const pathProps = {
|
|
11283
11601
|
d: pie.path(arc) || "",
|
|
11284
|
-
fill
|
|
11602
|
+
fill,
|
|
11285
11603
|
"data-testid": "pie-segment"
|
|
11286
11604
|
};
|
|
11287
11605
|
const groupProps = {};
|
|
@@ -11309,7 +11627,7 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
|
|
|
11309
11627
|
ry: 4,
|
|
11310
11628
|
pointerEvents: "none"
|
|
11311
11629
|
}), /* @__PURE__ */ jsx("text", {
|
|
11312
|
-
className: pie_chart_module_default["pie-chart__label-text"],
|
|
11630
|
+
className: clsx(pie_chart_module_default["pie-chart__label-text"], { [pie_chart_module_default["pie-chart__label-text--on-light"]]: labelNeedsDarkText(fill, labelPointers) }),
|
|
11313
11631
|
x: centroidX,
|
|
11314
11632
|
y: centroidY,
|
|
11315
11633
|
dy: ".33em",
|