@automattic/charts 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/TOKENS.md +107 -48
  3. package/dist/index.cjs +584 -209
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +97 -43
  6. package/dist/index.d.cts +46 -41
  7. package/dist/index.d.ts +46 -41
  8. package/dist/index.js +582 -210
  9. package/dist/index.js.map +1 -1
  10. package/package.json +2 -2
  11. package/src/charts/area-chart/area-chart.module.scss +8 -3
  12. package/src/charts/area-chart/area-chart.tsx +3 -5
  13. package/src/charts/bar-chart/bar-chart.module.scss +14 -6
  14. package/src/charts/bar-chart/bar-chart.tsx +24 -15
  15. package/src/charts/bar-chart/private/use-bar-chart-options.ts +197 -30
  16. package/src/charts/bar-chart/test/bar-chart.test.tsx +685 -1
  17. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +17 -12
  18. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +107 -86
  19. package/src/charts/conversion-funnel-chart/test/chart-scope.test.tsx +36 -0
  20. package/src/charts/geo-chart/geo-chart.tsx +12 -4
  21. package/src/charts/heatmap-chart/heatmap-chart.module.scss +13 -9
  22. package/src/charts/heatmap-chart/heatmap-chart.tsx +12 -5
  23. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +13 -7
  24. package/src/charts/leaderboard-chart/types.ts +0 -2
  25. package/src/charts/line-chart/line-chart.module.scss +10 -5
  26. package/src/charts/line-chart/line-chart.tsx +3 -5
  27. package/src/charts/line-chart/private/line-chart-annotation.tsx +4 -4
  28. package/src/charts/pie-chart/pie-chart.tsx +4 -1
  29. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +4 -1
  30. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
  31. package/src/charts/private/test/time-axis.test.ts +247 -2
  32. package/src/charts/private/time-axis.ts +252 -63
  33. package/src/charts/private/with-responsive/test/with-responsive.test.tsx +16 -0
  34. package/src/charts/private/with-responsive/with-responsive.tsx +18 -11
  35. package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
  36. package/src/components/legend/private/base-legend.tsx +3 -1
  37. package/src/components/legend/test/legend.test.tsx +2 -1
  38. package/src/components/tooltip/accessible-tooltip.tsx +4 -2
  39. package/src/components/tooltip/base-tooltip.tsx +8 -1
  40. package/src/components/trend-indicator/test/trend-indicator.test.tsx +22 -1
  41. package/src/components/trend-indicator/trend-indicator.module.scss +3 -6
  42. package/src/components/trend-indicator/trend-indicator.tsx +3 -0
  43. package/src/hooks/test/use-chart-margin.test.tsx +60 -0
  44. package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +28 -0
  45. package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +89 -0
  46. package/src/hooks/test/use-xychart-theme.test.tsx +25 -0
  47. package/src/hooks/use-chart-margin.tsx +3 -4
  48. package/src/hooks/use-xychart-theme.ts +19 -11
  49. package/src/index.ts +9 -2
  50. package/src/providers/chart-context/global-charts-provider.tsx +35 -12
  51. package/src/providers/chart-context/private/theme-override-vars.ts +87 -0
  52. package/src/providers/chart-context/private/with-catalog-pointers.ts +50 -0
  53. package/src/providers/chart-context/test/chart-context.test.tsx +0 -40
  54. package/src/providers/chart-context/test/chart-scope-class.test.tsx +19 -0
  55. package/src/providers/chart-context/test/sparse-theme.test.ts +35 -0
  56. package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +29 -0
  57. package/src/providers/chart-context/test/theme-override-vars.test.ts +118 -0
  58. package/src/providers/chart-context/test/with-catalog-pointers.test.ts +47 -0
  59. package/src/providers/chart-context/themes.ts +18 -27
  60. package/src/providers/chart-context/types.ts +0 -3
  61. package/src/providers/chart-scope/chart-scope-context.ts +8 -0
  62. package/src/providers/chart-scope/index.ts +3 -0
  63. package/src/providers/chart-scope/test/chart-scope.test.tsx +24 -0
  64. package/src/providers/chart-scope/test/use-standalone-scope-class.test.tsx +20 -0
  65. package/src/providers/chart-scope/use-chart-scope-element.ts +9 -0
  66. package/src/providers/chart-scope/use-standalone-scope-class.ts +13 -0
  67. package/src/providers/index.ts +1 -0
  68. package/src/styles/chart-scope-class.ts +6 -0
  69. package/src/styles/chart-scope.scss +83 -0
  70. package/src/styles/test/chart-scope.test.ts +165 -0
  71. package/src/types.ts +6 -40
  72. package/src/utils/index.ts +1 -1
  73. package/src/utils/resolve-css-var.ts +69 -28
  74. package/src/utils/test/create-css-variable-resolver.test.ts +83 -0
  75. package/src/charts/private/grid-control/grid-control.module.scss +0 -8
  76. package/src/charts/private/grid-control/grid-control.tsx +0 -26
  77. package/src/charts/private/grid-control/index.ts +0 -1
  78. package/src/charts/private/grid-control/test/grid-control.test.tsx +0 -56
package/dist/index.js CHANGED
@@ -62,6 +62,14 @@ const useChartInstanceContext = () => {
62
62
  };
63
63
  const useSingleChartContext = useChartInstanceContext;
64
64
  //#endregion
65
+ //#region src/styles/chart-scope-class.ts
66
+ /**
67
+ * Stable class that carries the `--a8c-charts-*` catalog. Applied to the `GlobalChartsProvider` wrapper, to portal-rendered surfaces (which sit outside the provider tree), and to standalone components when no provider is above them.
68
+ *
69
+ * Deliberately unhashed: consumers are documented as being able to target it to override a token on the provider element.
70
+ */
71
+ const CHART_SCOPE_CLASS = "a8c-charts-scope";
72
+ //#endregion
65
73
  //#region src/utils/create-composition.ts
66
74
  /**
67
75
  * Utility function to create chart components with composition API.
@@ -508,15 +516,31 @@ const CSS_VAR_NAME_PATTERN = /^--[\w-]+$/;
508
516
  * @param element - Optional DOM element to resolve the variable from (defaults to document.documentElement)
509
517
  * @return The resolved value, fallback value, or null if unresolvable
510
518
  */
511
- const resolveCssVariable = (value, element) => {
512
- if (!value) return null;
513
- if (value.startsWith("var(") && value.endsWith(")")) {
514
- const parsed = parseVarExpression(value);
515
- if (parsed) return resolveVariableName(parsed.varName, element) || parsed.fallback;
516
- }
517
- if (value.startsWith("--")) return resolveVariableName(value, element);
518
- return value;
519
- };
519
+ const resolveCssVariable = (value, element) => createCssVariableResolver(element)(value);
520
+ /**
521
+ * Builds a resolver that reads every value from one `getComputedStyle` snapshot.
522
+ *
523
+ * `getComputedStyle` is the expensive half of resolving a token: each call can force the browser to flush pending style, and a chart theme resolves five roles at once. Taking the snapshot once per build turns that into a single query. The reads that share a snapshot happen synchronously inside one memo body, so no style change can land between them — the resolver is single-pass by design and is not meant to be held across renders.
524
+ *
525
+ * @param element - The element to resolve against, or null/undefined for the document root.
526
+ * @return A resolver that resolves each value exactly as `resolveCssVariable` does.
527
+ */
528
+ function createCssVariableResolver(element) {
529
+ let styles = null;
530
+ const getStyles = () => {
531
+ if (!styles) styles = computedStyleFor(element);
532
+ return styles;
533
+ };
534
+ return (value) => {
535
+ if (!value) return null;
536
+ if (value.startsWith("var(") && value.endsWith(")")) {
537
+ const parsed = parseVarExpression(value);
538
+ if (parsed) return readCustomProperty(parsed.varName, getStyles()) || parsed.fallback;
539
+ }
540
+ if (value.startsWith("--")) return readCustomProperty(value, getStyles());
541
+ return value;
542
+ };
543
+ }
520
544
  /**
521
545
  * Parses a var() expression into its variable name and optional fallback.
522
546
  * Uses string manipulation instead of complex regex to avoid ReDoS.
@@ -544,17 +568,30 @@ function parseVarExpression(expr) {
544
568
  };
545
569
  }
546
570
  /**
547
- * Resolves a plain CSS variable name to its computed value.
571
+ * Takes one computed-style snapshot, or null where there is nothing to read (SSR, or a detached element).
572
+ *
573
+ * @param element - The element to read from, or null/undefined for the document root.
574
+ * @return The computed styles, or null.
575
+ */
576
+ function computedStyleFor(element) {
577
+ if (typeof window === "undefined" || typeof document === "undefined") return null;
578
+ try {
579
+ return getComputedStyle(element || document.documentElement);
580
+ } catch {
581
+ return null;
582
+ }
583
+ }
584
+ /**
585
+ * Reads one custom property from a computed-style snapshot.
548
586
  *
549
587
  * @param varName - A CSS variable name like '--my-color'
550
- * @param element - Optional DOM element to resolve from
588
+ * @param styles - The snapshot to read from, or null
551
589
  * @return The computed value or null
552
590
  */
553
- function resolveVariableName(varName, element) {
554
- if (typeof window === "undefined" || typeof document === "undefined") return null;
591
+ function readCustomProperty(varName, styles) {
592
+ if (!styles) return null;
555
593
  try {
556
- const targetElement = element || document.documentElement;
557
- return getComputedStyle(targetElement).getPropertyValue(varName).trim() || null;
594
+ return styles.getPropertyValue(varName).trim() || null;
558
595
  } catch {
559
596
  return null;
560
597
  }
@@ -589,6 +626,14 @@ const resolveFontSize = (val) => {
589
626
  }
590
627
  };
591
628
  //#endregion
629
+ //#region src/providers/chart-scope/chart-scope-context.ts
630
+ /**
631
+ * The nearest chart element, published so JS token resolution reads the same CSS cascade that paints the chart's own colours.
632
+ *
633
+ * 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.
634
+ */
635
+ const ChartScopeContext = createContext(null);
636
+ //#endregion
592
637
  //#region src/providers/chart-context/private/get-chart-color.ts
593
638
  /**
594
639
  * Golden ratio for mathematically pleasing color distribution
@@ -718,12 +763,75 @@ const getChartColor = (index, colorCache) => {
718
763
  return hsl(Math.round(fallbackHue), fallbackSaturation / 100, fallbackLightness / 100).formatHex();
719
764
  };
720
765
  //#endregion
766
+ //#region src/providers/chart-context/private/theme-override-vars.ts
767
+ const ROLE_FOR_FIELD = [
768
+ ["--a8c-charts-color-background", (theme) => theme.backgroundColor],
769
+ ["--a8c-charts-color-grid", (theme) => theme.gridStyles?.stroke],
770
+ ["--a8c-charts-color-axis", (theme) => theme.xAxisLineStyles?.stroke],
771
+ ["--a8c-charts-color-tick", (theme) => theme.xTickLineStyles?.stroke],
772
+ ["--a8c-charts-color-label-axis", (theme) => theme.svgLabelSmall?.fill]
773
+ ];
774
+ /**
775
+ * The variable a `theme` prop override is published as, one layer outside the role itself.
776
+ *
777
+ * `chart-scope.scss` declares each of these roles as `var(<role>-theme, <catalog default>)`, so an override that is invalid at computed-value time — a fallback-less `var()` naming a token the host never set — only invalidates this variable, and the role still resolves its mapped `--wpds-*` token. Published as the role directly, such a value took the role and every bare `var(<role>)` read site down with it.
778
+ *
779
+ * @param role - The catalog role being overridden.
780
+ * @return The custom property carrying the consumer's value.
781
+ */
782
+ const themeLayerVar = (role) => `${role}-theme`;
783
+ ROLE_FOR_FIELD.map(([role]) => role);
784
+ /**
785
+ * Whether a value reads the role it is being published for, which would make the catalog entry depend on itself.
786
+ *
787
+ * The dependency runs `<role>: var(<role>-theme, …)`, so a value naming `<role>` closes a cycle through the catalog entry. CSS marks every custom property in a cycle invalid at computed-value time — the role's own fallback is *not* used, so the token resolves to nothing and every chart loses that colour. Verified in Chrome: `--role-theme: var(--role, blue); --role: var(--role-theme, green)` leaves `--role` empty, not `green`.
788
+ *
789
+ * Three things this has to get right, none of which a plain substring test gets:
790
+ *
791
+ * `var()` permits whitespace after the opening paren, so testing for `var(<role>` misses `var( --a8c-charts-color-grid, … )`.
792
+ *
793
+ * Several roles are prefixes of others (`--a8c-charts-color-label` of `--a8c-charts-color-label-secondary`), and every role is a prefix of its own theme layer. A prefix match would read a legitimate cross-role pointer as a self-reference and silently drop the override, so the role must be followed by something that cannot continue an identifier — `-` included. A value naming `<role>-theme` is therefore not treated as a self-reference: that cycle is confined to the theme layer, which leaves the role free to fall back to the catalog default.
794
+ *
795
+ * `resolveCssVariable` also accepts a bare custom-property name with no `var()` wrapper (`--a8c-charts-color-grid`), so that is legal `theme` input too. It forms no cycle — custom properties accept arbitrary token streams — so it survives as a literal string and drops silently at the use site instead of falling back to the catalog default.
796
+ *
797
+ * @param value - The consumer's value for the field.
798
+ * @param role - The catalog role this value would override.
799
+ * @return True when the value reads `role`, so it must not be published.
800
+ */
801
+ const readsOwnRole = (value, role) => value.trim() === role || new RegExp(`var\\(\\s*${role.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`).test(value);
802
+ /**
803
+ * Maps a sparse consumer theme onto the theme-layer variables its overridden catalog roles read, so CSS-painted and JS-resolved colours read one source.
804
+ *
805
+ * A value that reads the role it would override is left unpublished — that is the default theme's own pointer surviving `mergeThemes`, and publishing it would invalidate the role. The role is still reported in `roles`: `withCatalogPointers` restores its theme field to the catalog pointer either way, so CSS and visx agree on the catalog default rather than visx painting a literal CSS never sees.
806
+ *
807
+ * @param theme - The consumer's `theme` prop, before merging with the default theme.
808
+ * @return The wrapper's custom properties, and the roles the consumer overrode.
809
+ */
810
+ const themeOverrideVars = (theme) => {
811
+ if (!theme) return {
812
+ vars: {},
813
+ roles: []
814
+ };
815
+ const vars = {};
816
+ const roles = [];
817
+ for (const [role, read] of ROLE_FOR_FIELD) {
818
+ const value = read(theme);
819
+ if (typeof value !== "string" || value === "") continue;
820
+ roles.push(role);
821
+ if (!readsOwnRole(value, role)) vars[themeLayerVar(role)] = value;
822
+ }
823
+ return {
824
+ vars,
825
+ roles
826
+ };
827
+ };
828
+ //#endregion
721
829
  //#region src/providers/chart-context/themes.ts
722
830
  /**
723
831
  * Default theme configuration
724
832
  */
725
833
  const defaultTheme = {
726
- backgroundColor: "var(--a8c-charts-color-background, var(--wpds-color-background-surface-neutral-strong, #fff))",
834
+ backgroundColor: "var(--a8c-charts-color-background, #fff)",
727
835
  labelBackgroundColor: "transparent",
728
836
  labelTextColor: "var(--a8c-charts-color-label-on-fill, #FFFFFF)",
729
837
  colors: [
@@ -734,59 +842,59 @@ const defaultTheme = {
734
842
  "#FF8C8F"
735
843
  ],
736
844
  gridStyles: {
737
- stroke: "var(--a8c-charts-color-grid, var(--wpds-color-stroke-surface-neutral, #dbdbdb))",
845
+ stroke: "var(--a8c-charts-color-grid, #dbdbdb)",
738
846
  strokeWidth: 1
739
847
  },
740
848
  tickLength: 4,
741
849
  gridColor: "",
742
850
  gridColorDark: "",
743
851
  xTickLineStyles: {
744
- stroke: "var(--a8c-charts-color-tick, var(--wpds-color-stroke-surface-neutral, #dbdbdb))",
852
+ stroke: "var(--a8c-charts-color-tick, #dbdbdb)",
745
853
  strokeWidth: 1
746
854
  },
747
855
  xAxisLineStyles: {
748
- stroke: "var(--a8c-charts-color-axis, var(--wpds-color-stroke-surface-neutral, #dbdbdb))",
856
+ stroke: "var(--a8c-charts-color-axis, #dbdbdb)",
749
857
  strokeWidth: 1
750
858
  },
751
859
  legend: {
752
- labelStyles: { color: "var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e))" },
860
+ labelStyles: { color: "var(--a8c-charts-color-label, #1e1e1e)" },
753
861
  containerStyles: {},
754
862
  shapeStyles: []
755
863
  },
756
864
  seriesLineStyles: [],
757
865
  glyphs: [],
758
866
  svgLabelSmall: {
759
- fill: "var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e))",
867
+ fill: "var(--a8c-charts-color-label-axis, #1e1e1e)",
760
868
  fontFamily: "inherit"
761
869
  },
762
870
  svgLabelBig: { fontFamily: "inherit" },
763
871
  annotationStyles: {
764
872
  label: {
765
- anchorLineStroke: "var(--a8c-charts-color-annotation, var(--wpds-color-foreground-content-neutral, #1e1e1e))",
766
- backgroundFill: "var(--a8c-charts-color-background, var(--wpds-color-background-surface-neutral-strong, #fff))"
873
+ anchorLineStroke: "var(--a8c-charts-color-annotation, #1e1e1e)",
874
+ backgroundFill: "var(--a8c-charts-color-surface, #fff)"
767
875
  },
768
- connector: { stroke: "var(--a8c-charts-color-annotation, var(--wpds-color-foreground-content-neutral, #1e1e1e))" },
876
+ connector: { stroke: "var(--a8c-charts-color-annotation, #1e1e1e)" },
769
877
  circleSubject: {
770
878
  stroke: "transparent",
771
- fill: "var(--a8c-charts-color-annotation, var(--wpds-color-foreground-content-neutral, #1e1e1e))",
879
+ fill: "var(--a8c-charts-color-annotation, #1e1e1e)",
772
880
  radius: 5
773
881
  }
774
882
  },
775
- geoChart: { featureFillColor: "var(--a8c-charts-color-surface-secondary, var(--wpds-color-background-surface-neutral-weak, #f4f4f4))" },
883
+ geoChart: { featureFillColor: "var(--a8c-charts-color-surface-secondary, #f4f4f4)" },
776
884
  leaderboardChart: {
777
885
  rowGap: 12,
778
886
  columnGap: 4,
779
887
  labelSpacing: "xs",
780
888
  deltaColors: [
781
- "var(--a8c-charts-color-trend-down, var(--wpds-color-foreground-content-error-weak, #cc1818))",
782
- "var(--a8c-charts-color-trend-neutral, var(--wpds-color-foreground-content-neutral-weak, #707070))",
783
- "var(--a8c-charts-color-trend-up, var(--wpds-color-foreground-content-success-weak, #008030))"
889
+ "var(--a8c-charts-color-trend-down, #cc1818)",
890
+ "var(--a8c-charts-color-trend-neutral, #707070)",
891
+ "var(--a8c-charts-color-trend-up, #008030)"
784
892
  ]
785
893
  },
786
894
  conversionFunnelChart: {
787
- backgroundColor: "var(--a8c-charts-color-surface-secondary, var(--wpds-color-background-surface-neutral-weak, #f4f4f4))",
788
- positiveChangeColor: "var(--a8c-charts-color-trend-up, var(--wpds-color-foreground-content-success-weak, #008030))",
789
- negativeChangeColor: "var(--a8c-charts-color-trend-down, var(--wpds-color-foreground-content-error-weak, #cc1818))"
895
+ backgroundColor: "var(--a8c-charts-color-surface-secondary, #f4f4f4)",
896
+ positiveChangeColor: "var(--a8c-charts-color-trend-up, #008030)",
897
+ negativeChangeColor: "var(--a8c-charts-color-trend-down, #cc1818)"
790
898
  },
791
899
  lineChart: { lineStyles: { comparison: {
792
900
  strokeDasharray: "4 4",
@@ -811,15 +919,65 @@ const defaultTheme = {
811
919
  }
812
920
  };
813
921
  //#endregion
922
+ //#region src/providers/chart-context/private/with-catalog-pointers.ts
923
+ const CATALOG_RESTORE_FOR_ROLE = {
924
+ "--a8c-charts-color-background": () => ({ backgroundColor: defaultTheme.backgroundColor }),
925
+ "--a8c-charts-color-grid": (theme) => ({ gridStyles: {
926
+ ...theme.gridStyles,
927
+ stroke: defaultTheme.gridStyles.stroke
928
+ } }),
929
+ "--a8c-charts-color-axis": (theme) => ({ xAxisLineStyles: {
930
+ ...theme.xAxisLineStyles,
931
+ stroke: defaultTheme.xAxisLineStyles.stroke
932
+ } }),
933
+ "--a8c-charts-color-tick": (theme) => ({ xTickLineStyles: {
934
+ ...theme.xTickLineStyles,
935
+ stroke: defaultTheme.xTickLineStyles.stroke
936
+ } }),
937
+ "--a8c-charts-color-label-axis": (theme) => ({ svgLabelSmall: {
938
+ ...theme.svgLabelSmall,
939
+ fill: defaultTheme.svgLabelSmall.fill
940
+ } })
941
+ };
942
+ /**
943
+ * Restores the mapped theme fields of `overriddenRoles` to their catalog pointer, so the theme-layer variable `themeOverrideVars` writes on the provider wrapper is the only carrier for an overridden role — CSS and the JS bridge (`useXYChartTheme`) then resolve it through the same cascade instead of visx reading a baked literal that can disagree with a closer CSS override.
944
+ *
945
+ * This covers roles `themeOverrideVars` deliberately left unpublished as well as the ones it published. A value that reads its own role is not publishable, but leaving visx the consumer's literal is what the whole mechanism exists to prevent: `theme={ { gridStyles: { stroke: 'var(--brand, var(--a8c-charts-color-grid, red))' } } }` would have CSS paint the catalog default while visx painted `--brand`.
946
+ *
947
+ * Every field outside the five mapped roles is left exactly as `merged` provided it: those aren't published as theme-layer vars, so rewriting them would erase a consumer's override with no replacement carrier.
948
+ *
949
+ * @param merged - The consumer theme merged onto `defaultTheme` (`mergeThemes` output).
950
+ * @param overriddenRoles - The catalog roles the consumer overrode, from `themeOverrideVars`.
951
+ * @return A new theme object; `merged` is not mutated.
952
+ */
953
+ const withCatalogPointers = (merged, overriddenRoles) => {
954
+ let result = merged;
955
+ for (const role of overriddenRoles) {
956
+ const restore = CATALOG_RESTORE_FOR_ROLE[role];
957
+ if (restore) result = {
958
+ ...result,
959
+ ...restore(result)
960
+ };
961
+ }
962
+ return result;
963
+ };
964
+ //#endregion
814
965
  //#region src/providers/chart-context/global-charts-provider.tsx
815
966
  const GlobalChartsContext = createContext(null);
816
967
  const GlobalChartsProvider = ({ children, theme }) => {
817
968
  const [charts, setCharts] = useState(() => /* @__PURE__ */ new Map());
818
969
  const [hiddenSeries, setHiddenSeries] = useState(() => /* @__PURE__ */ new Map());
819
970
  const wrapperRef = useRef(null);
971
+ const [scopeNode, setScopeNode] = useState(null);
972
+ const setWrapperNode = useCallback((node) => {
973
+ wrapperRef.current = node;
974
+ setScopeNode(node);
975
+ }, []);
976
+ const { vars: overrideVars, roles: overriddenRoles } = useMemo(() => themeOverrideVars(theme), [theme]);
820
977
  const providerTheme = useMemo(() => {
821
- return theme ? mergeThemes(defaultTheme, theme) : defaultTheme;
822
- }, [theme]);
978
+ if (!theme) return defaultTheme;
979
+ return withCatalogPointers(mergeThemes(defaultTheme, theme), overriddenRoles);
980
+ }, [theme, overriddenRoles]);
823
981
  const [colorCache, setColorCache] = useState(() => ({
824
982
  colors: [],
825
983
  hues: [],
@@ -896,7 +1054,6 @@ const GlobalChartsProvider = ({ children, theme }) => {
896
1054
  }
897
1055
  return getChartColor(index, colorCache);
898
1056
  }, [colorCache, groupToColorMap]);
899
- const resolveThemeColor = useCallback((value) => value ? normalizeColorToHex(value, wrapperRef.current, resolveCssVariable) : "", []);
900
1057
  const getElementStyles = useCallback(({ data, index, overrideColor, legendShape }) => {
901
1058
  const isSeriesData = data && typeof data === "object" && "data" in data && "options" in data;
902
1059
  const isPointPercentageData = data && typeof data === "object" && "value" in data && typeof data.value === "number" && !("data" in data);
@@ -939,7 +1096,6 @@ const GlobalChartsProvider = ({ children, theme }) => {
939
1096
  getChartData,
940
1097
  theme: providerTheme,
941
1098
  getElementStyles,
942
- resolveThemeColor,
943
1099
  toggleSeriesVisibility,
944
1100
  isSeriesVisible,
945
1101
  getHiddenSeries,
@@ -951,7 +1107,6 @@ const GlobalChartsProvider = ({ children, theme }) => {
951
1107
  getChartData,
952
1108
  providerTheme,
953
1109
  getElementStyles,
954
- resolveThemeColor,
955
1110
  toggleSeriesVisibility,
956
1111
  isSeriesVisible,
957
1112
  getHiddenSeries,
@@ -960,9 +1115,16 @@ const GlobalChartsProvider = ({ children, theme }) => {
960
1115
  return /* @__PURE__ */ jsx(GlobalChartsContext.Provider, {
961
1116
  value,
962
1117
  children: /* @__PURE__ */ jsx("div", {
963
- ref: wrapperRef,
964
- style: { display: "contents" },
965
- children
1118
+ ref: setWrapperNode,
1119
+ className: CHART_SCOPE_CLASS,
1120
+ style: {
1121
+ display: "contents",
1122
+ ...overrideVars
1123
+ },
1124
+ children: /* @__PURE__ */ jsx(ChartScopeContext.Provider, {
1125
+ value: scopeNode,
1126
+ children
1127
+ })
966
1128
  })
967
1129
  });
968
1130
  };
@@ -1022,12 +1184,33 @@ const useDeepMemo = (value) => {
1022
1184
  return ref.current;
1023
1185
  };
1024
1186
  //#endregion
1187
+ //#region src/providers/chart-scope/use-chart-scope-element.ts
1188
+ /**
1189
+ * Returns the nearest chart scope element, or `null` when rendered outside any scope (and during SSR, before the ref is attached).
1190
+ *
1191
+ * @return {HTMLElement | null} The nearest chart scope element, or `null`.
1192
+ */
1193
+ const useChartScopeElement = () => useContext(ChartScopeContext);
1194
+ //#endregion
1195
+ //#region src/providers/chart-scope/use-standalone-scope-class.ts
1196
+ /**
1197
+ * Returns the catalog class when there is no `GlobalChartsProvider` above this component, and `undefined` when there is.
1198
+ *
1199
+ * Standalone components need the catalog to resolve at all, but inside a provider the tokens already inherit — re-declaring them there would shadow a consumer override set anywhere in the tree.
1200
+ *
1201
+ * @return The scope class name, or undefined when a provider is present.
1202
+ */
1203
+ const useStandaloneScopeClass = () => useContext(GlobalChartsContext) ? void 0 : CHART_SCOPE_CLASS;
1204
+ //#endregion
1025
1205
  //#region src/hooks/use-xychart-theme.ts
1026
- const resolveColor = (value) => value ? resolveCssVariable(value) ?? value : value;
1027
1206
  const useXYChartTheme = (data) => {
1028
1207
  const theme = useGlobalChartsTheme();
1208
+ const scopeElement = useChartScopeElement();
1209
+ const seriesColorKey = JSON.stringify((data ?? []).map((series) => series.options?.stroke).filter((color) => Boolean(color)));
1029
1210
  return useMemo(() => {
1030
- const seriesColors = (data ?? []).map((series) => series.options?.stroke).filter((color) => Boolean(color));
1211
+ const resolve = createCssVariableResolver(scopeElement);
1212
+ const resolveColor = (value) => value ? resolve(value) ?? value : value;
1213
+ const seriesColors = JSON.parse(seriesColorKey);
1031
1214
  return buildChartTheme({
1032
1215
  ...theme,
1033
1216
  colors: [...seriesColors, ...theme.colors ?? []],
@@ -1049,7 +1232,11 @@ const useXYChartTheme = (data) => {
1049
1232
  fill: resolveColor(theme.svgLabelSmall.fill)
1050
1233
  }
1051
1234
  });
1052
- }, [theme, data]);
1235
+ }, [
1236
+ theme,
1237
+ seriesColorKey,
1238
+ scopeElement
1239
+ ]);
1053
1240
  };
1054
1241
  //#endregion
1055
1242
  //#region src/hooks/use-chart-data-transform.ts
@@ -1136,7 +1323,7 @@ const getXAxisLabelMetrics = (theme, orientation) => {
1136
1323
  const useChartMargin = (height, options, data, theme, horizontal = false) => {
1137
1324
  const yTicks = useMemo(() => {
1138
1325
  const allDataPoints = data.flatMap((series) => series.data);
1139
- if (horizontal) return allDataPoints.map((d) => d.label || options.axis?.y?.tickFormat(d.date.getTime(), 0, []));
1326
+ if (horizontal) return allDataPoints.map((d) => d.label || d.date?.getTime());
1140
1327
  if (options.axis?.y?.tickValues?.length) return options.axis.y.tickValues;
1141
1328
  const minY = Math.min(...allDataPoints.map((d) => d.value));
1142
1329
  const maxY = Math.max(...allDataPoints.map((d) => d.value));
@@ -1525,6 +1712,7 @@ const BaseLegend = forwardRef(({ items, className, orientation = "horizontal", a
1525
1712
  const { width: shapeWidth = 16, height: shapeHeight = 16, margin: shapeMargin = "2px 4px 2px 0" } = shapeStyles ?? {};
1526
1713
  const theme = useGlobalChartsTheme();
1527
1714
  const context = useContext(GlobalChartsContext);
1715
+ const standaloneScopeClass = useStandaloneScopeClass();
1528
1716
  const legendScale = scaleOrdinal({
1529
1717
  domain: items.map((item) => item.label),
1530
1718
  range: items.map((item) => item.color)
@@ -1577,7 +1765,7 @@ const BaseLegend = forwardRef(({ items, className, orientation = "horizontal", a
1577
1765
  justify: orientation === "horizontal" ? flexAlignment : void 0,
1578
1766
  wrap: orientation === "horizontal" ? "wrap" : void 0,
1579
1767
  role: "list",
1580
- className: clsx(base_legend_module_default.legend, className),
1768
+ className: clsx(standaloneScopeClass, base_legend_module_default.legend, className),
1581
1769
  style: theme.legend?.containerStyles,
1582
1770
  children: labels.map((label, i) => {
1583
1771
  const matchedItem = items[i];
@@ -1864,9 +2052,10 @@ const BaseTooltip = ({ data, top, left, component: Component = DefaultTooltipCon
1864
2052
  data,
1865
2053
  className
1866
2054
  });
2055
+ const standaloneScopeClass = useStandaloneScopeClass();
1867
2056
  if (!renderContainer) return content;
1868
2057
  return /* @__PURE__ */ jsx("div", {
1869
- className: base_tooltip_module_default.tooltip,
2058
+ className: clsx(standaloneScopeClass, base_tooltip_module_default.tooltip),
1870
2059
  style: {
1871
2060
  top,
1872
2061
  left,
@@ -1935,10 +2124,11 @@ const AccessibleTooltip = ({ renderTooltip, selectedIndex, tooltipRef, keyboardF
1935
2124
  tabIndex: -1,
1936
2125
  role: "tooltip",
1937
2126
  "aria-atomic": "true",
1938
- className: keyboardFocusedClassName,
2127
+ className: clsx(CHART_SCOPE_CLASS, keyboardFocusedClassName),
1939
2128
  children: tooltipContent
1940
2129
  }, `chart-tooltip-${selectedIndex}`);
1941
2130
  return /* @__PURE__ */ jsx("div", {
2131
+ className: CHART_SCOPE_CLASS,
1942
2132
  role: "tooltip",
1943
2133
  "aria-live": "polite",
1944
2134
  children: tooltipContent
@@ -2231,28 +2421,38 @@ const getCurveType = (type, smoothing) => {
2231
2421
  default: return curveLinear;
2232
2422
  }
2233
2423
  };
2234
- const formatYearTick = (timestamp) => {
2235
- return new Date(timestamp).toLocaleDateString(void 0, { year: "numeric" });
2236
- };
2237
- const formatDateTick$1 = (timestamp) => {
2238
- return new Date(timestamp).toLocaleDateString(void 0, {
2239
- month: "short",
2240
- day: "numeric"
2241
- });
2242
- };
2243
- const formatHourTick = (timestamp) => {
2244
- return new Date(timestamp).toLocaleTimeString(void 0, {
2245
- hour: "numeric",
2246
- hour12: true
2247
- });
2248
- };
2249
- const formatDateOrHourTick = (timestamp) => {
2250
- const date = new Date(timestamp);
2251
- return date.getHours() === 0 && date.getMinutes() === 0 ? formatDateTick$1(timestamp) : formatHourTick(timestamp);
2424
+ const dateTimeFormat = (options) => {
2425
+ let formatter;
2426
+ return (timestamp) => {
2427
+ if (!Number.isFinite(Number(timestamp))) return new Date(timestamp).toLocaleDateString();
2428
+ formatter = formatter ?? new Intl.DateTimeFormat(void 0, options);
2429
+ return formatter.format(timestamp);
2430
+ };
2252
2431
  };
2253
- const formatMonthOrYearTick = (timestamp) => {
2254
- const date = new Date(timestamp);
2255
- return date.getMonth() === 0 ? formatYearTick(timestamp) : date.toLocaleDateString(void 0, { month: "short" });
2432
+ const formatYearTick = dateTimeFormat({ year: "numeric" });
2433
+ const formatDateTick = dateTimeFormat({
2434
+ month: "short",
2435
+ day: "numeric"
2436
+ });
2437
+ const formatHourTick = dateTimeFormat({
2438
+ hour: "numeric",
2439
+ hour12: true
2440
+ });
2441
+ const formatMonthTick = dateTimeFormat({ month: "short" });
2442
+ const boundaryFormat = (isAnchor, atBoundary, between) => {
2443
+ const format = (timestamp) => isAnchor(new Date(timestamp)) ? atBoundary(timestamp) : between(timestamp);
2444
+ format.isAnchor = isAnchor;
2445
+ return format;
2446
+ };
2447
+ const formatDateOrHourTick = boundaryFormat((date) => date.getHours() === 0 && date.getMinutes() === 0, formatDateTick, formatHourTick);
2448
+ const formatMonthOrYearTick = boundaryFormat((date) => date.getMonth() === 0, formatYearTick, formatMonthTick);
2449
+ const getSpan = (sortedData) => {
2450
+ const bounds = sortedData.map((datom) => [datom.data.at(0)?.date, datom.data.at(-1)?.date]).filter(([first, last]) => first !== void 0 && last !== void 0);
2451
+ if (!bounds.length) return null;
2452
+ return {
2453
+ minX: Math.min(...bounds.map(([first]) => Number(first))),
2454
+ maxX: Math.max(...bounds.map(([, last]) => Number(last)))
2455
+ };
2256
2456
  };
2257
2457
  const getPointSpacingInHours = (sortedData) => {
2258
2458
  return sortedData.reduce((spacing, datom) => datom.data.reduce((seriesSpacing, point, index) => {
@@ -2261,26 +2461,90 @@ const getPointSpacingInHours = (sortedData) => {
2261
2461
  return Math.min(seriesSpacing, Math.abs(differenceInHours(point.date, previous.date)));
2262
2462
  }, spacing), Number.POSITIVE_INFINITY);
2263
2463
  };
2264
- const SPACING_BY_RESOLUTION = {
2265
- hour: 1,
2266
- day: 24,
2267
- week: 168,
2268
- month: 672
2464
+ const SUB_DAILY_SPACING_HOURS = 23;
2465
+ const MONTHLY_SPACING_HOURS = 672;
2466
+ const getBucketResolution = (sortedData, tickResolution) => {
2467
+ if (tickResolution) return tickResolution === "week" ? "day" : tickResolution;
2468
+ const spacingInHours = getPointSpacingInHours(sortedData);
2469
+ if (spacingInHours < SUB_DAILY_SPACING_HOURS) return "hour";
2470
+ if (!Number.isFinite(spacingInHours) || spacingInHours < MONTHLY_SPACING_HOURS) return "day";
2471
+ return spacingInHours < 12 * MONTHLY_SPACING_HOURS ? "month" : "year";
2269
2472
  };
2270
2473
  const getFormatter = (sortedData, tickResolution) => {
2271
- if (tickResolution === "year") return formatYearTick;
2272
- const minX = Math.min(...sortedData.map((datom) => datom.data.at(0)?.date));
2273
- const maxX = Math.max(...sortedData.map((datom) => datom.data.at(-1)?.date));
2274
- const spacingInHours = tickResolution ? SPACING_BY_RESOLUTION[tickResolution] : getPointSpacingInHours(sortedData);
2275
- const isSubDaily = spacingInHours < 23;
2276
- const diffInHours = Math.abs(differenceInHours(maxX, minX));
2277
- if (diffInHours <= 24 && isSubDaily) return formatHourTick;
2278
- if (diffInHours <= 168 && isSubDaily) return formatDateOrHourTick;
2279
- if (Math.abs(differenceInYears(maxX, minX)) <= 1) return Number.isFinite(spacingInHours) && spacingInHours >= 672 ? formatMonthOrYearTick : formatDateTick$1;
2280
- return formatYearTick;
2474
+ const resolution = getBucketResolution(sortedData, tickResolution);
2475
+ if (resolution === "year") return formatYearTick;
2476
+ if (resolution === "month") return formatMonthOrYearTick;
2477
+ const span = getSpan(sortedData);
2478
+ if (!span) return formatDateTick;
2479
+ const diffInHours = Math.abs(differenceInHours(span.maxX, span.minX));
2480
+ if (resolution === "hour") {
2481
+ if (diffInHours <= 24) return formatHourTick;
2482
+ if (diffInHours <= 168) return formatDateOrHourTick;
2483
+ }
2484
+ return Math.abs(differenceInYears(span.maxX, span.minX)) <= 1 ? formatDateTick : formatYearTick;
2485
+ };
2486
+ const getAnchorIndices = (domain, isAnchor) => domain.reduce((indices, date, index) => {
2487
+ if (isAnchor(date)) indices.push(index);
2488
+ return indices;
2489
+ }, []);
2490
+ const getCandidateSteps = (length, maxTicks, anchorSpacing) => {
2491
+ const steps = /* @__PURE__ */ new Set();
2492
+ for (let count = maxTicks; count > 1; count--) {
2493
+ steps.add(Math.max(1, Math.floor((length - 1) / (count - 1))));
2494
+ steps.add(Math.max(1, Math.ceil((length - 1) / (count - 1))));
2495
+ }
2496
+ if (anchorSpacing) {
2497
+ for (let divisor = 1; divisor <= anchorSpacing; divisor++) if (anchorSpacing % divisor === 0) steps.add(anchorSpacing / divisor);
2498
+ const minStep = Math.floor((length - 1) / Math.max(1, maxTicks)) + 1;
2499
+ steps.add(Math.ceil(minStep / anchorSpacing) * anchorSpacing);
2500
+ }
2501
+ return steps;
2502
+ };
2503
+ /**
2504
+ * Tick values for a band time axis.
2505
+ *
2506
+ * visx samples a band domain by index from offset zero, blind to which labels
2507
+ * carry a boundary and without collapsing repeats — so the tick that prints the
2508
+ * year or the date often isn't sampled at all, and a long series can show the
2509
+ * same label twice. Choose the values instead: sweep the evenly spaced
2510
+ * candidates, including those that step from anchor to anchor, and keep the one
2511
+ * that reaches the most anchors without thinning the axis or putting two
2512
+ * identical labels side by side.
2513
+ *
2514
+ * @param domain - Band domain, in axis order.
2515
+ * @param tickFormatter - Formatter the axis will render these values with.
2516
+ * @param maxTicks - Most ticks the axis should carry.
2517
+ * @return Values to hand the axis as `tickValues`.
2518
+ */
2519
+ const getBandTickValues = (domain, tickFormatter, maxTicks) => {
2520
+ if (!domain.length) return [];
2521
+ const { isAnchor } = tickFormatter;
2522
+ const anchorIndices = isAnchor ? getAnchorIndices(domain, isAnchor) : [];
2523
+ const domainLabels = domain.map((date) => tickFormatter(date.getTime()));
2524
+ const candidates = [];
2525
+ const consider = (indices) => {
2526
+ if (!indices.length || indices.length > maxTicks) return;
2527
+ if (!indices.some((index, position) => position > 0 && domainLabels[index] === domainLabels[indices[position - 1]])) candidates.push(indices);
2528
+ };
2529
+ const anchorSpacing = anchorIndices.length > 1 ? anchorIndices[1] - anchorIndices[0] : null;
2530
+ for (const step of getCandidateSteps(domain.length, maxTicks, anchorSpacing)) for (let offset = 0; offset < step; offset++) {
2531
+ const indices = [];
2532
+ for (let index = offset; index < domain.length; index += step) indices.push(index);
2533
+ consider(indices);
2534
+ }
2535
+ for (let stride = 1; stride <= anchorIndices.length; stride++) consider(anchorIndices.filter((_, position) => position % stride === 0));
2536
+ if (!candidates.length) return [domain[0]];
2537
+ const densest = candidates.reduce((most, indices) => Math.max(most, indices.length), 0);
2538
+ const anchorsIn = (indices) => isAnchor ? indices.filter((index) => isAnchor(domain[index])).length : 0;
2539
+ return candidates.filter((indices) => indices.length >= densest - 1).reduce((chosen, indices) => {
2540
+ const gain = anchorsIn(indices) - anchorsIn(chosen);
2541
+ return gain > 0 || gain === 0 && indices.length > chosen.length ? indices : chosen;
2542
+ }).map((index) => domain[index]);
2281
2543
  };
2282
2544
  const guessOptimalNumTicks = (data, chartWidth, tickFormatter) => {
2283
- const xScale = scaleTime({ domain: [Math.min(...data.map((datom) => datom.data.at(0)?.date)), Math.max(...data.map((datom) => datom.data.at(-1)?.date))] });
2545
+ const span = getSpan(data);
2546
+ if (!span) return 1;
2547
+ const xScale = scaleTime({ domain: [span.minX, span.maxX] });
2284
2548
  const upperBound = Math.min(data[0]?.data.length || 3, Math.ceil(chartWidth / X_TICK_WIDTH));
2285
2549
  let secondBestGuess = 1;
2286
2550
  for (let numTicks = upperBound; numTicks > 1; --numTicks) {
@@ -2317,9 +2581,11 @@ function withResponsive(WrappedComponent) {
2317
2581
  enableDebounceLeadingCall: true
2318
2582
  });
2319
2583
  const hasAspectRatio = aspectRatio !== void 0 && aspectRatio > 0;
2584
+ const [scopeNode, setScopeNode] = useState(null);
2320
2585
  const wrapperRef = useRef(null);
2321
2586
  const setWrapperRef = useCallback((node) => {
2322
2587
  wrapperRef.current = node;
2588
+ setScopeNode(node);
2323
2589
  if (typeof parentRef === "function") parentRef(node);
2324
2590
  else if (parentRef) parentRef.current = node;
2325
2591
  }, [parentRef]);
@@ -2365,14 +2631,17 @@ function withResponsive(WrappedComponent) {
2365
2631
  ...width !== void 0 ? { width } : null,
2366
2632
  ...height !== void 0 ? { height } : null
2367
2633
  },
2368
- children: hasAspectRatio ? /* @__PURE__ */ jsx("div", {
2369
- className: with_responsive_module_default.content,
2370
- style: {
2371
- width: boxWidth,
2372
- height: boxHeight
2373
- },
2374
- children: wrappedComponent
2375
- }) : wrappedComponent
2634
+ children: /* @__PURE__ */ jsx(ChartScopeContext.Provider, {
2635
+ value: scopeNode,
2636
+ children: hasAspectRatio ? /* @__PURE__ */ jsx("div", {
2637
+ className: with_responsive_module_default.content,
2638
+ style: {
2639
+ width: boxWidth,
2640
+ height: boxHeight
2641
+ },
2642
+ children: wrappedComponent
2643
+ }) : wrappedComponent
2644
+ })
2376
2645
  });
2377
2646
  };
2378
2647
  }
@@ -2807,11 +3076,12 @@ const getVerticalAnchor = (subjectType, isFlippedVertically, y, yMax, height) =>
2807
3076
  };
2808
3077
  const LineChartAnnotation = ({ datum, title, subtitle, subjectType = "circle", styles: datumStyles, testId, renderLabel, renderLabelPopover }) => {
2809
3078
  const providerTheme = useGlobalChartsTheme();
3079
+ const scopeElement = useChartScopeElement();
2810
3080
  const { xScale, yScale } = useContext(DataContext) || {};
2811
3081
  const labelRef = useRef(null);
2812
3082
  const [height, setHeight] = useState(null);
2813
3083
  const styles = deepmerge(providerTheme.annotationStyles ?? {}, datumStyles ?? {});
2814
- const resolveColor = (value) => value ? resolveCssVariable(value) ?? value : value;
3084
+ const resolveColor = (value) => value ? resolveCssVariable(value, scopeElement) ?? value : value;
2815
3085
  useEffect(() => {
2816
3086
  if (labelRef.current?.getBBox) {
2817
3087
  const bbox = labelRef.current.getBBox();
@@ -3058,8 +3328,8 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
3058
3328
  const legendShape = legend.shape ?? "line";
3059
3329
  const legendPosition = legend.position ?? "bottom";
3060
3330
  const providerTheme = useGlobalChartsTheme();
3061
- const resolvedBackgroundColor = resolveCssVariable(providerTheme.backgroundColor) ?? providerTheme.backgroundColor;
3062
3331
  const theme = useXYChartTheme(data);
3332
+ const resolvedBackgroundColor = theme.backgroundColor ?? providerTheme.backgroundColor;
3063
3333
  const chartId = useChartId(providedChartId);
3064
3334
  const chartRef = useRef(null);
3065
3335
  const [selectedIndex, setSelectedIndex] = useState(void 0);
@@ -3835,7 +4105,7 @@ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
3835
4105
  stacked,
3836
4106
  stackOffset,
3837
4107
  getElementStyles,
3838
- strokeColor: resolveCssVariable(providerTheme.backgroundColor) ?? providerTheme.backgroundColor
4108
+ strokeColor: theme.backgroundColor ?? providerTheme.backgroundColor
3839
4109
  })] }),
3840
4110
  /* @__PURE__ */ jsx(AreaChartScalesRef, {
3841
4111
  chartRef: internalChartRef,
@@ -3995,11 +4265,73 @@ const TruncatedYTickComponent = createTruncatedTickComponent("y");
3995
4265
  const BASE_BAND_PADDING = .2;
3996
4266
  /** Inner padding of the category band scale (the base gap between ticks). */
3997
4267
  const BASE_BAND_PADDING_INNER = .1;
3998
- const formatDateTick = (timestamp) => {
3999
- return new Date(timestamp).toLocaleDateString(void 0, {
4000
- month: "short",
4268
+ /** Ticks each axis carries unless the caller asks for a different count. */
4269
+ const DEFAULT_NUM_TICKS = 4;
4270
+ const ALL_RENDERED = () => true;
4271
+ const TOOLTIP_FORMAT_BY_RESOLUTION = {
4272
+ hour: {
4273
+ year: "numeric",
4274
+ month: "long",
4275
+ day: "numeric",
4276
+ hour: "numeric",
4277
+ hour12: true
4278
+ },
4279
+ day: {
4280
+ year: "numeric",
4281
+ month: "long",
4001
4282
  day: "numeric"
4002
- });
4283
+ },
4284
+ month: {
4285
+ year: "numeric",
4286
+ month: "long"
4287
+ },
4288
+ year: { year: "numeric" }
4289
+ };
4290
+ /**
4291
+ * Labels one bar's bucket, at the bucket's own granularity — finer than the
4292
+ * ticks whenever the overall time span coarsens the axis.
4293
+ *
4294
+ * @param data - Date-based series, already parsed and sorted by `useChartDataTransform`.
4295
+ * @param tickResolution - Caller-declared bucket resolution, when known.
4296
+ * @return Tooltip label formatter.
4297
+ */
4298
+ const getTooltipFormatter = (data, tickResolution) => {
4299
+ if (tickResolution === "week") return (timestamp) => sprintf(__("Week of %s", "jetpack-charts"), new Date(timestamp).toLocaleDateString(void 0, {
4300
+ year: "numeric",
4301
+ month: "long",
4302
+ day: "numeric"
4303
+ }));
4304
+ const format = TOOLTIP_FORMAT_BY_RESOLUTION[getBucketResolution(data, tickResolution)] ?? TOOLTIP_FORMAT_BY_RESOLUTION.day;
4305
+ return (timestamp) => new Date(timestamp).toLocaleString(void 0, format);
4306
+ };
4307
+ const identity = (label) => label;
4308
+ const byBucket = (format) => (bucket) => typeof bucket === "string" ? bucket : format(Number(bucket));
4309
+ const bucketAccessor = (d) => d?.label || d?.date;
4310
+ /**
4311
+ * The buckets the band axis can put a tick on.
4312
+ *
4313
+ * Built from the accessor visx is given, over the same series in the same order,
4314
+ * so that a tick value is a key the scale actually holds; `scaleBand` returns
4315
+ * `undefined` for one it does not, which parks the tick at the origin. Null
4316
+ * rather than a partial list when any bucket is labelled instead of dated.
4317
+ *
4318
+ * @param data - Every series handed to the chart.
4319
+ * @param isSeriesRendered - Whether visx mounts a series, i.e. the legend shows it.
4320
+ * @return Bucket dates in axis order, or null if the axis cannot be ticked by date.
4321
+ */
4322
+ const getBandDomain = (data, isSeriesRendered) => {
4323
+ const registry = {};
4324
+ for (const series of data) {
4325
+ if (series.options?.type === "comparison" || !isSeriesRendered(series)) continue;
4326
+ registry[series.label] = series;
4327
+ }
4328
+ const byTimestamp = /* @__PURE__ */ new Map();
4329
+ for (const key of Object.keys(registry)) for (const point of registry[key].data) {
4330
+ const bucket = bucketAccessor(point);
4331
+ if (!(bucket instanceof Date)) return null;
4332
+ if (!byTimestamp.has(bucket.getTime())) byTimestamp.set(bucket.getTime(), bucket);
4333
+ }
4334
+ return byTimestamp.size ? [...byTimestamp.values()] : null;
4003
4335
  };
4004
4336
  /**
4005
4337
  * Get the group padding of a scale.
@@ -4013,12 +4345,26 @@ const getGroupPadding = (scale) => {
4013
4345
  /**
4014
4346
  * Returns the merged options for the bar chart, including axis and scale configuration based on the orientation.
4015
4347
  *
4016
- * @param data - The data to be displayed in the chart.
4017
- * @param horizontal - Whether the chart is horizontal or vertical.
4018
- * @param options - The options for the chart.
4348
+ * @param data - The data to be displayed in the chart.
4349
+ * @param horizontal - Whether the chart is horizontal or vertical.
4350
+ * @param options - The options for the chart.
4351
+ * @param isSeriesRendered - Whether visx mounts a series, i.e. the legend shows it.
4019
4352
  * @return The merged options for the chart.
4020
4353
  */
4021
- function useBarChartOptions(data, horizontal, options = {}) {
4354
+ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = ALL_RENDERED) {
4355
+ const stableOptions = useDeepMemo(options);
4356
+ const axisConfig = useMemo(() => {
4357
+ const { labelOverflow: xLabelOverflow, tickResolution: xTickResolution, ...xAxisOptions } = stableOptions.axis?.x || {};
4358
+ const { labelOverflow: yLabelOverflow, tickResolution: yTickResolution, ...yAxisOptions } = stableOptions.axis?.y || {};
4359
+ return {
4360
+ xLabelOverflow,
4361
+ yLabelOverflow,
4362
+ xAxisOptions,
4363
+ yAxisOptions,
4364
+ tickResolution: horizontal ? yTickResolution : xTickResolution
4365
+ };
4366
+ }, [stableOptions, horizontal]);
4367
+ const { tickResolution } = axisConfig;
4022
4368
  const defaultOptions = useMemo(() => {
4023
4369
  const bandScale = {
4024
4370
  type: "band",
@@ -4030,19 +4376,26 @@ function useBarChartOptions(data, horizontal, options = {}) {
4030
4376
  nice: true,
4031
4377
  zero: false
4032
4378
  };
4033
- const labelFormatter = data?.[0]?.data?.[0]?.label ? (label) => label : formatDateTick;
4379
+ const hasLabels = Boolean(data?.[0]?.data?.[0]?.label);
4380
+ const timeTickFormatter = hasLabels ? null : getFormatter(data, tickResolution);
4381
+ const labelFormatter = timeTickFormatter ? byBucket(timeTickFormatter) : identity;
4382
+ const tooltipDatumFormatter = hasLabels ? labelFormatter : byBucket(getTooltipFormatter(data, tickResolution));
4034
4383
  const valueFormatter = formatNumberCompact;
4035
- const labelAccessor = (d) => d?.label || d?.date;
4384
+ const bandDomain = timeTickFormatter ? getBandDomain(data, isSeriesRendered) : null;
4036
4385
  const valueAccessor = (d) => {
4037
4386
  const enhancedPoint = d;
4038
4387
  return enhancedPoint?.visualValue !== void 0 ? enhancedPoint.visualValue : d?.value;
4039
4388
  };
4040
4389
  return {
4390
+ timeAxis: bandDomain && timeTickFormatter && {
4391
+ domain: bandDomain,
4392
+ tickFormatter: timeTickFormatter
4393
+ },
4041
4394
  vertical: {
4042
4395
  xTickFormat: labelFormatter,
4043
4396
  yTickFormat: valueFormatter,
4044
- tooltipLabelFormatter: labelFormatter,
4045
- xAccessor: labelAccessor,
4397
+ tooltipLabelFormatter: tooltipDatumFormatter,
4398
+ xAccessor: bucketAccessor,
4046
4399
  yAccessor: valueAccessor,
4047
4400
  gridVisibility: "x",
4048
4401
  xScale: bandScale,
@@ -4051,20 +4404,24 @@ function useBarChartOptions(data, horizontal, options = {}) {
4051
4404
  horizontal: {
4052
4405
  xTickFormat: valueFormatter,
4053
4406
  yTickFormat: labelFormatter,
4054
- tooltipLabelFormatter: labelFormatter,
4407
+ tooltipLabelFormatter: tooltipDatumFormatter,
4055
4408
  xAccessor: valueAccessor,
4056
- yAccessor: labelAccessor,
4409
+ yAccessor: bucketAccessor,
4057
4410
  gridVisibility: "y",
4058
4411
  xScale: linearScale,
4059
4412
  yScale: bandScale
4060
4413
  }
4061
4414
  };
4062
- }, [data]);
4415
+ }, [
4416
+ data,
4417
+ tickResolution,
4418
+ isSeriesRendered
4419
+ ]);
4063
4420
  return useMemo(() => {
4064
4421
  const { xTickFormat, yTickFormat, tooltipLabelFormatter: defaultTooltipLabelFormatter, xAccessor, yAccessor, gridVisibility, xScale: baseXScale, yScale: baseYScale } = defaultOptions[horizontal ? "horizontal" : "vertical"];
4065
4422
  let valueScaleDomainOverride = {};
4066
4423
  if (data.some((s) => s.options?.type === "comparison")) {
4067
- if (!(!horizontal ? options.yScale?.domain : options.xScale?.domain)) {
4424
+ if (!(!horizontal ? stableOptions.yScale?.domain : stableOptions.xScale?.domain)) {
4068
4425
  const allValues = [];
4069
4426
  data.forEach((series) => {
4070
4427
  series.data.forEach((d) => {
@@ -4078,17 +4435,20 @@ function useBarChartOptions(data, horizontal, options = {}) {
4078
4435
  }
4079
4436
  const xScale = {
4080
4437
  ...baseXScale,
4081
- ...options.xScale || {},
4438
+ ...stableOptions.xScale || {},
4082
4439
  ...horizontal ? valueScaleDomainOverride : {}
4083
4440
  };
4084
4441
  const yScale = {
4085
4442
  ...baseYScale,
4086
- ...options.yScale || {},
4443
+ ...stableOptions.yScale || {},
4087
4444
  ...!horizontal ? valueScaleDomainOverride : {}
4088
4445
  };
4089
- const providedToolTipLabelFormatter = horizontal ? options.axis?.y?.tickFormat : options.axis?.x?.tickFormat;
4090
- const { labelOverflow: xLabelOverflow, ...xAxisOptions } = options.axis?.x || {};
4091
- const { labelOverflow: yLabelOverflow, ...yAxisOptions } = options.axis?.y || {};
4446
+ const { xLabelOverflow, yLabelOverflow, xAxisOptions, yAxisOptions } = axisConfig;
4447
+ const providedToolTipLabelFormatter = horizontal ? yAxisOptions.tickFormat : xAxisOptions.tickFormat;
4448
+ const { timeAxis } = defaultOptions;
4449
+ const dateAxisOptions = horizontal ? yAxisOptions : xAxisOptions;
4450
+ const bandTickValues = timeAxis && !dateAxisOptions.tickFormat ? getBandTickValues(timeAxis.domain, timeAxis.tickFormatter, dateAxisOptions.numTicks ?? DEFAULT_NUM_TICKS) : null;
4451
+ const dateAxisTickValues = bandTickValues ? { tickValues: bandTickValues } : {};
4092
4452
  return {
4093
4453
  gridVisibility,
4094
4454
  xScale,
@@ -4100,15 +4460,17 @@ function useBarChartOptions(data, horizontal, options = {}) {
4100
4460
  axis: {
4101
4461
  x: {
4102
4462
  orientation: "bottom",
4103
- numTicks: 4,
4463
+ numTicks: DEFAULT_NUM_TICKS,
4104
4464
  tickFormat: xTickFormat,
4465
+ ...horizontal ? {} : dateAxisTickValues,
4105
4466
  ...xLabelOverflow === "ellipsis" ? { tickComponent: TruncatedXTickComponent } : {},
4106
4467
  ...xAxisOptions
4107
4468
  },
4108
4469
  y: {
4109
4470
  orientation: "left",
4110
- numTicks: 4,
4471
+ numTicks: DEFAULT_NUM_TICKS,
4111
4472
  tickFormat: yTickFormat,
4473
+ ...horizontal ? dateAxisTickValues : {},
4112
4474
  ...yLabelOverflow === "ellipsis" ? { tickComponent: TruncatedYTickComponent } : {},
4113
4475
  ...yAxisOptions
4114
4476
  }
@@ -4118,7 +4480,8 @@ function useBarChartOptions(data, horizontal, options = {}) {
4118
4480
  };
4119
4481
  }, [
4120
4482
  defaultOptions,
4121
- options,
4483
+ axisConfig,
4484
+ stableOptions,
4122
4485
  horizontal,
4123
4486
  data
4124
4487
  ]);
@@ -4280,7 +4643,13 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4280
4643
  valueAxisLength: horizontal ? width : height
4281
4644
  });
4282
4645
  const legendItems = useChartLegendItems(dataSorted, useMemo(() => ({ collapseGroups: legendCollapseGroups }), [legendCollapseGroups]));
4283
- const chartOptions = useBarChartOptions(dataWithVisibleZeros, horizontal, options);
4646
+ const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
4647
+ const isSeriesRendered = useCallback((series) => !chartId || !legendInteractive || isSeriesVisible(chartId, series.label), [
4648
+ chartId,
4649
+ legendInteractive,
4650
+ isSeriesVisible
4651
+ ]);
4652
+ const chartOptions = useBarChartOptions(dataWithVisibleZeros, horizontal, options, isSeriesRendered);
4284
4653
  const defaultMargin = useChartMargin(height, chartOptions, dataSorted, theme, horizontal);
4285
4654
  const chartRef = useRef(null);
4286
4655
  const { legendChildren, nonLegendChildren } = useChartChildren(children, "BarChart");
@@ -4299,24 +4668,11 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4299
4668
  chartRef,
4300
4669
  totalPoints: Math.max(0, ...primarySeriesForNav.map((s) => s.data?.length || 0)) * primarySeriesForNav.length
4301
4670
  });
4302
- const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
4303
- const seriesWithVisibility = useMemo(() => {
4304
- if (!chartId || !legendInteractive) return dataWithVisibleZeros.map((series, index) => ({
4305
- series,
4306
- index,
4307
- isVisible: true
4308
- }));
4309
- return dataWithVisibleZeros.map((series, index) => ({
4310
- series,
4311
- index,
4312
- isVisible: isSeriesVisible(chartId, series.label)
4313
- }));
4314
- }, [
4315
- dataWithVisibleZeros,
4316
- chartId,
4317
- isSeriesVisible,
4318
- legendInteractive
4319
- ]);
4671
+ const seriesWithVisibility = useMemo(() => dataWithVisibleZeros.map((series, index) => ({
4672
+ series,
4673
+ index,
4674
+ isVisible: isSeriesRendered(series)
4675
+ })), [dataWithVisibleZeros, isSeriesRendered]);
4320
4676
  const allSeriesHidden = useMemo(() => {
4321
4677
  return seriesWithVisibility.every(({ isVisible }) => !isVisible);
4322
4678
  }, [seriesWithVisibility]);
@@ -4876,12 +5232,22 @@ const ConversionFunnelChartInternal = ({ mainRate, changeIndicator, steps, loadi
4876
5232
  const { getElementStyles, isColorPaletteResolved } = useGlobalChartsContext();
4877
5233
  const chartRef = useRef(null);
4878
5234
  const selectedBarRef = useRef(null);
5235
+ const [scopeNode, setScopeNode] = useState(null);
4879
5236
  const { tooltipData, tooltipLeft, tooltipTop, tooltipOpen, showTooltip, hideTooltip } = useTooltip();
4880
5237
  const { handleBarClick, handleBarKeyDown, clearSelection, getStepState } = useFunnelSelection(hideTooltip);
4881
5238
  const { containerRef: portalContainerRef, TooltipInPortal, containerBounds } = useTooltipInPortal({
4882
5239
  detectBounds: true,
4883
5240
  scroll: true
4884
5241
  });
5242
+ const portalContainerRefRef = useRef(portalContainerRef);
5243
+ useEffect(() => {
5244
+ portalContainerRefRef.current = portalContainerRef;
5245
+ }, [portalContainerRef]);
5246
+ const setChartRef = useCallback((node) => {
5247
+ portalContainerRefRef.current(node);
5248
+ chartRef.current = node;
5249
+ setScopeNode(node);
5250
+ }, []);
4885
5251
  const clearSelectionAndRef = useCallback(() => {
4886
5252
  clearSelection();
4887
5253
  selectedBarRef.current = null;
@@ -5027,6 +5393,7 @@ const ConversionFunnelChartInternal = ({ mainRate, changeIndicator, steps, loadi
5027
5393
  direction: "column",
5028
5394
  align: "center",
5029
5395
  justify: "center",
5396
+ ref: setScopeNode,
5030
5397
  className: clsx(conversion_funnel_chart_module_default["conversion-funnel-chart"], loading && conversion_funnel_chart_module_default["conversion-funnel-chart--loading"], className),
5031
5398
  style: {
5032
5399
  ...style,
@@ -5038,80 +5405,80 @@ const ConversionFunnelChartInternal = ({ mainRate, changeIndicator, steps, loadi
5038
5405
  })
5039
5406
  });
5040
5407
  const maxRate = Math.max(...steps.map((step) => step.rate));
5041
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Stack, {
5408
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Stack, {
5042
5409
  direction: "column",
5043
5410
  gap: "xl",
5044
- ref: (node) => {
5045
- portalContainerRef(node);
5046
- chartRef.current = node;
5047
- },
5411
+ ref: setChartRef,
5048
5412
  className: clsx(conversion_funnel_chart_module_default["conversion-funnel-chart"], loading && conversion_funnel_chart_module_default["conversion-funnel-chart--loading"], className),
5049
5413
  style: {
5050
5414
  ...style,
5051
5415
  height: resolvedHeight
5052
5416
  },
5053
- children: [renderMainMetric ? renderMainMetric({
5054
- mainRate,
5055
- changeIndicator,
5056
- className: conversion_funnel_chart_module_default["main-metric"],
5057
- changeColor
5058
- }) : /* @__PURE__ */ jsx(Stack, {
5059
- direction: "row",
5060
- align: "baseline",
5061
- gap: "sm",
5062
- className: conversion_funnel_chart_module_default["main-metric"],
5063
- children: renderDefaultMainMetric()
5064
- }), /* @__PURE__ */ jsx(Stack, {
5065
- direction: "row",
5066
- align: "flex-end",
5067
- gap: "lg",
5068
- className: conversion_funnel_chart_module_default["funnel-container"],
5069
- children: steps.map((step, index) => {
5070
- const barHeight = step.rate / maxRate * 100;
5071
- const { isBlurred } = getStepState(step.id);
5072
- return /* @__PURE__ */ jsxs(Stack, {
5073
- direction: "column",
5074
- className: clsx(conversion_funnel_chart_module_default["funnel-step"], isColorPaletteResolved && conversion_funnel_chart_module_default["funnel-step--animated"], isBlurred && conversion_funnel_chart_module_default["funnel-step--blurred"]),
5075
- gap: "xl",
5076
- children: [/* @__PURE__ */ jsxs(Stack, {
5417
+ children: /* @__PURE__ */ jsxs(ChartScopeContext.Provider, {
5418
+ value: scopeNode,
5419
+ children: [renderMainMetric ? renderMainMetric({
5420
+ mainRate,
5421
+ changeIndicator,
5422
+ className: conversion_funnel_chart_module_default["main-metric"],
5423
+ changeColor
5424
+ }) : /* @__PURE__ */ jsx(Stack, {
5425
+ direction: "row",
5426
+ align: "baseline",
5427
+ gap: "sm",
5428
+ className: conversion_funnel_chart_module_default["main-metric"],
5429
+ children: renderDefaultMainMetric()
5430
+ }), /* @__PURE__ */ jsx(Stack, {
5431
+ direction: "row",
5432
+ align: "flex-end",
5433
+ gap: "lg",
5434
+ className: conversion_funnel_chart_module_default["funnel-container"],
5435
+ children: steps.map((step, index) => {
5436
+ const barHeight = step.rate / maxRate * 100;
5437
+ const { isBlurred } = getStepState(step.id);
5438
+ return /* @__PURE__ */ jsxs(Stack, {
5077
5439
  direction: "column",
5078
- gap: "xs",
5079
- children: [renderStepLabel ? renderStepLabel({
5080
- step,
5081
- index,
5082
- className: conversion_funnel_chart_module_default["step-label"]
5083
- }) : /* @__PURE__ */ jsx("span", {
5084
- className: conversion_funnel_chart_module_default["step-label"],
5085
- children: step.label
5086
- }), renderStepRate ? renderStepRate({
5087
- step,
5088
- index,
5089
- className: conversion_funnel_chart_module_default["step-rate"]
5090
- }) : /* @__PURE__ */ jsx("span", {
5091
- className: conversion_funnel_chart_module_default["step-rate"],
5092
- children: formatPercentage(step.rate)
5440
+ className: clsx(conversion_funnel_chart_module_default["funnel-step"], isColorPaletteResolved && conversion_funnel_chart_module_default["funnel-step--animated"], isBlurred && conversion_funnel_chart_module_default["funnel-step--blurred"]),
5441
+ gap: "xl",
5442
+ children: [/* @__PURE__ */ jsxs(Stack, {
5443
+ direction: "column",
5444
+ gap: "xs",
5445
+ children: [renderStepLabel ? renderStepLabel({
5446
+ step,
5447
+ index,
5448
+ className: conversion_funnel_chart_module_default["step-label"]
5449
+ }) : /* @__PURE__ */ jsx("span", {
5450
+ className: conversion_funnel_chart_module_default["step-label"],
5451
+ children: step.label
5452
+ }), renderStepRate ? renderStepRate({
5453
+ step,
5454
+ index,
5455
+ className: conversion_funnel_chart_module_default["step-rate"]
5456
+ }) : /* @__PURE__ */ jsx("span", {
5457
+ className: conversion_funnel_chart_module_default["step-rate"],
5458
+ children: formatPercentage(step.rate)
5459
+ })]
5460
+ }), /* @__PURE__ */ jsx(Stack, {
5461
+ direction: "column",
5462
+ justify: "flex-end",
5463
+ className: conversion_funnel_chart_module_default["bar-container"],
5464
+ onClick: stepHandlers.get(step.id)?.onClick,
5465
+ onKeyDown: stepHandlers.get(step.id)?.onKeyDown,
5466
+ role: "button",
5467
+ tabIndex: isBlurred ? -1 : 0,
5468
+ "aria-label": step.label,
5469
+ style: { backgroundColor: barBackgroundColor },
5470
+ children: /* @__PURE__ */ jsx("div", {
5471
+ className: clsx(conversion_funnel_chart_module_default["funnel-bar"], { [conversion_funnel_chart_module_default["funnel-bar--animated"]]: animation && !loading && !prefersReducedMotion }),
5472
+ style: {
5473
+ height: `${barHeight}%`,
5474
+ backgroundColor: barColor
5475
+ }
5476
+ })
5093
5477
  })]
5094
- }), /* @__PURE__ */ jsx(Stack, {
5095
- direction: "column",
5096
- justify: "flex-end",
5097
- className: conversion_funnel_chart_module_default["bar-container"],
5098
- onClick: stepHandlers.get(step.id)?.onClick,
5099
- onKeyDown: stepHandlers.get(step.id)?.onKeyDown,
5100
- role: "button",
5101
- tabIndex: isBlurred ? -1 : 0,
5102
- "aria-label": step.label,
5103
- style: { backgroundColor: barBackgroundColor },
5104
- children: /* @__PURE__ */ jsx("div", {
5105
- className: clsx(conversion_funnel_chart_module_default["funnel-bar"], { [conversion_funnel_chart_module_default["funnel-bar--animated"]]: animation && !loading && !prefersReducedMotion }),
5106
- style: {
5107
- height: `${barHeight}%`,
5108
- backgroundColor: barColor
5109
- }
5110
- })
5111
- })]
5112
- }, step.id);
5113
- })
5114
- })]
5478
+ }, step.id);
5479
+ })
5480
+ })]
5481
+ })
5115
5482
  }), tooltipOpen && tooltipData && (() => {
5116
5483
  const tooltipContent = renderTooltip ? renderTooltip({
5117
5484
  step: tooltipData,
@@ -5124,7 +5491,7 @@ const ConversionFunnelChartInternal = ({ mainRate, changeIndicator, steps, loadi
5124
5491
  return /* @__PURE__ */ jsx(TooltipInPortal, {
5125
5492
  top: tooltipTop,
5126
5493
  left: tooltipLeft,
5127
- className: conversion_funnel_chart_module_default["tooltip-wrapper"],
5494
+ className: clsx("a8c-charts-scope", conversion_funnel_chart_module_default["tooltip-wrapper"]),
5128
5495
  children: tooltipContent
5129
5496
  }, Math.random());
5130
5497
  })()] });
@@ -5274,6 +5641,7 @@ function normalizeGeoChartError(eventArgs) {
5274
5641
  */
5275
5642
  const GeoChartInternal = ({ className, data, width, height, region = "world", resolution = "countries", onError, renderPlaceholder }) => {
5276
5643
  const { getElementStyles, theme: { geoChart: { featureFillColor }, backgroundColor } } = useGlobalChartsContext();
5644
+ const scopeElement = useChartScopeElement();
5277
5645
  const containerRef = useRef(null);
5278
5646
  const reportedErrorIdsRef = useRef(/* @__PURE__ */ new Set());
5279
5647
  useEffect(() => {
@@ -5306,8 +5674,8 @@ const GeoChartInternal = ({ className, data, width, height, region = "world", re
5306
5674
  });
5307
5675
  const fullColorHex = getElementStyles({ index: 0 }).color;
5308
5676
  const lightColorHex = lightenHexColor(fullColorHex, .8);
5309
- const backgroundColorHex = normalizeColorToHex(backgroundColor, null, resolveCssVariable) || DEFAULT_BACKGROUND_COLOR;
5310
- const defaultFillColorHex = normalizeColorToHex(featureFillColor, null, resolveCssVariable) || DEFAULT_FEATURE_FILL_COLOR;
5677
+ const backgroundColorHex = normalizeColorToHex(backgroundColor, scopeElement, resolveCssVariable) || DEFAULT_BACKGROUND_COLOR;
5678
+ const defaultFillColorHex = normalizeColorToHex(featureFillColor, scopeElement, resolveCssVariable) || DEFAULT_FEATURE_FILL_COLOR;
5311
5679
  const sanitizedData = useMemo(() => {
5312
5680
  if (data.length === 0) return {
5313
5681
  data,
@@ -5567,7 +5935,8 @@ const HeatmapLegend = ({ steps = 5, lessLabel, moreLabel }) => {
5567
5935
  const CELL_MIX_FLOOR = .15;
5568
5936
  const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, height = 0, className, compact = false, showValues, maxCellWidth, maxCellHeight, minCellWidth, minCellHeight, rowLabels = [], primaryColor, gap = "md", withTooltips = false, renderTooltip, children }) => {
5569
5937
  const chartId = useChartId(providedChartId);
5570
- const { getElementStyles, resolveThemeColor, theme } = useGlobalChartsContext();
5938
+ const { getElementStyles, theme } = useGlobalChartsContext();
5939
+ const scopeElement = useChartScopeElement();
5571
5940
  const { heatmapChart: heatmapChartSettings } = theme;
5572
5941
  const { nonLegendChildren } = useChartChildren(children, "HeatmapChart");
5573
5942
  const [selectedIndex, setSelectedIndex] = useState();
@@ -5582,7 +5951,7 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
5582
5951
  index: 0,
5583
5952
  overrideColor: primaryColor || heatmapChartSettings.primaryColor
5584
5953
  });
5585
- const chartBackgroundHex = resolveThemeColor(theme.backgroundColor);
5954
+ const chartBackgroundHex = normalizeColorToHex(theme.backgroundColor, scopeElement, resolveCssVariable);
5586
5955
  const primaryHex = normalizeColorToHex(primaryColorHex);
5587
5956
  const cellHasLightText = (intensity) => isValidHexColor(primaryHex) && isValidHexColor(chartBackgroundHex) && prefersLightText(mixHexColors(primaryHex, chartBackgroundHex, 1 - (CELL_MIX_FLOOR + (1 - CELL_MIX_FLOOR) * intensity)));
5588
5957
  const extent = useMemo(() => getValueExtent(data), [data]);
@@ -5809,6 +6178,7 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
5809
6178
  top: tooltipTop,
5810
6179
  left: tooltipLeft,
5811
6180
  children: /* @__PURE__ */ jsx("div", {
6181
+ className: "a8c-charts-scope",
5812
6182
  role: "tooltip",
5813
6183
  tabIndex: -1,
5814
6184
  children: (renderTooltip ?? defaultRenderTooltip)(tooltipData)
@@ -8779,6 +9149,7 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
8779
9149
  top: tooltipTop || 0,
8780
9150
  left: tooltipLeft || 0,
8781
9151
  children: /* @__PURE__ */ jsx("div", {
9152
+ className: "a8c-charts-scope",
8782
9153
  role: "tooltip",
8783
9154
  children: renderTooltip({ tooltipData })
8784
9155
  })
@@ -9077,6 +9448,7 @@ const PieSemiCircleChartInternal = ({ data, chartId: providedChartId, width: pro
9077
9448
  top: tooltipTop || 0,
9078
9449
  left: tooltipLeft || 0,
9079
9450
  children: /* @__PURE__ */ jsx("div", {
9451
+ className: "a8c-charts-scope",
9080
9452
  role: "tooltip",
9081
9453
  children: renderTooltip({ tooltipData })
9082
9454
  })
@@ -9364,7 +9736,7 @@ const Icon$1 = ({ direction }) => {
9364
9736
  function TrendIndicator({ direction, value, className, style, showIcon = true }) {
9365
9737
  const ariaLabel = `${DIRECTION_LABELS[direction]}: ${value}`;
9366
9738
  return /* @__PURE__ */ jsxs("span", {
9367
- className: clsx(trend_indicator_module_default["trend-indicator"], trend_indicator_module_default[`trend-indicator--${direction}`], className),
9739
+ className: clsx(useStandaloneScopeClass(), trend_indicator_module_default["trend-indicator"], trend_indicator_module_default[`trend-indicator--${direction}`], className),
9368
9740
  style,
9369
9741
  "aria-label": ariaLabel,
9370
9742
  children: [showIcon && /* @__PURE__ */ jsx(Icon$1, { direction }), /* @__PURE__ */ jsx("span", {
@@ -9374,6 +9746,6 @@ function TrendIndicator({ direction, value, className, style, showIcon = true })
9374
9746
  });
9375
9747
  }
9376
9748
  //#endregion
9377
- export { AccessibleTooltip, AreaChartResponsive as AreaChart, AreaChart as AreaChartUnresponsive, BarChartResponsive as BarChart, BarChart as BarChartUnresponsive, BarListChartResponsive as BarListChart, BarListChart as BarListChartUnresponsive, BaseTooltip, ConversionFunnelChartWithProvider as ConversionFunnelChart, GeoChartResponsive as GeoChart, GeoChartWithProvider as GeoChartUnresponsive, GlobalChartsContext, GlobalChartsProvider, GlobalChartsProvider as ThemeProvider, HeatmapChartResponsive as HeatmapChart, HeatmapChart as HeatmapChartUnresponsive, LeaderboardChartResponsive as LeaderboardChart, LeaderboardChart as LeaderboardChartUnresponsive, Legend, LineChartResponsive as LineChart, LineChart as LineChartUnresponsive, PieChartResponsive as PieChart, PieChart as PieChartUnresponsive, PieSemiCircleChartResponsive as PieSemiCircleChart, PieSemiCircleChart as PieSemiCircleChartUnresponsive, Sparkline, SparklineUnresponsive, TrendIndicator, buildCalendarHeatmapData, defaultTheme, formatMetricValue, formatPercentage, getColorDistance, hexToRgba, isValidHexColor, lightenHexColor, mergeThemes, mixHexColors, normalizeColorToHex, parseAsLocalDate, parseHslString, parseRgbString, prefersLightText, relativeLuminance, useChartLegendItems, useGlobalChartsContext, useGlobalChartsTheme, useLeaderboardLegendItems, validateHexColor };
9749
+ export { AccessibleTooltip, AreaChartResponsive as AreaChart, AreaChart as AreaChartUnresponsive, BarChartResponsive as BarChart, BarChart as BarChartUnresponsive, BarListChartResponsive as BarListChart, BarListChart as BarListChartUnresponsive, BaseTooltip, ConversionFunnelChartWithProvider as ConversionFunnelChart, GeoChartResponsive as GeoChart, GeoChartWithProvider as GeoChartUnresponsive, GlobalChartsContext, GlobalChartsProvider, GlobalChartsProvider as ThemeProvider, HeatmapChartResponsive as HeatmapChart, HeatmapChart as HeatmapChartUnresponsive, LeaderboardChartResponsive as LeaderboardChart, LeaderboardChart as LeaderboardChartUnresponsive, Legend, LineChartResponsive as LineChart, LineChart as LineChartUnresponsive, PieChartResponsive as PieChart, PieChart as PieChartUnresponsive, PieSemiCircleChartResponsive as PieSemiCircleChart, PieSemiCircleChart as PieSemiCircleChartUnresponsive, Sparkline, SparklineUnresponsive, TrendIndicator, buildCalendarHeatmapData, defaultTheme, formatMetricValue, formatPercentage, getColorDistance, hexToRgba, isValidHexColor, lightenHexColor, mergeThemes, mixHexColors, normalizeColorToHex, parseAsLocalDate, parseHslString, parseRgbString, prefersLightText, relativeLuminance, resolveCssVariable, useChartLegendItems, useChartRegistration, useChartScopeElement, useGlobalChartsContext, useGlobalChartsTheme, useLeaderboardLegendItems, validateHexColor };
9378
9750
 
9379
9751
  //# sourceMappingURL=index.js.map