@automattic/charts 4.7.0 → 4.8.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 (61) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/TOKENS.md +6 -3
  3. package/dist/index.cjs +212 -95
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +10 -9
  6. package/dist/index.d.cts +60 -20
  7. package/dist/index.d.ts +60 -20
  8. package/dist/index.js +212 -97
  9. package/dist/index.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/charts/area-chart/area-chart.tsx +2 -2
  12. package/src/charts/area-chart/test/area-chart.test.tsx +1 -1
  13. package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +1 -1
  14. package/src/charts/bar-chart/bar-chart.module.scss +7 -0
  15. package/src/charts/bar-chart/bar-chart.tsx +20 -14
  16. package/src/charts/bar-chart/private/comparison-bars.tsx +1 -0
  17. package/src/charts/bar-chart/test/bar-chart.test.tsx +25 -0
  18. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +1 -1
  19. package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +9 -0
  20. package/src/charts/heatmap-chart/heatmap-chart.module.scss +6 -4
  21. package/src/charts/heatmap-chart/heatmap-chart.tsx +39 -29
  22. package/src/charts/heatmap-chart/private/heatmap-context.ts +2 -2
  23. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +2 -2
  24. package/src/charts/heatmap-chart/private/heatmap-scale.ts +74 -0
  25. package/src/charts/heatmap-chart/private/index.ts +2 -1
  26. package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +24 -6
  27. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +99 -7
  28. package/src/charts/heatmap-chart/test/heatmap-scale.test.ts +78 -0
  29. package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +18 -6
  30. package/src/charts/heatmap-chart/types.ts +4 -3
  31. package/src/charts/line-chart/line-chart.tsx +2 -2
  32. package/src/charts/pie-chart/pie-chart.tsx +4 -4
  33. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +4 -4
  34. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +1 -1
  35. package/src/charts/private/test/label-text-color.test.ts +6 -8
  36. package/src/components/tooltip/base-tooltip.tsx +31 -32
  37. package/src/components/tooltip/index.ts +3 -1
  38. package/src/components/tooltip/private/bounded-tooltip.tsx +15 -15
  39. package/src/components/tooltip/private/label-value-content.tsx +8 -0
  40. package/src/components/tooltip/private/xy-chart-tooltip-overlay.tsx +336 -0
  41. package/src/components/tooltip/test/tool-tip.test.tsx +8 -6
  42. package/src/components/tooltip/test/tooltip-box.test.tsx +53 -0
  43. package/src/components/tooltip/test/xy-chart-tooltip-overlay.test.tsx +445 -0
  44. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +282 -365
  45. package/src/components/tooltip/{base-tooltip.module.scss → tooltip-box.module.scss} +5 -8
  46. package/src/components/tooltip/tooltip-box.tsx +43 -0
  47. package/src/components/tooltip/types.ts +7 -1
  48. package/src/components/tooltip/xy-chart-tooltip.tsx +410 -300
  49. package/src/hooks/test/use-chart-margin.test.tsx +8 -4
  50. package/src/hooks/use-chart-margin.tsx +21 -18
  51. package/src/index.ts +8 -3
  52. package/src/providers/chart-context/global-charts-provider.tsx +5 -14
  53. package/src/providers/chart-context/hooks/use-default-hidden-series.ts +1 -1
  54. package/src/providers/chart-context/private/catalog-pointers.ts +1 -0
  55. package/src/providers/chart-context/private/resolve-opaque-hex.ts +16 -0
  56. package/src/providers/chart-context/test/use-default-hidden-series.test.tsx +1 -1
  57. package/src/styles/chart-scope.scss +1 -0
  58. package/src/test-utils/zoned-parsing-suite.ts +0 -10
  59. package/src/utils/test/date-formatting.test.ts +0 -10
  60. package/src/components/tooltip/accessible-tooltip.tsx +0 -433
  61. package/src/components/tooltip/test/accessible-tooltip.test.tsx +0 -331
package/dist/index.cjs CHANGED
@@ -29,12 +29,12 @@ let clsx = require("clsx");
29
29
  clsx = __toESM(clsx, 1);
30
30
  let react = require("react");
31
31
  react = __toESM(react);
32
- let _visx_vendor_d3_color = require("@visx/vendor/d3-color");
33
32
  let date_fns = require("date-fns");
34
33
  let _date_fns_tz = require("@date-fns/tz");
35
34
  let _visx_text = require("@visx/text");
36
35
  let deepmerge = require("deepmerge");
37
36
  deepmerge = __toESM(deepmerge, 1);
37
+ let _visx_vendor_d3_color = require("@visx/vendor/d3-color");
38
38
  let react_jsx_runtime = require("react/jsx-runtime");
39
39
  let _visx_tooltip = require("@visx/tooltip");
40
40
  let _visx_scale = require("@visx/scale");
@@ -836,6 +836,7 @@ const CATALOG_POINTERS = {
836
836
  annotation: "var(--a8c-charts-color-annotation, #1e1e1e)",
837
837
  surface: "var(--a8c-charts-color-surface, #fff)",
838
838
  surfaceSecondary: "var(--a8c-charts-color-surface-secondary, #f4f4f4)",
839
+ track: "var(--a8c-charts-color-track, #f0f0f0)",
839
840
  trendUp: "var(--a8c-charts-color-trend-up, #008030)",
840
841
  trendDown: "var(--a8c-charts-color-trend-down, #cc1818)",
841
842
  series: SERIES_PALETTE_POINTERS
@@ -1065,6 +1066,15 @@ const rgbLuminance = (rgb) => {
1065
1066
  * @return Ratio from 1 to 21.
1066
1067
  */
1067
1068
  const luminanceContrastRatio = (first, second) => (Math.max(first, second) + .05) / (Math.min(first, second) + .05);
1069
+ /**
1070
+ * WCAG contrast ratio between two hex colors.
1071
+ *
1072
+ * @param first - First hex color.
1073
+ * @param second - Second hex color.
1074
+ * @return Ratio from 1 to 21.
1075
+ * @throws {Error} if either hex string is malformed
1076
+ */
1077
+ const contrastRatio = (first, second) => luminanceContrastRatio(relativeLuminance(first), relativeLuminance(second));
1068
1078
  /** WCAG 1.4.3 text contrast, which a label drawn on a fill needs from at least one label color. */
1069
1079
  const MIN_LABEL_CONTRAST = 4.5;
1070
1080
  /** Degrees of hue one unit of OKLCH chroma is worth when ranking candidates by hue. */
@@ -1255,6 +1265,14 @@ const buildPaletteGenerator = (normalizedSeeds, background, labelColors) => {
1255
1265
  };
1256
1266
  };
1257
1267
  //#endregion
1268
+ //#region src/providers/chart-context/private/resolve-opaque-hex.ts
1269
+ const resolveOpaqueHex = (pointer, resolve) => {
1270
+ const raw = resolve(pointer);
1271
+ if (!raw || (0, _visx_vendor_d3_color.color)(raw)?.opacity !== 1) return null;
1272
+ const hex = normalizeColorToHex(raw);
1273
+ return isValidHexColor(hex) ? hex : null;
1274
+ };
1275
+ //#endregion
1258
1276
  //#region src/providers/chart-context/themes.ts
1259
1277
  /**
1260
1278
  * Default theme configuration: the shape and spacing a consumer can override.
@@ -1303,12 +1321,6 @@ const defaultTheme = {
1303
1321
  };
1304
1322
  //#endregion
1305
1323
  //#region src/providers/chart-context/global-charts-provider.tsx
1306
- const resolveOpaqueHex = (pointer, element) => {
1307
- const raw = resolveCssVariable(pointer, element);
1308
- if (!raw || (0, _visx_vendor_d3_color.color)(raw)?.opacity !== 1) return null;
1309
- const hex = normalizeColorToHex(pointer, element, resolveCssVariable);
1310
- return isValidHexColor(hex) ? hex : null;
1311
- };
1312
1324
  const PLACEHOLDER_LABEL_COLORS = [LABEL_FALLBACK, LABEL_INVERSE_FALLBACK];
1313
1325
  const GlobalChartsContext = (0, react.createContext)(null);
1314
1326
  const GlobalChartsProvider = ({ children, theme, locale, timeZone }) => {
@@ -1336,8 +1348,9 @@ const GlobalChartsProvider = ({ children, theme, locale, timeZone }) => {
1336
1348
  const normalizedColor = normalizeColorToHex(color, wrapperRef.current, resolveCssVariable);
1337
1349
  if (isValidHexColor(normalizedColor)) resolvedColors.push(normalizedColor);
1338
1350
  }
1339
- const backgroundHex = resolveOpaqueHex(CATALOG_POINTERS.background, wrapperRef.current) ?? "#ffffff";
1340
- const labelColors = [CATALOG_POINTERS.label, CATALOG_POINTERS.labelInverse].map((pointer) => resolveOpaqueHex(pointer, wrapperRef.current)).filter((hex) => hex !== null);
1351
+ const resolveAtWrapper = createCssVariableResolver(wrapperRef.current);
1352
+ const backgroundHex = resolveOpaqueHex(CATALOG_POINTERS.background, resolveAtWrapper) ?? "#ffffff";
1353
+ const labelColors = [CATALOG_POINTERS.label, CATALOG_POINTERS.labelInverse].map((pointer) => resolveOpaqueHex(pointer, resolveAtWrapper)).filter((hex) => hex !== null);
1341
1354
  setColorCache({
1342
1355
  colors: resolvedColors,
1343
1356
  background: backgroundHex,
@@ -1673,10 +1686,10 @@ const DEFAULT_MARGIN_BOTTOM = 20;
1673
1686
  */
1674
1687
  const DEFAULT_MARGIN_LEFT = 20;
1675
1688
  /**
1676
- * Bottom margin to use when the X-axis is rendered at the top.
1689
+ * Bottom margin when no X-axis sits below the chart, because it is on top or hidden.
1677
1690
  * We only need a small buffer below the chart in that case.
1678
1691
  */
1679
- const DEFAULT_BOTTOM_FOR_TOP_AXIS = 10;
1692
+ const DEFAULT_BOTTOM_WITHOUT_X_AXIS = 10;
1680
1693
  /**
1681
1694
  * Fallback font size used when we cannot derive a font size
1682
1695
  * from the theme or axis styles for X-axis tick labels.
@@ -1756,18 +1769,18 @@ const useChartMargin = (height, options, data, theme, horizontal = false) => {
1756
1769
  if (yAxisOrientation === "right") defaultMargin.right = yMarginValue;
1757
1770
  else defaultMargin.left = yMarginValue;
1758
1771
  }
1759
- const xOrientation = options.axis?.x?.orientation === "top" ? "top" : "bottom";
1760
- const { fontSize, tickLength, tickLabelStyle } = getXAxisLabelMetrics(theme, xOrientation);
1761
- const computedXMargin = fontSize + tickLength;
1762
- if (xOrientation === "top") {
1763
- defaultMargin.top = Math.max(defaultMargin.top, computedXMargin);
1764
- defaultMargin.bottom = DEFAULT_BOTTOM_FOR_TOP_AXIS;
1765
- } else defaultMargin.bottom = Math.max(defaultMargin.bottom, computedXMargin);
1766
1772
  if (options.axis?.x?.display !== false) {
1773
+ const xOrientation = options.axis?.x?.orientation === "top" ? "top" : "bottom";
1774
+ const { fontSize, tickLength, tickLabelStyle } = getXAxisLabelMetrics(theme, xOrientation);
1775
+ const computedXMargin = fontSize + tickLength;
1776
+ if (xOrientation === "top") {
1777
+ defaultMargin.top = Math.max(defaultMargin.top, computedXMargin);
1778
+ defaultMargin.bottom = DEFAULT_BOTTOM_WITHOUT_X_AXIS;
1779
+ } else defaultMargin.bottom = Math.max(defaultMargin.bottom, computedXMargin);
1767
1780
  const { first, last } = getEdgeTickWidths(options.axis?.x?.tickValues ?? [], options.axis?.x?.tickFormat, tickLabelStyle);
1768
1781
  defaultMargin.left = Math.max(defaultMargin.left, Math.ceil(first / 2));
1769
1782
  defaultMargin.right = Math.max(defaultMargin.right, Math.ceil(last / 2));
1770
- }
1783
+ } else defaultMargin.bottom = DEFAULT_BOTTOM_WITHOUT_X_AXIS;
1771
1784
  return defaultMargin;
1772
1785
  }, [
1773
1786
  options,
@@ -2479,11 +2492,12 @@ function useChartLegendItems(data, options = {}, legendShape) {
2479
2492
  ]);
2480
2493
  }
2481
2494
  //#endregion
2482
- //#region src/components/tooltip/base-tooltip.module.scss
2483
- var base_tooltip_module_default = {
2484
- "surface": "a8ccharts--zY0xG-surface",
2485
- "tooltip": "a8ccharts--zY0xG-tooltip"
2486
- };
2495
+ //#region src/components/tooltip/private/label-value-content.tsx
2496
+ const LabelValueContent = ({ data }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2497
+ data.label,
2498
+ ": ",
2499
+ data.valueDisplay || (0, _automattic_number_formatters.formatNumber)(data.value)
2500
+ ] });
2487
2501
  //#endregion
2488
2502
  //#region src/components/tooltip/private/tooltip-theme.tsx
2489
2503
  const DEFAULT_SURFACE = "#1e1e1e";
@@ -2509,28 +2523,63 @@ const TooltipTheme = ({ children }) => {
2509
2523
  });
2510
2524
  };
2511
2525
  //#endregion
2526
+ //#region src/components/tooltip/tooltip-box.module.scss
2527
+ var tooltip_box_module_default = { "surface": "a8ccharts-ddpU1W-surface" };
2528
+ //#endregion
2529
+ //#region src/components/tooltip/tooltip-box.tsx
2530
+ /**
2531
+ * The chart tooltip box: the dark surface every chart tooltip draws. It does not position itself; place it with `style` or a wrapper.
2532
+ *
2533
+ * @param props - Div attributes, plus `unstyled`.
2534
+ * @param props.unstyled - Drop the surface, scope class and theme.
2535
+ * @param ref - Forwarded to the box element.
2536
+ * @return The tooltip box.
2537
+ */
2538
+ const TooltipBox = (0, react.forwardRef)(({ unstyled = false, role = "tooltip", className, children, ...rest }, ref) => {
2539
+ const box = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2540
+ ref,
2541
+ role,
2542
+ className: (0, clsx.default)("visx-tooltip", !unstyled && ["a8c-charts-tooltip-scope", tooltip_box_module_default.surface], className),
2543
+ ...rest,
2544
+ children
2545
+ });
2546
+ return unstyled ? box : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTheme, { children: box });
2547
+ });
2548
+ TooltipBox.displayName = "TooltipBox";
2549
+ //#endregion
2512
2550
  //#region src/components/tooltip/base-tooltip.tsx
2513
- const DefaultTooltipContent = ({ data }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2514
- data?.label,
2515
- ": ",
2516
- data?.valueDisplay || (0, _automattic_number_formatters.formatNumber)(data?.value)
2517
- ] });
2518
- const BaseTooltip = ({ data, top, left, component: Component = DefaultTooltipContent, children, className, style, renderContainer = true }) => {
2551
+ /**
2552
+ * A tooltip box with children or `label: value` content. Prefer `TooltipBox`.
2553
+ *
2554
+ * @param props - Tooltip props.
2555
+ * @param props.data - Data for the default content.
2556
+ * @param props.top - Deprecated vertical position.
2557
+ * @param props.left - Deprecated horizontal position.
2558
+ * @param props.component - Deprecated content component.
2559
+ * @param props.children - Tooltip content.
2560
+ * @param props.className - Class name passed to the content component.
2561
+ * @param props.style - Styles for the box.
2562
+ * @param props.renderContainer - Deprecated; render the content alone when false.
2563
+ * @return The tooltip.
2564
+ */
2565
+ const BaseTooltip = ({ data, top, left, component: Component = LabelValueContent, children, className, style, renderContainer = true }) => {
2519
2566
  const content = children || data && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
2520
2567
  data,
2521
2568
  className
2522
2569
  });
2523
2570
  if (!renderContainer) return content;
2524
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTheme, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2525
- className: (0, clsx.default)(CHART_SCOPE_CLASS, base_tooltip_module_default.tooltip),
2571
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipBox, {
2526
2572
  style: {
2527
- top,
2528
- left,
2573
+ ...top === void 0 && left === void 0 ? void 0 : {
2574
+ position: "absolute",
2575
+ top,
2576
+ left,
2577
+ transform: "translate(-50%, -100%)"
2578
+ },
2529
2579
  ...style
2530
2580
  },
2531
- role: "tooltip",
2532
2581
  children: content
2533
- }) });
2582
+ });
2534
2583
  };
2535
2584
  //#endregion
2536
2585
  //#region src/components/tooltip/private/bounded-tooltip.tsx
@@ -2610,7 +2659,7 @@ const getBoundedPosition = ({ left, top, offsetLeft, offsetTop, box, wrapper, bo
2610
2659
  };
2611
2660
  };
2612
2661
  /**
2613
- * visx's `Tooltip`, positioned like its `TooltipWithBounds` but kept inside the
2662
+ * A `TooltipBox`, positioned like visx's `TooltipWithBounds` but kept inside the
2614
2663
  * nearest clipping ancestor — or the viewport when there is none — rather than
2615
2664
  * inside its own parent. Rendered in-tree, a tooltip's parent is the chart
2616
2665
  * wrapper, which is often narrower than the box; measuring against the parent
@@ -2619,7 +2668,7 @@ const getBoundedPosition = ({ left, top, offsetLeft, offsetTop, box, wrapper, bo
2619
2668
  * Re-measures on every render, so a box whose content changes width between
2620
2669
  * two hovers is placed for its current size.
2621
2670
  *
2622
- * @param props - visx `Tooltip` props.
2671
+ * @param props - `TooltipBox` props plus placement.
2623
2672
  * @param props.left - Anchor x, in wrapper coordinates.
2624
2673
  * @param props.top - Anchor y, in wrapper coordinates.
2625
2674
  * @param props.offsetLeft - Gap between the anchor and the box, horizontally.
@@ -2685,9 +2734,11 @@ const BoundedTooltip = ({ left = 0, top = 0, offsetLeft = DEFAULT_OFFSET, offset
2685
2734
  ]);
2686
2735
  const x = position?.x ?? left + offsetLeft;
2687
2736
  const y = position?.y ?? top + (placement === "below-axis" ? POINTER_HEIGHT : offsetTop);
2688
- const box = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_visx_tooltip.Tooltip, {
2737
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(TooltipBox, {
2689
2738
  ref: nodeRef,
2690
- className: (0, clsx.default)(!unstyled && ["a8c-charts-tooltip-scope", base_tooltip_module_default.surface], className),
2739
+ role: "presentation",
2740
+ unstyled,
2741
+ className,
2691
2742
  style: {
2692
2743
  position: "absolute",
2693
2744
  left: 0,
@@ -2710,10 +2761,9 @@ const BoundedTooltip = ({ left = 0, top = 0, offsetLeft = DEFAULT_OFFSET, offset
2710
2761
  }
2711
2762
  }), children]
2712
2763
  });
2713
- return unstyled ? box : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTheme, { children: box });
2714
2764
  };
2715
2765
  //#endregion
2716
- //#region src/components/tooltip/xy-chart-tooltip.tsx
2766
+ //#region src/components/tooltip/private/xy-chart-tooltip-overlay.tsx
2717
2767
  const CROSSHAIR_STROKE_WIDTH = 1.5;
2718
2768
  const CROSSHAIR_PAINT_PROPERTIES = /* @__PURE__ */ new Set([
2719
2769
  "stroke",
@@ -2815,21 +2865,35 @@ const XyChartTooltipContent = ({ tooltipContext, container, renderTooltip, rende
2815
2865
  const crosshairStroke = theme?.gridStyles?.stroke ?? theme?.htmlLabel?.color ?? FALLBACK_COLOR;
2816
2866
  const marginTop = margin?.top ?? 0;
2817
2867
  const marginLeft = margin?.left ?? 0;
2818
- const TooltipComponent = detectBounds || tooltipPlacement !== "auto" ? BoundedTooltip : _visx_tooltip.Tooltip;
2819
2868
  const boxStyle = {
2820
2869
  zIndex,
2821
2870
  ...style
2822
2871
  };
2823
- const themesOwnBox = TooltipComponent === _visx_tooltip.Tooltip && !tooltipProps.unstyled;
2824
- const box = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipComponent, {
2872
+ const { offsetLeft = 10, offsetTop = 10, unstyled, className, ...boxProps } = tooltipProps;
2873
+ const anchorTop = tooltipPlacement === "below-axis" ? marginTop + innerHeight + (margin?.bottom ?? 0) : tooltipAnchorTop ?? tooltipTop;
2874
+ const box = detectBounds || tooltipPlacement !== "auto" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BoundedTooltip, {
2825
2875
  left: tooltipLeft,
2826
- top: tooltipPlacement === "below-axis" ? marginTop + innerHeight + (margin?.bottom ?? 0) : tooltipAnchorTop ?? tooltipTop,
2876
+ top: anchorTop,
2877
+ offsetLeft,
2878
+ offsetTop,
2879
+ unstyled,
2880
+ className,
2827
2881
  style: boxStyle,
2828
- applyPositionStyle: true,
2829
- ...tooltipProps,
2830
- className: (0, clsx.default)(themesOwnBox && ["a8c-charts-scope", base_tooltip_module_default.surface], tooltipProps.className),
2882
+ ...boxProps,
2831
2883
  ...tooltipPlacement !== "auto" && { placement: tooltipPlacement },
2832
2884
  children: tooltipContent
2885
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipBox, {
2886
+ role: "presentation",
2887
+ unstyled,
2888
+ className,
2889
+ style: {
2890
+ position: "absolute",
2891
+ left: (tooltipLeft ?? 0) + offsetLeft,
2892
+ top: (anchorTop ?? 0) + offsetTop,
2893
+ ...!unstyled && boxStyle
2894
+ },
2895
+ ...boxProps,
2896
+ children: tooltipContent
2833
2897
  });
2834
2898
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
2835
2899
  className: "visx-tooltip-overlay",
@@ -2857,7 +2921,7 @@ const XyChartTooltipContent = ({ tooltipContext, container, renderTooltip, rende
2857
2921
  }),
2858
2922
  glyphs
2859
2923
  ]
2860
- }), container && (0, react_dom.createPortal)(themesOwnBox ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTheme, { children: box }) : box, container)] });
2924
+ }), container && (0, react_dom.createPortal)(box, container)] });
2861
2925
  };
2862
2926
  /**
2863
2927
  * In-tree replacement for `@visx/xychart`'s `Tooltip`.
@@ -2882,7 +2946,7 @@ const XyChartTooltipContent = ({ tooltipContext, container, renderTooltip, rende
2882
2946
  * @param props - visx's `Tooltip` options. `scroll`, `debounce` and `resizeObserverPolyfill` are accepted and ignored.
2883
2947
  * @return An anchor in the SVG, plus the overlay and the tooltip box while the tooltip is open.
2884
2948
  */
2885
- const XyChartTooltip = (props) => {
2949
+ const XyChartTooltipOverlay = (props) => {
2886
2950
  const tooltipContext = (0, react.useContext)(_visx_xychart.TooltipContext);
2887
2951
  const [container, setContainer] = (0, react.useState)(null);
2888
2952
  const anchorRef = (0, react.useCallback)((node) => {
@@ -2895,8 +2959,8 @@ const XyChartTooltip = (props) => {
2895
2959
  })] });
2896
2960
  };
2897
2961
  //#endregion
2898
- //#region src/components/tooltip/accessible-tooltip.tsx
2899
- const AccessibleTooltip = ({ renderTooltip, selectedIndex, tooltipRef, keyboardFocusedClassName, series = [], mode = "group", verticalCrosshairStyle, horizontalCrosshairStyle, ...props }) => {
2962
+ //#region src/components/tooltip/xy-chart-tooltip.tsx
2963
+ const XYChartTooltip = ({ renderTooltip, selectedIndex, tooltipRef, keyboardFocusedClassName, series = [], mode = "group", verticalCrosshairStyle, horizontalCrosshairStyle, ...props }) => {
2900
2964
  const tooltipContext = (0, react.useContext)(_visx_xychart.TooltipContext);
2901
2965
  const scopeElement = useChartScopeElement();
2902
2966
  const crosshairStroke = (0, react.useMemo)(() => {
@@ -3002,7 +3066,7 @@ const AccessibleTooltip = ({ renderTooltip, selectedIndex, tooltipRef, keyboardF
3002
3066
  keyboardFocusedClassName,
3003
3067
  standaloneScopeClass
3004
3068
  ]);
3005
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(XyChartTooltip, {
3069
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(XyChartTooltipOverlay, {
3006
3070
  ...props,
3007
3071
  verticalCrosshairStyle: {
3008
3072
  ...crosshairStroke,
@@ -3015,6 +3079,8 @@ const AccessibleTooltip = ({ renderTooltip, selectedIndex, tooltipRef, keyboardF
3015
3079
  renderTooltip: focusableRenderTooltip
3016
3080
  });
3017
3081
  };
3082
+ /** @deprecated Use `XYChartTooltip`. */
3083
+ const AccessibleTooltip = XYChartTooltip;
3018
3084
  const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating, setIsNavigating, chartRef, totalPoints, onActivate, preventTooltipScroll = false, visibleSeriesKey }) => {
3019
3085
  const getChartRoot = (0, react.useCallback)(() => chartRef.current?.closest("[role=\"grid\"]") ?? chartRef.current, [chartRef]);
3020
3086
  const focusWithoutScrollIfNeeded = (0, react.useCallback)((element) => {
@@ -5020,7 +5086,7 @@ const LineChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
5020
5086
  ] }, seriesData?.label || index);
5021
5087
  })
5022
5088
  }),
5023
- withTooltips && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessibleTooltip, {
5089
+ withTooltips && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(XYChartTooltip, {
5024
5090
  detectBounds: true,
5025
5091
  snapTooltipToDatumX: true,
5026
5092
  tooltipPlacement,
@@ -5496,7 +5562,7 @@ const AreaChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
5496
5562
  children: seriesWithVisibility.map(renderSeries)
5497
5563
  }), !allSeriesHidden && !stacked && seriesWithVisibility.map(renderSeries)]
5498
5564
  }),
5499
- withTooltips && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessibleTooltip, {
5565
+ withTooltips && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(XYChartTooltip, {
5500
5566
  detectBounds: true,
5501
5567
  snapTooltipToDatumX: true,
5502
5568
  snapTooltipToDatumY: !stacked,
@@ -6107,6 +6173,7 @@ const ComparisonBars = ({ comparisonEntries, primaryKeys, groupPadding, horizont
6107
6173
  widthFactor
6108
6174
  });
6109
6175
  rects.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
6176
+ className: "bar-chart__comparison-bar",
6110
6177
  x: rect.x,
6111
6178
  y: rect.y,
6112
6179
  width: rect.width,
@@ -6522,7 +6589,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
6522
6589
  const patternId = getPatternId(chartId, index);
6523
6590
  return `
6524
6591
  .visx-bar[fill="url(#${patternId})"],
6525
- .bar-chart__comparison-bars rect[fill="url(#${patternId})"] {
6592
+ .bar-chart__comparison-bar[fill="url(#${patternId})"] {
6526
6593
  stroke: ${color};
6527
6594
  stroke-width: 1;
6528
6595
  }
@@ -6704,15 +6771,15 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
6704
6771
  }),
6705
6772
  !allSeriesHidden && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WholeNumberTicks, {
6706
6773
  ...wholeNumberTicksProps,
6707
- children: (valueTicks) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, {
6774
+ children: (valueTicks) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [chartOptions.axis.x.display !== false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, {
6708
6775
  ...chartOptions.axis.x,
6709
6776
  ...horizontal && valueTicks ? { tickValues: valueTicks } : {}
6710
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, {
6777
+ }), chartOptions.axis.y.display !== false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, {
6711
6778
  ...chartOptions.axis.y,
6712
6779
  ...!horizontal && valueTicks ? { tickValues: valueTicks } : {}
6713
6780
  })] })
6714
6781
  }),
6715
- withTooltips && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessibleTooltip, {
6782
+ withTooltips && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(XYChartTooltip, {
6716
6783
  tooltipPlacement,
6717
6784
  tooltipAnchorTop,
6718
6785
  style: tooltipStyle,
@@ -7239,7 +7306,10 @@ const ConversionFunnelChartInternal = ({ mainRate, changeIndicator, steps, loadi
7239
7306
  left: tooltipLeft,
7240
7307
  className: standaloneScopeClass,
7241
7308
  style: tooltipStyle,
7242
- children: tooltipContent
7309
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7310
+ role: "tooltip",
7311
+ children: tooltipContent
7312
+ })
7243
7313
  });
7244
7314
  })()]
7245
7315
  });
@@ -7626,30 +7696,44 @@ var heatmap_chart_module_default = {
7626
7696
  //#region src/charts/heatmap-chart/private/use-heatmap-colors.ts
7627
7697
  const isPresent = (value) => value !== null && value !== void 0 && !isNaN(value);
7628
7698
  /**
7629
- * Get the min and max values from heatmap data, ignoring null/NaN. Summary
7630
- * columns stay out: a roll-up on the scale would flatten every real cell.
7699
+ * Get the min and max values from heatmap data, ignoring null/NaN and the zeros
7700
+ * `isEmptyValue` paints as empty. Summary columns stay out: a roll-up on the
7701
+ * scale would flatten every real cell.
7631
7702
  * @param data - The heatmap columns
7632
7703
  * @return Tuple of [min, max] values
7633
7704
  */
7634
7705
  const getValueExtent = (data) => {
7635
7706
  let min = Infinity;
7636
7707
  let max = -Infinity;
7708
+ let hasZero = false;
7637
7709
  for (const column of data) {
7638
7710
  if (column.summary) continue;
7639
7711
  for (const cell of column.data) {
7640
7712
  if (!isPresent(cell.value)) continue;
7713
+ if (cell.value === 0) {
7714
+ hasZero = true;
7715
+ continue;
7716
+ }
7641
7717
  if (cell.value < min) min = cell.value;
7642
7718
  if (cell.value > max) max = cell.value;
7643
7719
  }
7644
7720
  }
7721
+ if (hasZero && min < 0) max = Math.max(max, 0);
7645
7722
  if (min === Infinity) return [0, 0];
7646
7723
  return [min, max];
7647
7724
  };
7648
7725
  /**
7726
+ * Whether a value paints as an empty cell: a zero in data with no negatives
7727
+ * counts nothing, so it must not look like the lowest step of activity.
7728
+ *
7729
+ * @param value - The cell value
7730
+ * @param extent - The extent from `getValueExtent`
7731
+ * @return True when the cell takes the empty-cell color
7732
+ */
7733
+ const isEmptyValue = (value, extent) => value === 0 && extent[0] >= 0;
7734
+ /**
7649
7735
  * Normalize a value to 0–1 within the extent. A flat extent (min === max)
7650
- * maps to 1 — every cell is equally the "highest" — except an all-zero
7651
- * extent, which maps to 0 so a no-activity grid renders at the scale's
7652
- * bottom instead of full intensity.
7736
+ * maps to 1, since every cell is equally the "highest".
7653
7737
  *
7654
7738
  * @param value - The value to normalize
7655
7739
  * @param extent - Tuple of [min, max] values for the normalization range
@@ -7657,9 +7741,40 @@ const getValueExtent = (data) => {
7657
7741
  */
7658
7742
  const getNormalizedValue = (value, extent) => {
7659
7743
  const [min, max] = extent;
7660
- if (min === max) return max === 0 ? 0 : 1;
7744
+ if (min === max) return 1;
7661
7745
  return Math.min(1, Math.max(0, (value - min) / (max - min)));
7662
7746
  };
7747
+ const STEPS = 200;
7748
+ const firstMeeting = (from, to, meets) => {
7749
+ for (let step = 0; step <= STEPS; step++) {
7750
+ const color = mixHexColors(from, to, step / STEPS);
7751
+ if (meets(color)) return color;
7752
+ }
7753
+ return null;
7754
+ };
7755
+ /**
7756
+ * The two ends of the heatmap fill scale: 3:1 against the background and the empty cell at the
7757
+ * low end, toward 9:1 against the background at the high end.
7758
+ *
7759
+ * @param primary - The resolved primary color, as six-digit hex.
7760
+ * @param background - The resolved chart background, as six-digit hex.
7761
+ * @param emptyCell - The resolved empty-cell color, as six-digit hex, when it is opaque.
7762
+ * @return The scale ends, as six-digit hex.
7763
+ */
7764
+ const getHeatmapScale = (primary, background, emptyCell) => {
7765
+ const primaryStandsOut = contrastRatio(primary, background) >= 3;
7766
+ const extreme = (primaryStandsOut ? relativeLuminance(primary) > relativeLuminance(background) : contrastRatio("#ffffff", background) > contrastRatio("#000000", background)) ? "#ffffff" : "#000000";
7767
+ const lowAgainst = (references) => {
7768
+ const meets = (color) => references.every((reference) => contrastRatio(color, reference) >= 3);
7769
+ return (primaryStandsOut ? firstMeeting(background, primary, meets) : null) ?? firstMeeting(primary, extreme, meets);
7770
+ };
7771
+ const high = firstMeeting(primary, extreme, (color) => contrastRatio(color, background) >= 9) ?? extreme;
7772
+ const againstEmptyCell = emptyCell ? lowAgainst([background, emptyCell]) : null;
7773
+ return {
7774
+ low: (againstEmptyCell && contrastRatio(againstEmptyCell, background) <= contrastRatio(high, background) ? againstEmptyCell : lowAgainst([background])) ?? extreme,
7775
+ high
7776
+ };
7777
+ };
7663
7778
  //#endregion
7664
7779
  //#region src/charts/heatmap-chart/private/heatmap-context.ts
7665
7780
  /** Shared by the chart and legend without importing back from `heatmap-chart.tsx`. */
@@ -7670,7 +7785,7 @@ const HeatmapLegend = ({ steps = 5, variant = "swatches", lessLabel, moreLabel }
7670
7785
  const context = (0, react.useContext)(HeatmapContext);
7671
7786
  const { legend } = useGlobalChartsTheme();
7672
7787
  if (!context) return null;
7673
- const { primaryColorHex } = context;
7788
+ const { fillVars } = context;
7674
7789
  const labelStyle = legend.labelStyles;
7675
7790
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_wordpress_ui.Stack, {
7676
7791
  direction: "row",
@@ -7693,7 +7808,7 @@ const HeatmapLegend = ({ steps = 5, variant = "swatches", lessLabel, moreLabel }
7693
7808
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7694
7809
  className: heatmap_chart_module_default["heatmap-chart__legend-swatch"],
7695
7810
  style: {
7696
- "--a8c-charts-color-heatmap-primary": primaryColorHex,
7811
+ ...fillVars,
7697
7812
  "--a8c-charts-heatmap-cell-intensity": intensity
7698
7813
  }
7699
7814
  }, index);
@@ -7897,7 +8012,6 @@ const stepCalendarCell = (grid, blocks, from, key) => {
7897
8012
  };
7898
8013
  //#endregion
7899
8014
  //#region src/charts/heatmap-chart/heatmap-chart.tsx
7900
- const CELL_MIX_FLOOR = .15;
7901
8015
  const CELL_VALUE_MODIFIER = {
7902
8016
  label: void 0,
7903
8017
  "label-inverse": heatmap_chart_module_default["heatmap-chart__cell-value--inverse"],
@@ -7931,13 +8045,23 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
7931
8045
  index: 0,
7932
8046
  overrideColor: primaryColor
7933
8047
  });
7934
- const chartBackgroundHex = normalizeColorToHex(CATALOG_POINTERS.background, scopeElement, resolveCssVariable);
8048
+ const resolveAtScope = createCssVariableResolver(scopeElement);
8049
+ const chartBackgroundHex = resolveOpaqueHex(CATALOG_POINTERS.background, resolveAtScope) ?? "#ffffff";
8050
+ const emptyCellHex = resolveOpaqueHex(CATALOG_POINTERS.track, resolveAtScope);
7935
8051
  const primaryHex = normalizeColorToHex(primaryColorHex);
7936
- const cellMix = isValidHexColor(primaryHex) && isValidHexColor(chartBackgroundHex) ? {
7937
- primary: hexToRgb(primaryHex),
7938
- background: hexToRgb(chartBackgroundHex)
7939
- } : null;
7940
- const cellTextColor = (intensity) => cellMix ? pickLabelTextColorForFill(blendRgb(cellMix.primary, cellMix.background, CELL_MIX_FLOOR + .85 * intensity), labelRoles, "label") : "label";
8052
+ const scale = (0, react.useMemo)(() => isValidHexColor(primaryHex) ? getHeatmapScale(primaryHex, chartBackgroundHex, emptyCellHex) : null, [
8053
+ primaryHex,
8054
+ chartBackgroundHex,
8055
+ emptyCellHex
8056
+ ]);
8057
+ const fillVars = (0, react.useMemo)(() => ({
8058
+ "--a8c-charts-color-heatmap-primary": primaryColorHex,
8059
+ ...scale && {
8060
+ "--a8c-charts-color-heatmap-low": scale.low,
8061
+ "--a8c-charts-color-heatmap-high": scale.high
8062
+ }
8063
+ }), [primaryColorHex, scale]);
8064
+ const cellTextColor = (intensity) => scale ? pickLabelTextColorForFill(blendRgb(hexToRgb(scale.high), hexToRgb(scale.low), intensity), labelRoles, "label") : "label";
7941
8065
  useIsomorphicLayoutEffect(() => {
7942
8066
  if (!containerRef.current) return;
7943
8067
  const next = resolveLabelRoles(createCssVariableResolver(containerRef.current));
@@ -7951,8 +8075,8 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
7951
8075
  const extent = (0, react.useMemo)(() => getValueExtent(data), [data]);
7952
8076
  const heatmapContext = (0, react.useMemo)(() => ({
7953
8077
  extent,
7954
- primaryColorHex
7955
- }), [extent, primaryColorHex]);
8078
+ fillVars
8079
+ }), [extent, fillVars]);
7956
8080
  const columns = data.length;
7957
8081
  const rows = Math.max(0, ...data.map((column) => column.data.length));
7958
8082
  const groupLayout = (0, react.useMemo)(() => resolveColumnGroups(columnGroups, columns, 2), [columnGroups, columns]);
@@ -8103,9 +8227,10 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
8103
8227
  const firstDataRow = hasColumnLabels ? 2 : 1;
8104
8228
  const dataTrack = (column) => column.summary ? "minmax(auto, max-content)" : columnTrack;
8105
8229
  const gapTrack = compact ? `minmax(${groupGap}px, 1fr)` : `${groupGap}px`;
8230
+ const columnTracks = data.map((column, columnIndex) => groupLayout.columns[columnIndex].gapBefore ? `${gapTrack} ${dataTrack(column)}` : dataTrack(column)).join(" ");
8106
8231
  const gridStyle = {
8107
- "--a8c-charts-color-heatmap-primary": primaryColorHex,
8108
- gridTemplateColumns: `auto ${data.map((column, columnIndex) => groupLayout.columns[columnIndex].gapBefore ? `${gapTrack} ${dataTrack(column)}` : dataTrack(column)).join(" ")}`,
8232
+ ...fillVars,
8233
+ gridTemplateColumns: `auto ${columnTracks}`,
8109
8234
  gridTemplateRows: `${hasColumnLabels ? "auto " : ""}repeat(${rows}, ${rowTrack})${hasGroups ? " auto" : ""}`
8110
8235
  };
8111
8236
  if (compact) {
@@ -8205,7 +8330,7 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
8205
8330
  }, `cell-${columnIndex}-${rowIndex}`);
8206
8331
  const value = cell?.value ?? null;
8207
8332
  const present = isPresent(value);
8208
- const filled = present && !column.summary;
8333
+ const filled = present && !column.summary && !isEmptyValue(value, extent);
8209
8334
  const normalized = filled ? getNormalizedValue(value, extent) : 0;
8210
8335
  const info = buildTooltipData(columnIndex, rowIndex);
8211
8336
  const accessibleLabel = `${cellName(info)}: ${info.value === null ? (0, _wordpress_i18n.__)("No data", "jetpack-charts") : (0, _automattic_number_formatters.formatNumber)(info.value)}`;
@@ -11855,18 +11980,13 @@ var pie_chart_module_default = {
11855
11980
  //#region src/charts/pie-chart/pie-chart.tsx
11856
11981
  /**
11857
11982
  * Default tooltip renderer for pie charts.
11858
- * Renders a BaseTooltip with the hovered segment's data.
11983
+ * Renders the default `label: value` tooltip content for the hovered segment.
11859
11984
  *
11860
11985
  * @param {PieChartRenderTooltipParams} params - The tooltip parameters containing the hovered data point
11861
11986
  * @return {ReactNode} The rendered tooltip content
11862
11987
  */
11863
11988
  const renderDefaultPieTooltip = ({ tooltipData }) => {
11864
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BaseTooltip, {
11865
- data: tooltipData,
11866
- top: 0,
11867
- left: 0,
11868
- renderContainer: false
11869
- });
11989
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LabelValueContent, { data: tooltipData });
11870
11990
  };
11871
11991
  /**
11872
11992
  * Validates the pie chart data
@@ -12152,18 +12272,13 @@ var pie_semi_circle_chart_module_default = {
12152
12272
  //#region src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx
12153
12273
  /**
12154
12274
  * Default tooltip renderer for semi-circle pie charts.
12155
- * Renders a BaseTooltip with the hovered segment's data.
12275
+ * Renders the default `label: value` tooltip content for the hovered segment.
12156
12276
  *
12157
12277
  * @param {PieSemiCircleChartRenderTooltipParams} params - The tooltip parameters containing the hovered data point
12158
12278
  * @return {ReactNode} The rendered tooltip content
12159
12279
  */
12160
12280
  const renderDefaultPieSemiCircleTooltip = ({ tooltipData }) => {
12161
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BaseTooltip, {
12162
- data: tooltipData,
12163
- top: 0,
12164
- left: 0,
12165
- renderContainer: false
12166
- });
12281
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LabelValueContent, { data: tooltipData });
12167
12282
  };
12168
12283
  const PAD_ANGLE = .03;
12169
12284
  const DEFAULT_WIDTH$1 = 400;
@@ -12631,7 +12746,9 @@ exports.PieSemiCircleChartUnresponsive = PieSemiCircleChart;
12631
12746
  exports.Sparkline = Sparkline;
12632
12747
  exports.SparklineUnresponsive = SparklineUnresponsive;
12633
12748
  exports.ThemeProvider = GlobalChartsProvider;
12749
+ exports.TooltipBox = TooltipBox;
12634
12750
  exports.TrendIndicator = TrendIndicator;
12751
+ exports.XYChartTooltip = XYChartTooltip;
12635
12752
  exports.buildCalendarHeatmapData = buildCalendarHeatmapData;
12636
12753
  exports.buildMonthCalendarHeatmapData = buildMonthCalendarHeatmapData;
12637
12754
  exports.defaultTheme = defaultTheme;