@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.cjs CHANGED
@@ -64,6 +64,14 @@ const useChartInstanceContext = () => {
64
64
  };
65
65
  const useSingleChartContext = useChartInstanceContext;
66
66
  //#endregion
67
+ //#region src/styles/chart-scope-class.ts
68
+ /**
69
+ * 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.
70
+ *
71
+ * Deliberately unhashed: consumers are documented as being able to target it to override a token on the provider element.
72
+ */
73
+ const CHART_SCOPE_CLASS = "a8c-charts-scope";
74
+ //#endregion
67
75
  //#region src/utils/create-composition.ts
68
76
  /**
69
77
  * Utility function to create chart components with composition API.
@@ -510,15 +518,31 @@ const CSS_VAR_NAME_PATTERN = /^--[\w-]+$/;
510
518
  * @param element - Optional DOM element to resolve the variable from (defaults to document.documentElement)
511
519
  * @return The resolved value, fallback value, or null if unresolvable
512
520
  */
513
- const resolveCssVariable = (value, element) => {
514
- if (!value) return null;
515
- if (value.startsWith("var(") && value.endsWith(")")) {
516
- const parsed = parseVarExpression(value);
517
- if (parsed) return resolveVariableName(parsed.varName, element) || parsed.fallback;
518
- }
519
- if (value.startsWith("--")) return resolveVariableName(value, element);
520
- return value;
521
- };
521
+ const resolveCssVariable = (value, element) => createCssVariableResolver(element)(value);
522
+ /**
523
+ * Builds a resolver that reads every value from one `getComputedStyle` snapshot.
524
+ *
525
+ * `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.
526
+ *
527
+ * @param element - The element to resolve against, or null/undefined for the document root.
528
+ * @return A resolver that resolves each value exactly as `resolveCssVariable` does.
529
+ */
530
+ function createCssVariableResolver(element) {
531
+ let styles = null;
532
+ const getStyles = () => {
533
+ if (!styles) styles = computedStyleFor(element);
534
+ return styles;
535
+ };
536
+ return (value) => {
537
+ if (!value) return null;
538
+ if (value.startsWith("var(") && value.endsWith(")")) {
539
+ const parsed = parseVarExpression(value);
540
+ if (parsed) return readCustomProperty(parsed.varName, getStyles()) || parsed.fallback;
541
+ }
542
+ if (value.startsWith("--")) return readCustomProperty(value, getStyles());
543
+ return value;
544
+ };
545
+ }
522
546
  /**
523
547
  * Parses a var() expression into its variable name and optional fallback.
524
548
  * Uses string manipulation instead of complex regex to avoid ReDoS.
@@ -546,17 +570,30 @@ function parseVarExpression(expr) {
546
570
  };
547
571
  }
548
572
  /**
549
- * Resolves a plain CSS variable name to its computed value.
573
+ * Takes one computed-style snapshot, or null where there is nothing to read (SSR, or a detached element).
574
+ *
575
+ * @param element - The element to read from, or null/undefined for the document root.
576
+ * @return The computed styles, or null.
577
+ */
578
+ function computedStyleFor(element) {
579
+ if (typeof window === "undefined" || typeof document === "undefined") return null;
580
+ try {
581
+ return getComputedStyle(element || document.documentElement);
582
+ } catch {
583
+ return null;
584
+ }
585
+ }
586
+ /**
587
+ * Reads one custom property from a computed-style snapshot.
550
588
  *
551
589
  * @param varName - A CSS variable name like '--my-color'
552
- * @param element - Optional DOM element to resolve from
590
+ * @param styles - The snapshot to read from, or null
553
591
  * @return The computed value or null
554
592
  */
555
- function resolveVariableName(varName, element) {
556
- if (typeof window === "undefined" || typeof document === "undefined") return null;
593
+ function readCustomProperty(varName, styles) {
594
+ if (!styles) return null;
557
595
  try {
558
- const targetElement = element || document.documentElement;
559
- return getComputedStyle(targetElement).getPropertyValue(varName).trim() || null;
596
+ return styles.getPropertyValue(varName).trim() || null;
560
597
  } catch {
561
598
  return null;
562
599
  }
@@ -591,6 +628,14 @@ const resolveFontSize = (val) => {
591
628
  }
592
629
  };
593
630
  //#endregion
631
+ //#region src/providers/chart-scope/chart-scope-context.ts
632
+ /**
633
+ * The nearest chart element, published so JS token resolution reads the same CSS cascade that paints the chart's own colours.
634
+ *
635
+ * 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.
636
+ */
637
+ const ChartScopeContext = (0, react.createContext)(null);
638
+ //#endregion
594
639
  //#region src/providers/chart-context/private/get-chart-color.ts
595
640
  /**
596
641
  * Golden ratio for mathematically pleasing color distribution
@@ -720,12 +765,75 @@ const getChartColor = (index, colorCache) => {
720
765
  return (0, _visx_vendor_d3_color.hsl)(Math.round(fallbackHue), fallbackSaturation / 100, fallbackLightness / 100).formatHex();
721
766
  };
722
767
  //#endregion
768
+ //#region src/providers/chart-context/private/theme-override-vars.ts
769
+ const ROLE_FOR_FIELD = [
770
+ ["--a8c-charts-color-background", (theme) => theme.backgroundColor],
771
+ ["--a8c-charts-color-grid", (theme) => theme.gridStyles?.stroke],
772
+ ["--a8c-charts-color-axis", (theme) => theme.xAxisLineStyles?.stroke],
773
+ ["--a8c-charts-color-tick", (theme) => theme.xTickLineStyles?.stroke],
774
+ ["--a8c-charts-color-label-axis", (theme) => theme.svgLabelSmall?.fill]
775
+ ];
776
+ /**
777
+ * The variable a `theme` prop override is published as, one layer outside the role itself.
778
+ *
779
+ * `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.
780
+ *
781
+ * @param role - The catalog role being overridden.
782
+ * @return The custom property carrying the consumer's value.
783
+ */
784
+ const themeLayerVar = (role) => `${role}-theme`;
785
+ ROLE_FOR_FIELD.map(([role]) => role);
786
+ /**
787
+ * Whether a value reads the role it is being published for, which would make the catalog entry depend on itself.
788
+ *
789
+ * 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`.
790
+ *
791
+ * Three things this has to get right, none of which a plain substring test gets:
792
+ *
793
+ * `var()` permits whitespace after the opening paren, so testing for `var(<role>` misses `var( --a8c-charts-color-grid, … )`.
794
+ *
795
+ * 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.
796
+ *
797
+ * `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.
798
+ *
799
+ * @param value - The consumer's value for the field.
800
+ * @param role - The catalog role this value would override.
801
+ * @return True when the value reads `role`, so it must not be published.
802
+ */
803
+ const readsOwnRole = (value, role) => value.trim() === role || new RegExp(`var\\(\\s*${role.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`).test(value);
804
+ /**
805
+ * 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.
806
+ *
807
+ * 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.
808
+ *
809
+ * @param theme - The consumer's `theme` prop, before merging with the default theme.
810
+ * @return The wrapper's custom properties, and the roles the consumer overrode.
811
+ */
812
+ const themeOverrideVars = (theme) => {
813
+ if (!theme) return {
814
+ vars: {},
815
+ roles: []
816
+ };
817
+ const vars = {};
818
+ const roles = [];
819
+ for (const [role, read] of ROLE_FOR_FIELD) {
820
+ const value = read(theme);
821
+ if (typeof value !== "string" || value === "") continue;
822
+ roles.push(role);
823
+ if (!readsOwnRole(value, role)) vars[themeLayerVar(role)] = value;
824
+ }
825
+ return {
826
+ vars,
827
+ roles
828
+ };
829
+ };
830
+ //#endregion
723
831
  //#region src/providers/chart-context/themes.ts
724
832
  /**
725
833
  * Default theme configuration
726
834
  */
727
835
  const defaultTheme = {
728
- backgroundColor: "var(--a8c-charts-color-background, var(--wpds-color-background-surface-neutral-strong, #fff))",
836
+ backgroundColor: "var(--a8c-charts-color-background, #fff)",
729
837
  labelBackgroundColor: "transparent",
730
838
  labelTextColor: "var(--a8c-charts-color-label-on-fill, #FFFFFF)",
731
839
  colors: [
@@ -736,59 +844,59 @@ const defaultTheme = {
736
844
  "#FF8C8F"
737
845
  ],
738
846
  gridStyles: {
739
- stroke: "var(--a8c-charts-color-grid, var(--wpds-color-stroke-surface-neutral, #dbdbdb))",
847
+ stroke: "var(--a8c-charts-color-grid, #dbdbdb)",
740
848
  strokeWidth: 1
741
849
  },
742
850
  tickLength: 4,
743
851
  gridColor: "",
744
852
  gridColorDark: "",
745
853
  xTickLineStyles: {
746
- stroke: "var(--a8c-charts-color-tick, var(--wpds-color-stroke-surface-neutral, #dbdbdb))",
854
+ stroke: "var(--a8c-charts-color-tick, #dbdbdb)",
747
855
  strokeWidth: 1
748
856
  },
749
857
  xAxisLineStyles: {
750
- stroke: "var(--a8c-charts-color-axis, var(--wpds-color-stroke-surface-neutral, #dbdbdb))",
858
+ stroke: "var(--a8c-charts-color-axis, #dbdbdb)",
751
859
  strokeWidth: 1
752
860
  },
753
861
  legend: {
754
- labelStyles: { color: "var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e))" },
862
+ labelStyles: { color: "var(--a8c-charts-color-label, #1e1e1e)" },
755
863
  containerStyles: {},
756
864
  shapeStyles: []
757
865
  },
758
866
  seriesLineStyles: [],
759
867
  glyphs: [],
760
868
  svgLabelSmall: {
761
- fill: "var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e))",
869
+ fill: "var(--a8c-charts-color-label-axis, #1e1e1e)",
762
870
  fontFamily: "inherit"
763
871
  },
764
872
  svgLabelBig: { fontFamily: "inherit" },
765
873
  annotationStyles: {
766
874
  label: {
767
- anchorLineStroke: "var(--a8c-charts-color-annotation, var(--wpds-color-foreground-content-neutral, #1e1e1e))",
768
- backgroundFill: "var(--a8c-charts-color-background, var(--wpds-color-background-surface-neutral-strong, #fff))"
875
+ anchorLineStroke: "var(--a8c-charts-color-annotation, #1e1e1e)",
876
+ backgroundFill: "var(--a8c-charts-color-surface, #fff)"
769
877
  },
770
- connector: { stroke: "var(--a8c-charts-color-annotation, var(--wpds-color-foreground-content-neutral, #1e1e1e))" },
878
+ connector: { stroke: "var(--a8c-charts-color-annotation, #1e1e1e)" },
771
879
  circleSubject: {
772
880
  stroke: "transparent",
773
- fill: "var(--a8c-charts-color-annotation, var(--wpds-color-foreground-content-neutral, #1e1e1e))",
881
+ fill: "var(--a8c-charts-color-annotation, #1e1e1e)",
774
882
  radius: 5
775
883
  }
776
884
  },
777
- geoChart: { featureFillColor: "var(--a8c-charts-color-surface-secondary, var(--wpds-color-background-surface-neutral-weak, #f4f4f4))" },
885
+ geoChart: { featureFillColor: "var(--a8c-charts-color-surface-secondary, #f4f4f4)" },
778
886
  leaderboardChart: {
779
887
  rowGap: 12,
780
888
  columnGap: 4,
781
889
  labelSpacing: "xs",
782
890
  deltaColors: [
783
- "var(--a8c-charts-color-trend-down, var(--wpds-color-foreground-content-error-weak, #cc1818))",
784
- "var(--a8c-charts-color-trend-neutral, var(--wpds-color-foreground-content-neutral-weak, #707070))",
785
- "var(--a8c-charts-color-trend-up, var(--wpds-color-foreground-content-success-weak, #008030))"
891
+ "var(--a8c-charts-color-trend-down, #cc1818)",
892
+ "var(--a8c-charts-color-trend-neutral, #707070)",
893
+ "var(--a8c-charts-color-trend-up, #008030)"
786
894
  ]
787
895
  },
788
896
  conversionFunnelChart: {
789
- backgroundColor: "var(--a8c-charts-color-surface-secondary, var(--wpds-color-background-surface-neutral-weak, #f4f4f4))",
790
- positiveChangeColor: "var(--a8c-charts-color-trend-up, var(--wpds-color-foreground-content-success-weak, #008030))",
791
- negativeChangeColor: "var(--a8c-charts-color-trend-down, var(--wpds-color-foreground-content-error-weak, #cc1818))"
897
+ backgroundColor: "var(--a8c-charts-color-surface-secondary, #f4f4f4)",
898
+ positiveChangeColor: "var(--a8c-charts-color-trend-up, #008030)",
899
+ negativeChangeColor: "var(--a8c-charts-color-trend-down, #cc1818)"
792
900
  },
793
901
  lineChart: { lineStyles: { comparison: {
794
902
  strokeDasharray: "4 4",
@@ -813,15 +921,65 @@ const defaultTheme = {
813
921
  }
814
922
  };
815
923
  //#endregion
924
+ //#region src/providers/chart-context/private/with-catalog-pointers.ts
925
+ const CATALOG_RESTORE_FOR_ROLE = {
926
+ "--a8c-charts-color-background": () => ({ backgroundColor: defaultTheme.backgroundColor }),
927
+ "--a8c-charts-color-grid": (theme) => ({ gridStyles: {
928
+ ...theme.gridStyles,
929
+ stroke: defaultTheme.gridStyles.stroke
930
+ } }),
931
+ "--a8c-charts-color-axis": (theme) => ({ xAxisLineStyles: {
932
+ ...theme.xAxisLineStyles,
933
+ stroke: defaultTheme.xAxisLineStyles.stroke
934
+ } }),
935
+ "--a8c-charts-color-tick": (theme) => ({ xTickLineStyles: {
936
+ ...theme.xTickLineStyles,
937
+ stroke: defaultTheme.xTickLineStyles.stroke
938
+ } }),
939
+ "--a8c-charts-color-label-axis": (theme) => ({ svgLabelSmall: {
940
+ ...theme.svgLabelSmall,
941
+ fill: defaultTheme.svgLabelSmall.fill
942
+ } })
943
+ };
944
+ /**
945
+ * 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.
946
+ *
947
+ * 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`.
948
+ *
949
+ * 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.
950
+ *
951
+ * @param merged - The consumer theme merged onto `defaultTheme` (`mergeThemes` output).
952
+ * @param overriddenRoles - The catalog roles the consumer overrode, from `themeOverrideVars`.
953
+ * @return A new theme object; `merged` is not mutated.
954
+ */
955
+ const withCatalogPointers = (merged, overriddenRoles) => {
956
+ let result = merged;
957
+ for (const role of overriddenRoles) {
958
+ const restore = CATALOG_RESTORE_FOR_ROLE[role];
959
+ if (restore) result = {
960
+ ...result,
961
+ ...restore(result)
962
+ };
963
+ }
964
+ return result;
965
+ };
966
+ //#endregion
816
967
  //#region src/providers/chart-context/global-charts-provider.tsx
817
968
  const GlobalChartsContext = (0, react.createContext)(null);
818
969
  const GlobalChartsProvider = ({ children, theme }) => {
819
970
  const [charts, setCharts] = (0, react.useState)(() => /* @__PURE__ */ new Map());
820
971
  const [hiddenSeries, setHiddenSeries] = (0, react.useState)(() => /* @__PURE__ */ new Map());
821
972
  const wrapperRef = (0, react.useRef)(null);
973
+ const [scopeNode, setScopeNode] = (0, react.useState)(null);
974
+ const setWrapperNode = (0, react.useCallback)((node) => {
975
+ wrapperRef.current = node;
976
+ setScopeNode(node);
977
+ }, []);
978
+ const { vars: overrideVars, roles: overriddenRoles } = (0, react.useMemo)(() => themeOverrideVars(theme), [theme]);
822
979
  const providerTheme = (0, react.useMemo)(() => {
823
- return theme ? mergeThemes(defaultTheme, theme) : defaultTheme;
824
- }, [theme]);
980
+ if (!theme) return defaultTheme;
981
+ return withCatalogPointers(mergeThemes(defaultTheme, theme), overriddenRoles);
982
+ }, [theme, overriddenRoles]);
825
983
  const [colorCache, setColorCache] = (0, react.useState)(() => ({
826
984
  colors: [],
827
985
  hues: [],
@@ -898,7 +1056,6 @@ const GlobalChartsProvider = ({ children, theme }) => {
898
1056
  }
899
1057
  return getChartColor(index, colorCache);
900
1058
  }, [colorCache, groupToColorMap]);
901
- const resolveThemeColor = (0, react.useCallback)((value) => value ? normalizeColorToHex(value, wrapperRef.current, resolveCssVariable) : "", []);
902
1059
  const getElementStyles = (0, react.useCallback)(({ data, index, overrideColor, legendShape }) => {
903
1060
  const isSeriesData = data && typeof data === "object" && "data" in data && "options" in data;
904
1061
  const isPointPercentageData = data && typeof data === "object" && "value" in data && typeof data.value === "number" && !("data" in data);
@@ -941,7 +1098,6 @@ const GlobalChartsProvider = ({ children, theme }) => {
941
1098
  getChartData,
942
1099
  theme: providerTheme,
943
1100
  getElementStyles,
944
- resolveThemeColor,
945
1101
  toggleSeriesVisibility,
946
1102
  isSeriesVisible,
947
1103
  getHiddenSeries,
@@ -953,7 +1109,6 @@ const GlobalChartsProvider = ({ children, theme }) => {
953
1109
  getChartData,
954
1110
  providerTheme,
955
1111
  getElementStyles,
956
- resolveThemeColor,
957
1112
  toggleSeriesVisibility,
958
1113
  isSeriesVisible,
959
1114
  getHiddenSeries,
@@ -962,9 +1117,16 @@ const GlobalChartsProvider = ({ children, theme }) => {
962
1117
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GlobalChartsContext.Provider, {
963
1118
  value,
964
1119
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
965
- ref: wrapperRef,
966
- style: { display: "contents" },
967
- children
1120
+ ref: setWrapperNode,
1121
+ className: CHART_SCOPE_CLASS,
1122
+ style: {
1123
+ display: "contents",
1124
+ ...overrideVars
1125
+ },
1126
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartScopeContext.Provider, {
1127
+ value: scopeNode,
1128
+ children
1129
+ })
968
1130
  })
969
1131
  });
970
1132
  };
@@ -1024,12 +1186,33 @@ const useDeepMemo = (value) => {
1024
1186
  return ref.current;
1025
1187
  };
1026
1188
  //#endregion
1189
+ //#region src/providers/chart-scope/use-chart-scope-element.ts
1190
+ /**
1191
+ * Returns the nearest chart scope element, or `null` when rendered outside any scope (and during SSR, before the ref is attached).
1192
+ *
1193
+ * @return {HTMLElement | null} The nearest chart scope element, or `null`.
1194
+ */
1195
+ const useChartScopeElement = () => (0, react.useContext)(ChartScopeContext);
1196
+ //#endregion
1197
+ //#region src/providers/chart-scope/use-standalone-scope-class.ts
1198
+ /**
1199
+ * Returns the catalog class when there is no `GlobalChartsProvider` above this component, and `undefined` when there is.
1200
+ *
1201
+ * 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.
1202
+ *
1203
+ * @return The scope class name, or undefined when a provider is present.
1204
+ */
1205
+ const useStandaloneScopeClass = () => (0, react.useContext)(GlobalChartsContext) ? void 0 : CHART_SCOPE_CLASS;
1206
+ //#endregion
1027
1207
  //#region src/hooks/use-xychart-theme.ts
1028
- const resolveColor = (value) => value ? resolveCssVariable(value) ?? value : value;
1029
1208
  const useXYChartTheme = (data) => {
1030
1209
  const theme = useGlobalChartsTheme();
1210
+ const scopeElement = useChartScopeElement();
1211
+ const seriesColorKey = JSON.stringify((data ?? []).map((series) => series.options?.stroke).filter((color) => Boolean(color)));
1031
1212
  return (0, react.useMemo)(() => {
1032
- const seriesColors = (data ?? []).map((series) => series.options?.stroke).filter((color) => Boolean(color));
1213
+ const resolve = createCssVariableResolver(scopeElement);
1214
+ const resolveColor = (value) => value ? resolve(value) ?? value : value;
1215
+ const seriesColors = JSON.parse(seriesColorKey);
1033
1216
  return (0, _visx_xychart.buildChartTheme)({
1034
1217
  ...theme,
1035
1218
  colors: [...seriesColors, ...theme.colors ?? []],
@@ -1051,7 +1234,11 @@ const useXYChartTheme = (data) => {
1051
1234
  fill: resolveColor(theme.svgLabelSmall.fill)
1052
1235
  }
1053
1236
  });
1054
- }, [theme, data]);
1237
+ }, [
1238
+ theme,
1239
+ seriesColorKey,
1240
+ scopeElement
1241
+ ]);
1055
1242
  };
1056
1243
  //#endregion
1057
1244
  //#region src/hooks/use-chart-data-transform.ts
@@ -1138,7 +1325,7 @@ const getXAxisLabelMetrics = (theme, orientation) => {
1138
1325
  const useChartMargin = (height, options, data, theme, horizontal = false) => {
1139
1326
  const yTicks = (0, react.useMemo)(() => {
1140
1327
  const allDataPoints = data.flatMap((series) => series.data);
1141
- if (horizontal) return allDataPoints.map((d) => d.label || options.axis?.y?.tickFormat(d.date.getTime(), 0, []));
1328
+ if (horizontal) return allDataPoints.map((d) => d.label || d.date?.getTime());
1142
1329
  if (options.axis?.y?.tickValues?.length) return options.axis.y.tickValues;
1143
1330
  const minY = Math.min(...allDataPoints.map((d) => d.value));
1144
1331
  const maxY = Math.max(...allDataPoints.map((d) => d.value));
@@ -1527,6 +1714,7 @@ const BaseLegend = (0, react.forwardRef)(({ items, className, orientation = "hor
1527
1714
  const { width: shapeWidth = 16, height: shapeHeight = 16, margin: shapeMargin = "2px 4px 2px 0" } = shapeStyles ?? {};
1528
1715
  const theme = useGlobalChartsTheme();
1529
1716
  const context = (0, react.useContext)(GlobalChartsContext);
1717
+ const standaloneScopeClass = useStandaloneScopeClass();
1530
1718
  const legendScale = (0, _visx_scale.scaleOrdinal)({
1531
1719
  domain: items.map((item) => item.label),
1532
1720
  range: items.map((item) => item.color)
@@ -1579,7 +1767,7 @@ const BaseLegend = (0, react.forwardRef)(({ items, className, orientation = "hor
1579
1767
  justify: orientation === "horizontal" ? flexAlignment : void 0,
1580
1768
  wrap: orientation === "horizontal" ? "wrap" : void 0,
1581
1769
  role: "list",
1582
- className: (0, clsx.default)(base_legend_module_default.legend, className),
1770
+ className: (0, clsx.default)(standaloneScopeClass, base_legend_module_default.legend, className),
1583
1771
  style: theme.legend?.containerStyles,
1584
1772
  children: labels.map((label, i) => {
1585
1773
  const matchedItem = items[i];
@@ -1866,9 +2054,10 @@ const BaseTooltip = ({ data, top, left, component: Component = DefaultTooltipCon
1866
2054
  data,
1867
2055
  className
1868
2056
  });
2057
+ const standaloneScopeClass = useStandaloneScopeClass();
1869
2058
  if (!renderContainer) return content;
1870
2059
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1871
- className: base_tooltip_module_default.tooltip,
2060
+ className: (0, clsx.default)(standaloneScopeClass, base_tooltip_module_default.tooltip),
1872
2061
  style: {
1873
2062
  top,
1874
2063
  left,
@@ -1937,10 +2126,11 @@ const AccessibleTooltip = ({ renderTooltip, selectedIndex, tooltipRef, keyboardF
1937
2126
  tabIndex: -1,
1938
2127
  role: "tooltip",
1939
2128
  "aria-atomic": "true",
1940
- className: keyboardFocusedClassName,
2129
+ className: (0, clsx.default)(CHART_SCOPE_CLASS, keyboardFocusedClassName),
1941
2130
  children: tooltipContent
1942
2131
  }, `chart-tooltip-${selectedIndex}`);
1943
2132
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2133
+ className: CHART_SCOPE_CLASS,
1944
2134
  role: "tooltip",
1945
2135
  "aria-live": "polite",
1946
2136
  children: tooltipContent
@@ -2233,28 +2423,38 @@ const getCurveType = (type, smoothing) => {
2233
2423
  default: return _visx_curve.curveLinear;
2234
2424
  }
2235
2425
  };
2236
- const formatYearTick = (timestamp) => {
2237
- return new Date(timestamp).toLocaleDateString(void 0, { year: "numeric" });
2238
- };
2239
- const formatDateTick$1 = (timestamp) => {
2240
- return new Date(timestamp).toLocaleDateString(void 0, {
2241
- month: "short",
2242
- day: "numeric"
2243
- });
2244
- };
2245
- const formatHourTick = (timestamp) => {
2246
- return new Date(timestamp).toLocaleTimeString(void 0, {
2247
- hour: "numeric",
2248
- hour12: true
2249
- });
2250
- };
2251
- const formatDateOrHourTick = (timestamp) => {
2252
- const date = new Date(timestamp);
2253
- return date.getHours() === 0 && date.getMinutes() === 0 ? formatDateTick$1(timestamp) : formatHourTick(timestamp);
2426
+ const dateTimeFormat = (options) => {
2427
+ let formatter;
2428
+ return (timestamp) => {
2429
+ if (!Number.isFinite(Number(timestamp))) return new Date(timestamp).toLocaleDateString();
2430
+ formatter = formatter ?? new Intl.DateTimeFormat(void 0, options);
2431
+ return formatter.format(timestamp);
2432
+ };
2254
2433
  };
2255
- const formatMonthOrYearTick = (timestamp) => {
2256
- const date = new Date(timestamp);
2257
- return date.getMonth() === 0 ? formatYearTick(timestamp) : date.toLocaleDateString(void 0, { month: "short" });
2434
+ const formatYearTick = dateTimeFormat({ year: "numeric" });
2435
+ const formatDateTick = dateTimeFormat({
2436
+ month: "short",
2437
+ day: "numeric"
2438
+ });
2439
+ const formatHourTick = dateTimeFormat({
2440
+ hour: "numeric",
2441
+ hour12: true
2442
+ });
2443
+ const formatMonthTick = dateTimeFormat({ month: "short" });
2444
+ const boundaryFormat = (isAnchor, atBoundary, between) => {
2445
+ const format = (timestamp) => isAnchor(new Date(timestamp)) ? atBoundary(timestamp) : between(timestamp);
2446
+ format.isAnchor = isAnchor;
2447
+ return format;
2448
+ };
2449
+ const formatDateOrHourTick = boundaryFormat((date) => date.getHours() === 0 && date.getMinutes() === 0, formatDateTick, formatHourTick);
2450
+ const formatMonthOrYearTick = boundaryFormat((date) => date.getMonth() === 0, formatYearTick, formatMonthTick);
2451
+ const getSpan = (sortedData) => {
2452
+ const bounds = sortedData.map((datom) => [datom.data.at(0)?.date, datom.data.at(-1)?.date]).filter(([first, last]) => first !== void 0 && last !== void 0);
2453
+ if (!bounds.length) return null;
2454
+ return {
2455
+ minX: Math.min(...bounds.map(([first]) => Number(first))),
2456
+ maxX: Math.max(...bounds.map(([, last]) => Number(last)))
2457
+ };
2258
2458
  };
2259
2459
  const getPointSpacingInHours = (sortedData) => {
2260
2460
  return sortedData.reduce((spacing, datom) => datom.data.reduce((seriesSpacing, point, index) => {
@@ -2263,26 +2463,90 @@ const getPointSpacingInHours = (sortedData) => {
2263
2463
  return Math.min(seriesSpacing, Math.abs((0, date_fns.differenceInHours)(point.date, previous.date)));
2264
2464
  }, spacing), Number.POSITIVE_INFINITY);
2265
2465
  };
2266
- const SPACING_BY_RESOLUTION = {
2267
- hour: 1,
2268
- day: 24,
2269
- week: 168,
2270
- month: 672
2466
+ const SUB_DAILY_SPACING_HOURS = 23;
2467
+ const MONTHLY_SPACING_HOURS = 672;
2468
+ const getBucketResolution = (sortedData, tickResolution) => {
2469
+ if (tickResolution) return tickResolution === "week" ? "day" : tickResolution;
2470
+ const spacingInHours = getPointSpacingInHours(sortedData);
2471
+ if (spacingInHours < SUB_DAILY_SPACING_HOURS) return "hour";
2472
+ if (!Number.isFinite(spacingInHours) || spacingInHours < MONTHLY_SPACING_HOURS) return "day";
2473
+ return spacingInHours < 12 * MONTHLY_SPACING_HOURS ? "month" : "year";
2271
2474
  };
2272
2475
  const getFormatter = (sortedData, tickResolution) => {
2273
- if (tickResolution === "year") return formatYearTick;
2274
- const minX = Math.min(...sortedData.map((datom) => datom.data.at(0)?.date));
2275
- const maxX = Math.max(...sortedData.map((datom) => datom.data.at(-1)?.date));
2276
- const spacingInHours = tickResolution ? SPACING_BY_RESOLUTION[tickResolution] : getPointSpacingInHours(sortedData);
2277
- const isSubDaily = spacingInHours < 23;
2278
- const diffInHours = Math.abs((0, date_fns.differenceInHours)(maxX, minX));
2279
- if (diffInHours <= 24 && isSubDaily) return formatHourTick;
2280
- if (diffInHours <= 168 && isSubDaily) return formatDateOrHourTick;
2281
- if (Math.abs((0, date_fns.differenceInYears)(maxX, minX)) <= 1) return Number.isFinite(spacingInHours) && spacingInHours >= 672 ? formatMonthOrYearTick : formatDateTick$1;
2282
- return formatYearTick;
2476
+ const resolution = getBucketResolution(sortedData, tickResolution);
2477
+ if (resolution === "year") return formatYearTick;
2478
+ if (resolution === "month") return formatMonthOrYearTick;
2479
+ const span = getSpan(sortedData);
2480
+ if (!span) return formatDateTick;
2481
+ const diffInHours = Math.abs((0, date_fns.differenceInHours)(span.maxX, span.minX));
2482
+ if (resolution === "hour") {
2483
+ if (diffInHours <= 24) return formatHourTick;
2484
+ if (diffInHours <= 168) return formatDateOrHourTick;
2485
+ }
2486
+ return Math.abs((0, date_fns.differenceInYears)(span.maxX, span.minX)) <= 1 ? formatDateTick : formatYearTick;
2487
+ };
2488
+ const getAnchorIndices = (domain, isAnchor) => domain.reduce((indices, date, index) => {
2489
+ if (isAnchor(date)) indices.push(index);
2490
+ return indices;
2491
+ }, []);
2492
+ const getCandidateSteps = (length, maxTicks, anchorSpacing) => {
2493
+ const steps = /* @__PURE__ */ new Set();
2494
+ for (let count = maxTicks; count > 1; count--) {
2495
+ steps.add(Math.max(1, Math.floor((length - 1) / (count - 1))));
2496
+ steps.add(Math.max(1, Math.ceil((length - 1) / (count - 1))));
2497
+ }
2498
+ if (anchorSpacing) {
2499
+ for (let divisor = 1; divisor <= anchorSpacing; divisor++) if (anchorSpacing % divisor === 0) steps.add(anchorSpacing / divisor);
2500
+ const minStep = Math.floor((length - 1) / Math.max(1, maxTicks)) + 1;
2501
+ steps.add(Math.ceil(minStep / anchorSpacing) * anchorSpacing);
2502
+ }
2503
+ return steps;
2504
+ };
2505
+ /**
2506
+ * Tick values for a band time axis.
2507
+ *
2508
+ * visx samples a band domain by index from offset zero, blind to which labels
2509
+ * carry a boundary and without collapsing repeats — so the tick that prints the
2510
+ * year or the date often isn't sampled at all, and a long series can show the
2511
+ * same label twice. Choose the values instead: sweep the evenly spaced
2512
+ * candidates, including those that step from anchor to anchor, and keep the one
2513
+ * that reaches the most anchors without thinning the axis or putting two
2514
+ * identical labels side by side.
2515
+ *
2516
+ * @param domain - Band domain, in axis order.
2517
+ * @param tickFormatter - Formatter the axis will render these values with.
2518
+ * @param maxTicks - Most ticks the axis should carry.
2519
+ * @return Values to hand the axis as `tickValues`.
2520
+ */
2521
+ const getBandTickValues = (domain, tickFormatter, maxTicks) => {
2522
+ if (!domain.length) return [];
2523
+ const { isAnchor } = tickFormatter;
2524
+ const anchorIndices = isAnchor ? getAnchorIndices(domain, isAnchor) : [];
2525
+ const domainLabels = domain.map((date) => tickFormatter(date.getTime()));
2526
+ const candidates = [];
2527
+ const consider = (indices) => {
2528
+ if (!indices.length || indices.length > maxTicks) return;
2529
+ if (!indices.some((index, position) => position > 0 && domainLabels[index] === domainLabels[indices[position - 1]])) candidates.push(indices);
2530
+ };
2531
+ const anchorSpacing = anchorIndices.length > 1 ? anchorIndices[1] - anchorIndices[0] : null;
2532
+ for (const step of getCandidateSteps(domain.length, maxTicks, anchorSpacing)) for (let offset = 0; offset < step; offset++) {
2533
+ const indices = [];
2534
+ for (let index = offset; index < domain.length; index += step) indices.push(index);
2535
+ consider(indices);
2536
+ }
2537
+ for (let stride = 1; stride <= anchorIndices.length; stride++) consider(anchorIndices.filter((_, position) => position % stride === 0));
2538
+ if (!candidates.length) return [domain[0]];
2539
+ const densest = candidates.reduce((most, indices) => Math.max(most, indices.length), 0);
2540
+ const anchorsIn = (indices) => isAnchor ? indices.filter((index) => isAnchor(domain[index])).length : 0;
2541
+ return candidates.filter((indices) => indices.length >= densest - 1).reduce((chosen, indices) => {
2542
+ const gain = anchorsIn(indices) - anchorsIn(chosen);
2543
+ return gain > 0 || gain === 0 && indices.length > chosen.length ? indices : chosen;
2544
+ }).map((index) => domain[index]);
2283
2545
  };
2284
2546
  const guessOptimalNumTicks = (data, chartWidth, tickFormatter) => {
2285
- const xScale = (0, _visx_scale.scaleTime)({ domain: [Math.min(...data.map((datom) => datom.data.at(0)?.date)), Math.max(...data.map((datom) => datom.data.at(-1)?.date))] });
2547
+ const span = getSpan(data);
2548
+ if (!span) return 1;
2549
+ const xScale = (0, _visx_scale.scaleTime)({ domain: [span.minX, span.maxX] });
2286
2550
  const upperBound = Math.min(data[0]?.data.length || 3, Math.ceil(chartWidth / X_TICK_WIDTH));
2287
2551
  let secondBestGuess = 1;
2288
2552
  for (let numTicks = upperBound; numTicks > 1; --numTicks) {
@@ -2319,9 +2583,11 @@ function withResponsive(WrappedComponent) {
2319
2583
  enableDebounceLeadingCall: true
2320
2584
  });
2321
2585
  const hasAspectRatio = aspectRatio !== void 0 && aspectRatio > 0;
2586
+ const [scopeNode, setScopeNode] = (0, react.useState)(null);
2322
2587
  const wrapperRef = (0, react.useRef)(null);
2323
2588
  const setWrapperRef = (0, react.useCallback)((node) => {
2324
2589
  wrapperRef.current = node;
2590
+ setScopeNode(node);
2325
2591
  if (typeof parentRef === "function") parentRef(node);
2326
2592
  else if (parentRef) parentRef.current = node;
2327
2593
  }, [parentRef]);
@@ -2367,14 +2633,17 @@ function withResponsive(WrappedComponent) {
2367
2633
  ...width !== void 0 ? { width } : null,
2368
2634
  ...height !== void 0 ? { height } : null
2369
2635
  },
2370
- children: hasAspectRatio ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2371
- className: with_responsive_module_default.content,
2372
- style: {
2373
- width: boxWidth,
2374
- height: boxHeight
2375
- },
2376
- children: wrappedComponent
2377
- }) : wrappedComponent
2636
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartScopeContext.Provider, {
2637
+ value: scopeNode,
2638
+ children: hasAspectRatio ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2639
+ className: with_responsive_module_default.content,
2640
+ style: {
2641
+ width: boxWidth,
2642
+ height: boxHeight
2643
+ },
2644
+ children: wrappedComponent
2645
+ }) : wrappedComponent
2646
+ })
2378
2647
  });
2379
2648
  };
2380
2649
  }
@@ -2809,11 +3078,12 @@ const getVerticalAnchor = (subjectType, isFlippedVertically, y, yMax, height) =>
2809
3078
  };
2810
3079
  const LineChartAnnotation = ({ datum, title, subtitle, subjectType = "circle", styles: datumStyles, testId, renderLabel, renderLabelPopover }) => {
2811
3080
  const providerTheme = useGlobalChartsTheme();
3081
+ const scopeElement = useChartScopeElement();
2812
3082
  const { xScale, yScale } = (0, react.useContext)(_visx_xychart.DataContext) || {};
2813
3083
  const labelRef = (0, react.useRef)(null);
2814
3084
  const [height, setHeight] = (0, react.useState)(null);
2815
3085
  const styles = (0, deepmerge.default)(providerTheme.annotationStyles ?? {}, datumStyles ?? {});
2816
- const resolveColor = (value) => value ? resolveCssVariable(value) ?? value : value;
3086
+ const resolveColor = (value) => value ? resolveCssVariable(value, scopeElement) ?? value : value;
2817
3087
  (0, react.useEffect)(() => {
2818
3088
  if (labelRef.current?.getBBox) {
2819
3089
  const bbox = labelRef.current.getBBox();
@@ -3060,8 +3330,8 @@ const LineChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
3060
3330
  const legendShape = legend.shape ?? "line";
3061
3331
  const legendPosition = legend.position ?? "bottom";
3062
3332
  const providerTheme = useGlobalChartsTheme();
3063
- const resolvedBackgroundColor = resolveCssVariable(providerTheme.backgroundColor) ?? providerTheme.backgroundColor;
3064
3333
  const theme = useXYChartTheme(data);
3334
+ const resolvedBackgroundColor = theme.backgroundColor ?? providerTheme.backgroundColor;
3065
3335
  const chartId = useChartId(providedChartId);
3066
3336
  const chartRef = (0, react.useRef)(null);
3067
3337
  const [selectedIndex, setSelectedIndex] = (0, react.useState)(void 0);
@@ -3837,7 +4107,7 @@ const AreaChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
3837
4107
  stacked,
3838
4108
  stackOffset,
3839
4109
  getElementStyles,
3840
- strokeColor: resolveCssVariable(providerTheme.backgroundColor) ?? providerTheme.backgroundColor
4110
+ strokeColor: theme.backgroundColor ?? providerTheme.backgroundColor
3841
4111
  })] }),
3842
4112
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AreaChartScalesRef, {
3843
4113
  chartRef: internalChartRef,
@@ -3997,11 +4267,73 @@ const TruncatedYTickComponent = createTruncatedTickComponent("y");
3997
4267
  const BASE_BAND_PADDING = .2;
3998
4268
  /** Inner padding of the category band scale (the base gap between ticks). */
3999
4269
  const BASE_BAND_PADDING_INNER = .1;
4000
- const formatDateTick = (timestamp) => {
4001
- return new Date(timestamp).toLocaleDateString(void 0, {
4002
- month: "short",
4270
+ /** Ticks each axis carries unless the caller asks for a different count. */
4271
+ const DEFAULT_NUM_TICKS = 4;
4272
+ const ALL_RENDERED = () => true;
4273
+ const TOOLTIP_FORMAT_BY_RESOLUTION = {
4274
+ hour: {
4275
+ year: "numeric",
4276
+ month: "long",
4277
+ day: "numeric",
4278
+ hour: "numeric",
4279
+ hour12: true
4280
+ },
4281
+ day: {
4282
+ year: "numeric",
4283
+ month: "long",
4003
4284
  day: "numeric"
4004
- });
4285
+ },
4286
+ month: {
4287
+ year: "numeric",
4288
+ month: "long"
4289
+ },
4290
+ year: { year: "numeric" }
4291
+ };
4292
+ /**
4293
+ * Labels one bar's bucket, at the bucket's own granularity — finer than the
4294
+ * ticks whenever the overall time span coarsens the axis.
4295
+ *
4296
+ * @param data - Date-based series, already parsed and sorted by `useChartDataTransform`.
4297
+ * @param tickResolution - Caller-declared bucket resolution, when known.
4298
+ * @return Tooltip label formatter.
4299
+ */
4300
+ const getTooltipFormatter = (data, tickResolution) => {
4301
+ if (tickResolution === "week") return (timestamp) => (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n.__)("Week of %s", "jetpack-charts"), new Date(timestamp).toLocaleDateString(void 0, {
4302
+ year: "numeric",
4303
+ month: "long",
4304
+ day: "numeric"
4305
+ }));
4306
+ const format = TOOLTIP_FORMAT_BY_RESOLUTION[getBucketResolution(data, tickResolution)] ?? TOOLTIP_FORMAT_BY_RESOLUTION.day;
4307
+ return (timestamp) => new Date(timestamp).toLocaleString(void 0, format);
4308
+ };
4309
+ const identity = (label) => label;
4310
+ const byBucket = (format) => (bucket) => typeof bucket === "string" ? bucket : format(Number(bucket));
4311
+ const bucketAccessor = (d) => d?.label || d?.date;
4312
+ /**
4313
+ * The buckets the band axis can put a tick on.
4314
+ *
4315
+ * Built from the accessor visx is given, over the same series in the same order,
4316
+ * so that a tick value is a key the scale actually holds; `scaleBand` returns
4317
+ * `undefined` for one it does not, which parks the tick at the origin. Null
4318
+ * rather than a partial list when any bucket is labelled instead of dated.
4319
+ *
4320
+ * @param data - Every series handed to the chart.
4321
+ * @param isSeriesRendered - Whether visx mounts a series, i.e. the legend shows it.
4322
+ * @return Bucket dates in axis order, or null if the axis cannot be ticked by date.
4323
+ */
4324
+ const getBandDomain = (data, isSeriesRendered) => {
4325
+ const registry = {};
4326
+ for (const series of data) {
4327
+ if (series.options?.type === "comparison" || !isSeriesRendered(series)) continue;
4328
+ registry[series.label] = series;
4329
+ }
4330
+ const byTimestamp = /* @__PURE__ */ new Map();
4331
+ for (const key of Object.keys(registry)) for (const point of registry[key].data) {
4332
+ const bucket = bucketAccessor(point);
4333
+ if (!(bucket instanceof Date)) return null;
4334
+ if (!byTimestamp.has(bucket.getTime())) byTimestamp.set(bucket.getTime(), bucket);
4335
+ }
4336
+ return byTimestamp.size ? [...byTimestamp.values()] : null;
4005
4337
  };
4006
4338
  /**
4007
4339
  * Get the group padding of a scale.
@@ -4015,12 +4347,26 @@ const getGroupPadding = (scale) => {
4015
4347
  /**
4016
4348
  * Returns the merged options for the bar chart, including axis and scale configuration based on the orientation.
4017
4349
  *
4018
- * @param data - The data to be displayed in the chart.
4019
- * @param horizontal - Whether the chart is horizontal or vertical.
4020
- * @param options - The options for the chart.
4350
+ * @param data - The data to be displayed in the chart.
4351
+ * @param horizontal - Whether the chart is horizontal or vertical.
4352
+ * @param options - The options for the chart.
4353
+ * @param isSeriesRendered - Whether visx mounts a series, i.e. the legend shows it.
4021
4354
  * @return The merged options for the chart.
4022
4355
  */
4023
- function useBarChartOptions(data, horizontal, options = {}) {
4356
+ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = ALL_RENDERED) {
4357
+ const stableOptions = useDeepMemo(options);
4358
+ const axisConfig = (0, react.useMemo)(() => {
4359
+ const { labelOverflow: xLabelOverflow, tickResolution: xTickResolution, ...xAxisOptions } = stableOptions.axis?.x || {};
4360
+ const { labelOverflow: yLabelOverflow, tickResolution: yTickResolution, ...yAxisOptions } = stableOptions.axis?.y || {};
4361
+ return {
4362
+ xLabelOverflow,
4363
+ yLabelOverflow,
4364
+ xAxisOptions,
4365
+ yAxisOptions,
4366
+ tickResolution: horizontal ? yTickResolution : xTickResolution
4367
+ };
4368
+ }, [stableOptions, horizontal]);
4369
+ const { tickResolution } = axisConfig;
4024
4370
  const defaultOptions = (0, react.useMemo)(() => {
4025
4371
  const bandScale = {
4026
4372
  type: "band",
@@ -4032,19 +4378,26 @@ function useBarChartOptions(data, horizontal, options = {}) {
4032
4378
  nice: true,
4033
4379
  zero: false
4034
4380
  };
4035
- const labelFormatter = data?.[0]?.data?.[0]?.label ? (label) => label : formatDateTick;
4381
+ const hasLabels = Boolean(data?.[0]?.data?.[0]?.label);
4382
+ const timeTickFormatter = hasLabels ? null : getFormatter(data, tickResolution);
4383
+ const labelFormatter = timeTickFormatter ? byBucket(timeTickFormatter) : identity;
4384
+ const tooltipDatumFormatter = hasLabels ? labelFormatter : byBucket(getTooltipFormatter(data, tickResolution));
4036
4385
  const valueFormatter = _automattic_number_formatters.formatNumberCompact;
4037
- const labelAccessor = (d) => d?.label || d?.date;
4386
+ const bandDomain = timeTickFormatter ? getBandDomain(data, isSeriesRendered) : null;
4038
4387
  const valueAccessor = (d) => {
4039
4388
  const enhancedPoint = d;
4040
4389
  return enhancedPoint?.visualValue !== void 0 ? enhancedPoint.visualValue : d?.value;
4041
4390
  };
4042
4391
  return {
4392
+ timeAxis: bandDomain && timeTickFormatter && {
4393
+ domain: bandDomain,
4394
+ tickFormatter: timeTickFormatter
4395
+ },
4043
4396
  vertical: {
4044
4397
  xTickFormat: labelFormatter,
4045
4398
  yTickFormat: valueFormatter,
4046
- tooltipLabelFormatter: labelFormatter,
4047
- xAccessor: labelAccessor,
4399
+ tooltipLabelFormatter: tooltipDatumFormatter,
4400
+ xAccessor: bucketAccessor,
4048
4401
  yAccessor: valueAccessor,
4049
4402
  gridVisibility: "x",
4050
4403
  xScale: bandScale,
@@ -4053,20 +4406,24 @@ function useBarChartOptions(data, horizontal, options = {}) {
4053
4406
  horizontal: {
4054
4407
  xTickFormat: valueFormatter,
4055
4408
  yTickFormat: labelFormatter,
4056
- tooltipLabelFormatter: labelFormatter,
4409
+ tooltipLabelFormatter: tooltipDatumFormatter,
4057
4410
  xAccessor: valueAccessor,
4058
- yAccessor: labelAccessor,
4411
+ yAccessor: bucketAccessor,
4059
4412
  gridVisibility: "y",
4060
4413
  xScale: linearScale,
4061
4414
  yScale: bandScale
4062
4415
  }
4063
4416
  };
4064
- }, [data]);
4417
+ }, [
4418
+ data,
4419
+ tickResolution,
4420
+ isSeriesRendered
4421
+ ]);
4065
4422
  return (0, react.useMemo)(() => {
4066
4423
  const { xTickFormat, yTickFormat, tooltipLabelFormatter: defaultTooltipLabelFormatter, xAccessor, yAccessor, gridVisibility, xScale: baseXScale, yScale: baseYScale } = defaultOptions[horizontal ? "horizontal" : "vertical"];
4067
4424
  let valueScaleDomainOverride = {};
4068
4425
  if (data.some((s) => s.options?.type === "comparison")) {
4069
- if (!(!horizontal ? options.yScale?.domain : options.xScale?.domain)) {
4426
+ if (!(!horizontal ? stableOptions.yScale?.domain : stableOptions.xScale?.domain)) {
4070
4427
  const allValues = [];
4071
4428
  data.forEach((series) => {
4072
4429
  series.data.forEach((d) => {
@@ -4080,17 +4437,20 @@ function useBarChartOptions(data, horizontal, options = {}) {
4080
4437
  }
4081
4438
  const xScale = {
4082
4439
  ...baseXScale,
4083
- ...options.xScale || {},
4440
+ ...stableOptions.xScale || {},
4084
4441
  ...horizontal ? valueScaleDomainOverride : {}
4085
4442
  };
4086
4443
  const yScale = {
4087
4444
  ...baseYScale,
4088
- ...options.yScale || {},
4445
+ ...stableOptions.yScale || {},
4089
4446
  ...!horizontal ? valueScaleDomainOverride : {}
4090
4447
  };
4091
- const providedToolTipLabelFormatter = horizontal ? options.axis?.y?.tickFormat : options.axis?.x?.tickFormat;
4092
- const { labelOverflow: xLabelOverflow, ...xAxisOptions } = options.axis?.x || {};
4093
- const { labelOverflow: yLabelOverflow, ...yAxisOptions } = options.axis?.y || {};
4448
+ const { xLabelOverflow, yLabelOverflow, xAxisOptions, yAxisOptions } = axisConfig;
4449
+ const providedToolTipLabelFormatter = horizontal ? yAxisOptions.tickFormat : xAxisOptions.tickFormat;
4450
+ const { timeAxis } = defaultOptions;
4451
+ const dateAxisOptions = horizontal ? yAxisOptions : xAxisOptions;
4452
+ const bandTickValues = timeAxis && !dateAxisOptions.tickFormat ? getBandTickValues(timeAxis.domain, timeAxis.tickFormatter, dateAxisOptions.numTicks ?? DEFAULT_NUM_TICKS) : null;
4453
+ const dateAxisTickValues = bandTickValues ? { tickValues: bandTickValues } : {};
4094
4454
  return {
4095
4455
  gridVisibility,
4096
4456
  xScale,
@@ -4102,15 +4462,17 @@ function useBarChartOptions(data, horizontal, options = {}) {
4102
4462
  axis: {
4103
4463
  x: {
4104
4464
  orientation: "bottom",
4105
- numTicks: 4,
4465
+ numTicks: DEFAULT_NUM_TICKS,
4106
4466
  tickFormat: xTickFormat,
4467
+ ...horizontal ? {} : dateAxisTickValues,
4107
4468
  ...xLabelOverflow === "ellipsis" ? { tickComponent: TruncatedXTickComponent } : {},
4108
4469
  ...xAxisOptions
4109
4470
  },
4110
4471
  y: {
4111
4472
  orientation: "left",
4112
- numTicks: 4,
4473
+ numTicks: DEFAULT_NUM_TICKS,
4113
4474
  tickFormat: yTickFormat,
4475
+ ...horizontal ? dateAxisTickValues : {},
4114
4476
  ...yLabelOverflow === "ellipsis" ? { tickComponent: TruncatedYTickComponent } : {},
4115
4477
  ...yAxisOptions
4116
4478
  }
@@ -4120,7 +4482,8 @@ function useBarChartOptions(data, horizontal, options = {}) {
4120
4482
  };
4121
4483
  }, [
4122
4484
  defaultOptions,
4123
- options,
4485
+ axisConfig,
4486
+ stableOptions,
4124
4487
  horizontal,
4125
4488
  data
4126
4489
  ]);
@@ -4282,7 +4645,13 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4282
4645
  valueAxisLength: horizontal ? width : height
4283
4646
  });
4284
4647
  const legendItems = useChartLegendItems(dataSorted, (0, react.useMemo)(() => ({ collapseGroups: legendCollapseGroups }), [legendCollapseGroups]));
4285
- const chartOptions = useBarChartOptions(dataWithVisibleZeros, horizontal, options);
4648
+ const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
4649
+ const isSeriesRendered = (0, react.useCallback)((series) => !chartId || !legendInteractive || isSeriesVisible(chartId, series.label), [
4650
+ chartId,
4651
+ legendInteractive,
4652
+ isSeriesVisible
4653
+ ]);
4654
+ const chartOptions = useBarChartOptions(dataWithVisibleZeros, horizontal, options, isSeriesRendered);
4286
4655
  const defaultMargin = useChartMargin(height, chartOptions, dataSorted, theme, horizontal);
4287
4656
  const chartRef = (0, react.useRef)(null);
4288
4657
  const { legendChildren, nonLegendChildren } = useChartChildren(children, "BarChart");
@@ -4301,24 +4670,11 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4301
4670
  chartRef,
4302
4671
  totalPoints: Math.max(0, ...primarySeriesForNav.map((s) => s.data?.length || 0)) * primarySeriesForNav.length
4303
4672
  });
4304
- const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
4305
- const seriesWithVisibility = (0, react.useMemo)(() => {
4306
- if (!chartId || !legendInteractive) return dataWithVisibleZeros.map((series, index) => ({
4307
- series,
4308
- index,
4309
- isVisible: true
4310
- }));
4311
- return dataWithVisibleZeros.map((series, index) => ({
4312
- series,
4313
- index,
4314
- isVisible: isSeriesVisible(chartId, series.label)
4315
- }));
4316
- }, [
4317
- dataWithVisibleZeros,
4318
- chartId,
4319
- isSeriesVisible,
4320
- legendInteractive
4321
- ]);
4673
+ const seriesWithVisibility = (0, react.useMemo)(() => dataWithVisibleZeros.map((series, index) => ({
4674
+ series,
4675
+ index,
4676
+ isVisible: isSeriesRendered(series)
4677
+ })), [dataWithVisibleZeros, isSeriesRendered]);
4322
4678
  const allSeriesHidden = (0, react.useMemo)(() => {
4323
4679
  return seriesWithVisibility.every(({ isVisible }) => !isVisible);
4324
4680
  }, [seriesWithVisibility]);
@@ -4878,12 +5234,22 @@ const ConversionFunnelChartInternal = ({ mainRate, changeIndicator, steps, loadi
4878
5234
  const { getElementStyles, isColorPaletteResolved } = useGlobalChartsContext();
4879
5235
  const chartRef = (0, react.useRef)(null);
4880
5236
  const selectedBarRef = (0, react.useRef)(null);
5237
+ const [scopeNode, setScopeNode] = (0, react.useState)(null);
4881
5238
  const { tooltipData, tooltipLeft, tooltipTop, tooltipOpen, showTooltip, hideTooltip } = (0, _visx_tooltip.useTooltip)();
4882
5239
  const { handleBarClick, handleBarKeyDown, clearSelection, getStepState } = useFunnelSelection(hideTooltip);
4883
5240
  const { containerRef: portalContainerRef, TooltipInPortal, containerBounds } = (0, _visx_tooltip.useTooltipInPortal)({
4884
5241
  detectBounds: true,
4885
5242
  scroll: true
4886
5243
  });
5244
+ const portalContainerRefRef = (0, react.useRef)(portalContainerRef);
5245
+ (0, react.useEffect)(() => {
5246
+ portalContainerRefRef.current = portalContainerRef;
5247
+ }, [portalContainerRef]);
5248
+ const setChartRef = (0, react.useCallback)((node) => {
5249
+ portalContainerRefRef.current(node);
5250
+ chartRef.current = node;
5251
+ setScopeNode(node);
5252
+ }, []);
4887
5253
  const clearSelectionAndRef = (0, react.useCallback)(() => {
4888
5254
  clearSelection();
4889
5255
  selectedBarRef.current = null;
@@ -5029,6 +5395,7 @@ const ConversionFunnelChartInternal = ({ mainRate, changeIndicator, steps, loadi
5029
5395
  direction: "column",
5030
5396
  align: "center",
5031
5397
  justify: "center",
5398
+ ref: setScopeNode,
5032
5399
  className: (0, clsx.default)(conversion_funnel_chart_module_default["conversion-funnel-chart"], loading && conversion_funnel_chart_module_default["conversion-funnel-chart--loading"], className),
5033
5400
  style: {
5034
5401
  ...style,
@@ -5040,80 +5407,80 @@ const ConversionFunnelChartInternal = ({ mainRate, changeIndicator, steps, loadi
5040
5407
  })
5041
5408
  });
5042
5409
  const maxRate = Math.max(...steps.map((step) => step.rate));
5043
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_wordpress_ui.Stack, {
5410
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_wordpress_ui.Stack, {
5044
5411
  direction: "column",
5045
5412
  gap: "xl",
5046
- ref: (node) => {
5047
- portalContainerRef(node);
5048
- chartRef.current = node;
5049
- },
5413
+ ref: setChartRef,
5050
5414
  className: (0, clsx.default)(conversion_funnel_chart_module_default["conversion-funnel-chart"], loading && conversion_funnel_chart_module_default["conversion-funnel-chart--loading"], className),
5051
5415
  style: {
5052
5416
  ...style,
5053
5417
  height: resolvedHeight
5054
5418
  },
5055
- children: [renderMainMetric ? renderMainMetric({
5056
- mainRate,
5057
- changeIndicator,
5058
- className: conversion_funnel_chart_module_default["main-metric"],
5059
- changeColor
5060
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_wordpress_ui.Stack, {
5061
- direction: "row",
5062
- align: "baseline",
5063
- gap: "sm",
5064
- className: conversion_funnel_chart_module_default["main-metric"],
5065
- children: renderDefaultMainMetric()
5066
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_wordpress_ui.Stack, {
5067
- direction: "row",
5068
- align: "flex-end",
5069
- gap: "lg",
5070
- className: conversion_funnel_chart_module_default["funnel-container"],
5071
- children: steps.map((step, index) => {
5072
- const barHeight = step.rate / maxRate * 100;
5073
- const { isBlurred } = getStepState(step.id);
5074
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_wordpress_ui.Stack, {
5075
- direction: "column",
5076
- className: (0, clsx.default)(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"]),
5077
- gap: "xl",
5078
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_wordpress_ui.Stack, {
5419
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ChartScopeContext.Provider, {
5420
+ value: scopeNode,
5421
+ children: [renderMainMetric ? renderMainMetric({
5422
+ mainRate,
5423
+ changeIndicator,
5424
+ className: conversion_funnel_chart_module_default["main-metric"],
5425
+ changeColor
5426
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_wordpress_ui.Stack, {
5427
+ direction: "row",
5428
+ align: "baseline",
5429
+ gap: "sm",
5430
+ className: conversion_funnel_chart_module_default["main-metric"],
5431
+ children: renderDefaultMainMetric()
5432
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_wordpress_ui.Stack, {
5433
+ direction: "row",
5434
+ align: "flex-end",
5435
+ gap: "lg",
5436
+ className: conversion_funnel_chart_module_default["funnel-container"],
5437
+ children: steps.map((step, index) => {
5438
+ const barHeight = step.rate / maxRate * 100;
5439
+ const { isBlurred } = getStepState(step.id);
5440
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_wordpress_ui.Stack, {
5079
5441
  direction: "column",
5080
- gap: "xs",
5081
- children: [renderStepLabel ? renderStepLabel({
5082
- step,
5083
- index,
5084
- className: conversion_funnel_chart_module_default["step-label"]
5085
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5086
- className: conversion_funnel_chart_module_default["step-label"],
5087
- children: step.label
5088
- }), renderStepRate ? renderStepRate({
5089
- step,
5090
- index,
5091
- className: conversion_funnel_chart_module_default["step-rate"]
5092
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5093
- className: conversion_funnel_chart_module_default["step-rate"],
5094
- children: formatPercentage(step.rate)
5442
+ className: (0, clsx.default)(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"]),
5443
+ gap: "xl",
5444
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_wordpress_ui.Stack, {
5445
+ direction: "column",
5446
+ gap: "xs",
5447
+ children: [renderStepLabel ? renderStepLabel({
5448
+ step,
5449
+ index,
5450
+ className: conversion_funnel_chart_module_default["step-label"]
5451
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5452
+ className: conversion_funnel_chart_module_default["step-label"],
5453
+ children: step.label
5454
+ }), renderStepRate ? renderStepRate({
5455
+ step,
5456
+ index,
5457
+ className: conversion_funnel_chart_module_default["step-rate"]
5458
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5459
+ className: conversion_funnel_chart_module_default["step-rate"],
5460
+ children: formatPercentage(step.rate)
5461
+ })]
5462
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_wordpress_ui.Stack, {
5463
+ direction: "column",
5464
+ justify: "flex-end",
5465
+ className: conversion_funnel_chart_module_default["bar-container"],
5466
+ onClick: stepHandlers.get(step.id)?.onClick,
5467
+ onKeyDown: stepHandlers.get(step.id)?.onKeyDown,
5468
+ role: "button",
5469
+ tabIndex: isBlurred ? -1 : 0,
5470
+ "aria-label": step.label,
5471
+ style: { backgroundColor: barBackgroundColor },
5472
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5473
+ className: (0, clsx.default)(conversion_funnel_chart_module_default["funnel-bar"], { [conversion_funnel_chart_module_default["funnel-bar--animated"]]: animation && !loading && !prefersReducedMotion }),
5474
+ style: {
5475
+ height: `${barHeight}%`,
5476
+ backgroundColor: barColor
5477
+ }
5478
+ })
5095
5479
  })]
5096
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_wordpress_ui.Stack, {
5097
- direction: "column",
5098
- justify: "flex-end",
5099
- className: conversion_funnel_chart_module_default["bar-container"],
5100
- onClick: stepHandlers.get(step.id)?.onClick,
5101
- onKeyDown: stepHandlers.get(step.id)?.onKeyDown,
5102
- role: "button",
5103
- tabIndex: isBlurred ? -1 : 0,
5104
- "aria-label": step.label,
5105
- style: { backgroundColor: barBackgroundColor },
5106
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5107
- className: (0, clsx.default)(conversion_funnel_chart_module_default["funnel-bar"], { [conversion_funnel_chart_module_default["funnel-bar--animated"]]: animation && !loading && !prefersReducedMotion }),
5108
- style: {
5109
- height: `${barHeight}%`,
5110
- backgroundColor: barColor
5111
- }
5112
- })
5113
- })]
5114
- }, step.id);
5115
- })
5116
- })]
5480
+ }, step.id);
5481
+ })
5482
+ })]
5483
+ })
5117
5484
  }), tooltipOpen && tooltipData && (() => {
5118
5485
  const tooltipContent = renderTooltip ? renderTooltip({
5119
5486
  step: tooltipData,
@@ -5126,7 +5493,7 @@ const ConversionFunnelChartInternal = ({ mainRate, changeIndicator, steps, loadi
5126
5493
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipInPortal, {
5127
5494
  top: tooltipTop,
5128
5495
  left: tooltipLeft,
5129
- className: conversion_funnel_chart_module_default["tooltip-wrapper"],
5496
+ className: (0, clsx.default)("a8c-charts-scope", conversion_funnel_chart_module_default["tooltip-wrapper"]),
5130
5497
  children: tooltipContent
5131
5498
  }, Math.random());
5132
5499
  })()] });
@@ -5276,6 +5643,7 @@ function normalizeGeoChartError(eventArgs) {
5276
5643
  */
5277
5644
  const GeoChartInternal = ({ className, data, width, height, region = "world", resolution = "countries", onError, renderPlaceholder }) => {
5278
5645
  const { getElementStyles, theme: { geoChart: { featureFillColor }, backgroundColor } } = useGlobalChartsContext();
5646
+ const scopeElement = useChartScopeElement();
5279
5647
  const containerRef = (0, react.useRef)(null);
5280
5648
  const reportedErrorIdsRef = (0, react.useRef)(/* @__PURE__ */ new Set());
5281
5649
  (0, react.useEffect)(() => {
@@ -5308,8 +5676,8 @@ const GeoChartInternal = ({ className, data, width, height, region = "world", re
5308
5676
  });
5309
5677
  const fullColorHex = getElementStyles({ index: 0 }).color;
5310
5678
  const lightColorHex = lightenHexColor(fullColorHex, .8);
5311
- const backgroundColorHex = normalizeColorToHex(backgroundColor, null, resolveCssVariable) || DEFAULT_BACKGROUND_COLOR;
5312
- const defaultFillColorHex = normalizeColorToHex(featureFillColor, null, resolveCssVariable) || DEFAULT_FEATURE_FILL_COLOR;
5679
+ const backgroundColorHex = normalizeColorToHex(backgroundColor, scopeElement, resolveCssVariable) || DEFAULT_BACKGROUND_COLOR;
5680
+ const defaultFillColorHex = normalizeColorToHex(featureFillColor, scopeElement, resolveCssVariable) || DEFAULT_FEATURE_FILL_COLOR;
5313
5681
  const sanitizedData = (0, react.useMemo)(() => {
5314
5682
  if (data.length === 0) return {
5315
5683
  data,
@@ -5569,7 +5937,8 @@ const HeatmapLegend = ({ steps = 5, lessLabel, moreLabel }) => {
5569
5937
  const CELL_MIX_FLOOR = .15;
5570
5938
  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 }) => {
5571
5939
  const chartId = useChartId(providedChartId);
5572
- const { getElementStyles, resolveThemeColor, theme } = useGlobalChartsContext();
5940
+ const { getElementStyles, theme } = useGlobalChartsContext();
5941
+ const scopeElement = useChartScopeElement();
5573
5942
  const { heatmapChart: heatmapChartSettings } = theme;
5574
5943
  const { nonLegendChildren } = useChartChildren(children, "HeatmapChart");
5575
5944
  const [selectedIndex, setSelectedIndex] = (0, react.useState)();
@@ -5584,7 +5953,7 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
5584
5953
  index: 0,
5585
5954
  overrideColor: primaryColor || heatmapChartSettings.primaryColor
5586
5955
  });
5587
- const chartBackgroundHex = resolveThemeColor(theme.backgroundColor);
5956
+ const chartBackgroundHex = normalizeColorToHex(theme.backgroundColor, scopeElement, resolveCssVariable);
5588
5957
  const primaryHex = normalizeColorToHex(primaryColorHex);
5589
5958
  const cellHasLightText = (intensity) => isValidHexColor(primaryHex) && isValidHexColor(chartBackgroundHex) && prefersLightText(mixHexColors(primaryHex, chartBackgroundHex, 1 - (CELL_MIX_FLOOR + (1 - CELL_MIX_FLOOR) * intensity)));
5590
5959
  const extent = (0, react.useMemo)(() => getValueExtent(data), [data]);
@@ -5811,6 +6180,7 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
5811
6180
  top: tooltipTop,
5812
6181
  left: tooltipLeft,
5813
6182
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6183
+ className: "a8c-charts-scope",
5814
6184
  role: "tooltip",
5815
6185
  tabIndex: -1,
5816
6186
  children: (renderTooltip ?? defaultRenderTooltip)(tooltipData)
@@ -9146,6 +9516,7 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
9146
9516
  top: tooltipTop || 0,
9147
9517
  left: tooltipLeft || 0,
9148
9518
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9519
+ className: "a8c-charts-scope",
9149
9520
  role: "tooltip",
9150
9521
  children: renderTooltip({ tooltipData })
9151
9522
  })
@@ -9444,6 +9815,7 @@ const PieSemiCircleChartInternal = ({ data, chartId: providedChartId, width: pro
9444
9815
  top: tooltipTop || 0,
9445
9816
  left: tooltipLeft || 0,
9446
9817
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9818
+ className: "a8c-charts-scope",
9447
9819
  role: "tooltip",
9448
9820
  children: renderTooltip({ tooltipData })
9449
9821
  })
@@ -9731,7 +10103,7 @@ const Icon = ({ direction }) => {
9731
10103
  function TrendIndicator({ direction, value, className, style, showIcon = true }) {
9732
10104
  const ariaLabel = `${DIRECTION_LABELS[direction]}: ${value}`;
9733
10105
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
9734
- className: (0, clsx.default)(trend_indicator_module_default["trend-indicator"], trend_indicator_module_default[`trend-indicator--${direction}`], className),
10106
+ className: (0, clsx.default)(useStandaloneScopeClass(), trend_indicator_module_default["trend-indicator"], trend_indicator_module_default[`trend-indicator--${direction}`], className),
9735
10107
  style,
9736
10108
  "aria-label": ariaLabel,
9737
10109
  children: [showIcon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { direction }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -9785,7 +10157,10 @@ exports.parseHslString = parseHslString;
9785
10157
  exports.parseRgbString = parseRgbString;
9786
10158
  exports.prefersLightText = prefersLightText;
9787
10159
  exports.relativeLuminance = relativeLuminance;
10160
+ exports.resolveCssVariable = resolveCssVariable;
9788
10161
  exports.useChartLegendItems = useChartLegendItems;
10162
+ exports.useChartRegistration = useChartRegistration;
10163
+ exports.useChartScopeElement = useChartScopeElement;
9789
10164
  exports.useGlobalChartsContext = useGlobalChartsContext;
9790
10165
  exports.useGlobalChartsTheme = useGlobalChartsTheme;
9791
10166
  exports.useLeaderboardLegendItems = useLeaderboardLegendItems;