@automattic/charts 4.1.1 → 4.3.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 (97) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/TOKENS.md +1 -1
  3. package/dist/index.cjs +1335 -348
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +30 -1
  6. package/dist/index.d.cts +129 -8
  7. package/dist/index.d.ts +129 -8
  8. package/dist/index.js +1336 -351
  9. package/dist/index.js.map +1 -1
  10. package/package.json +8 -8
  11. package/src/charts/area-chart/area-chart.tsx +42 -10
  12. package/src/charts/area-chart/private/overlays.tsx +1 -2
  13. package/src/charts/area-chart/private/validate-data.ts +2 -3
  14. package/src/charts/area-chart/test/area-chart.test.tsx +72 -1
  15. package/src/charts/bar-chart/bar-chart.tsx +137 -70
  16. package/src/charts/bar-chart/index.ts +1 -1
  17. package/src/charts/bar-chart/private/band-highlight.tsx +87 -0
  18. package/src/charts/bar-chart/private/band-scale.ts +13 -0
  19. package/src/charts/bar-chart/private/band-tooltip.tsx +138 -0
  20. package/src/charts/bar-chart/private/comparison-bars.tsx +5 -10
  21. package/src/charts/bar-chart/private/index.ts +3 -0
  22. package/src/charts/bar-chart/private/test/band-highlight.test.tsx +135 -0
  23. package/src/charts/bar-chart/private/test/band-tooltip.test.ts +26 -0
  24. package/src/charts/bar-chart/private/test/value-domain.test.ts +100 -0
  25. package/src/charts/bar-chart/private/truncated-tick-component.tsx +1 -1
  26. package/src/charts/bar-chart/private/use-bar-chart-options.ts +13 -40
  27. package/src/charts/bar-chart/private/value-domain.ts +90 -0
  28. package/src/charts/bar-chart/test/bar-chart-pointer-events.test.tsx +11 -23
  29. package/src/charts/bar-chart/test/bar-chart.test.tsx +1058 -19
  30. package/src/charts/bar-chart/types.ts +39 -0
  31. package/src/charts/bar-list-chart/bar-list-chart.tsx +5 -3
  32. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +1 -1
  33. package/src/charts/conversion-funnel-chart/types.ts +4 -2
  34. package/src/charts/geo-chart/types.ts +4 -2
  35. package/src/charts/heatmap-chart/build-calendar-data.ts +5 -11
  36. package/src/charts/heatmap-chart/build-month-calendar-data.ts +124 -0
  37. package/src/charts/heatmap-chart/heatmap-chart.module.scss +32 -0
  38. package/src/charts/heatmap-chart/heatmap-chart.tsx +191 -114
  39. package/src/charts/heatmap-chart/index.ts +6 -0
  40. package/src/charts/heatmap-chart/private/civil-day.ts +26 -0
  41. package/src/charts/heatmap-chart/private/column-groups.ts +71 -0
  42. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +19 -3
  43. package/src/charts/heatmap-chart/private/index.ts +2 -0
  44. package/src/charts/heatmap-chart/private/keyboard-navigation.ts +201 -0
  45. package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +36 -0
  46. package/src/charts/heatmap-chart/test/build-month-calendar-data.test.ts +194 -0
  47. package/src/charts/heatmap-chart/test/column-groups.test.ts +83 -0
  48. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +428 -2
  49. package/src/charts/heatmap-chart/test/keyboard-navigation.test.ts +25 -0
  50. package/src/charts/heatmap-chart/test/use-month-calendar-heatmap-data.test.tsx +90 -0
  51. package/src/charts/heatmap-chart/test/zoned-month-calendar-data.test.ts +23 -0
  52. package/src/charts/heatmap-chart/types.ts +59 -3
  53. package/src/charts/heatmap-chart/use-month-calendar-heatmap-data.ts +34 -0
  54. package/src/charts/leaderboard-chart/types.ts +13 -14
  55. package/src/charts/line-chart/line-chart.tsx +149 -36
  56. package/src/charts/line-chart/private/index.ts +1 -0
  57. package/src/charts/line-chart/private/line-chart-annotation.tsx +1 -1
  58. package/src/charts/line-chart/private/line-chart-glyph.tsx +17 -3
  59. package/src/charts/line-chart/private/nearest-pointer-events.tsx +155 -0
  60. package/src/charts/line-chart/test/line-chart.test.tsx +604 -9
  61. package/src/charts/line-chart/types.ts +4 -3
  62. package/src/charts/private/readings.ts +34 -0
  63. package/src/charts/private/test/readings.test.ts +29 -0
  64. package/src/charts/private/test/whole-number-ticks.test.tsx +57 -0
  65. package/src/charts/private/time-axis-options.ts +1 -1
  66. package/src/charts/private/whole-number-ticks.tsx +79 -0
  67. package/src/charts/private/with-responsive/with-responsive.tsx +2 -2
  68. package/src/charts/private/x-zoom/test/x-zoom.test.tsx +1 -3
  69. package/src/charts/private/x-zoom/x-zoom.tsx +1 -2
  70. package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +124 -0
  71. package/src/components/legend/hooks/use-chart-legend-items.ts +60 -2
  72. package/src/components/legend/private/base-legend.tsx +37 -22
  73. package/src/components/legend/test/legend.test.tsx +84 -0
  74. package/src/components/legend/types.ts +2 -0
  75. package/src/components/legend/utils/index.ts +0 -1
  76. package/src/components/tooltip/accessible-tooltip.tsx +128 -41
  77. package/src/components/tooltip/base-tooltip.module.scss +6 -1
  78. package/src/components/tooltip/private/bounded-tooltip.tsx +12 -6
  79. package/src/components/tooltip/test/accessible-tooltip.test.tsx +128 -0
  80. package/src/components/tooltip/test/bounded-tooltip.test.tsx +17 -0
  81. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +7 -0
  82. package/src/components/tooltip/xy-chart-tooltip.tsx +9 -10
  83. package/src/hooks/test/use-chart-margin.test.tsx +35 -0
  84. package/src/hooks/test/use-element-size.test.tsx +13 -13
  85. package/src/hooks/use-chart-margin.tsx +7 -2
  86. package/src/hooks/use-xychart-theme.ts +1 -1
  87. package/src/index.ts +7 -1
  88. package/src/providers/chart-context/global-charts-provider.tsx +1 -1
  89. package/src/providers/chart-context/test/chart-context.test.tsx +4 -4
  90. package/src/providers/chart-context/themes.ts +1 -0
  91. package/src/styles/test/chart-scope.test.ts +1 -1
  92. package/src/types.ts +14 -13
  93. package/src/utils/format-metric-value.ts +4 -4
  94. package/src/utils/get-edge-tick-widths.ts +1 -1
  95. package/src/utils/get-styles.ts +1 -1
  96. package/src/visx/types.ts +7 -10
  97. package/src/components/legend/utils/label-transform-factory.ts +0 -24
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getStringWidth as getStringWidth$1 } from "./visx/text/index.js";
2
2
  import { formatNumber, formatNumberCompact } from "@automattic/number-formatters";
3
- import { AnimatedAreaSeries, AnimatedAreaStack, AreaSeries, Axis, BarGroup, BarSeries, DataContext, Grid, TooltipContext, XYChart, buildChartTheme } from "@visx/xychart";
3
+ import { AnimatedAreaSeries, AnimatedAreaStack, AreaSeries, Axis, BarGroup, BarSeries, DataContext, Grid, TooltipContext, XYChart, buildChartTheme, useEventEmitter } from "@visx/xychart";
4
4
  import { __, _x, sprintf } from "@wordpress/i18n";
5
5
  import clsx from "clsx";
6
6
  import * as React from "react";
@@ -12,7 +12,7 @@ import { Text, getStringWidth } from "@visx/text";
12
12
  import deepmerge from "deepmerge";
13
13
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
14
14
  import { Tooltip, defaultStyles, useTooltip } from "@visx/tooltip";
15
- import { createScale, getTicks, scaleBand, scaleOrdinal, scaleTime } from "@visx/scale";
15
+ import { createScale, getTicks, scaleBand, scaleCanBeZeroed, scaleOrdinal, scaleTime } from "@visx/scale";
16
16
  import { Group } from "@visx/group";
17
17
  import { LegendItem, LegendLabel, LegendOrdinal, LegendShape } from "@visx/legend";
18
18
  import { IconButton, Stack, Text as Text$1 } from "@wordpress/ui";
@@ -21,6 +21,7 @@ import { LinearGradient } from "@visx/gradient";
21
21
  import { curveCatmullRom, curveLinear, curveMonotoneX } from "@visx/curve";
22
22
  import { useParentSize } from "@visx/responsive";
23
23
  import { Annotation, CircleSubject, Connector, HtmlLabel, Label, LineSubject } from "@visx/annotation";
24
+ import { bisector } from "@visx/vendor/d3-array";
24
25
  import { PatternCircles, PatternHexagons, PatternLines, PatternWaves } from "@visx/pattern";
25
26
  import { Chart } from "react-google-charts";
26
27
  import DOMPurify from "dompurify";
@@ -981,7 +982,8 @@ const defaultTheme = {
981
982
  },
982
983
  heatmapChart: {
983
984
  compactCellGap: 2,
984
- compactCellSize: 11
985
+ compactCellSize: 11,
986
+ groupGap: 24
985
987
  }
986
988
  };
987
989
  //#endregion
@@ -1073,7 +1075,7 @@ const GlobalChartsProvider = ({ children, theme, locale, timeZone }) => {
1073
1075
  return getChartColor(index, colorCache);
1074
1076
  }, [colorCache, groupToColorMap]);
1075
1077
  const getElementStyles = useCallback(({ data, index, overrideColor, legendShape }) => {
1076
- const isSeriesData = data && typeof data === "object" && "data" in data && "options" in data;
1078
+ const isSeriesData = data && typeof data === "object" && "data" in data;
1077
1079
  const isPointPercentageData = data && typeof data === "object" && "value" in data && typeof data.value === "number" && !("data" in data);
1078
1080
  return {
1079
1081
  color: resolveColor({
@@ -1454,8 +1456,9 @@ const useChartMargin = (height, options, data, theme, horizontal = false) => {
1454
1456
  const allDataPoints = data.flatMap((series) => series.data);
1455
1457
  if (horizontal) return allDataPoints.map((d) => d.label || d.date?.getTime());
1456
1458
  if (options.axis?.y?.tickValues?.length) return options.axis.y.tickValues;
1457
- const minY = Math.min(...allDataPoints.map((d) => d.value));
1458
- const maxY = Math.max(...allDataPoints.map((d) => d.value));
1459
+ const values = allDataPoints.map((d) => d.value).filter((value) => typeof value === "number" && Number.isFinite(value));
1460
+ const minY = values.length ? Math.min(...values) : 0;
1461
+ const maxY = values.length ? Math.max(...values) : 1;
1459
1462
  const yScale = createScale({
1460
1463
  ...options.yScale,
1461
1464
  domain: options.yScale?.domain ?? [minY, maxY],
@@ -1790,23 +1793,6 @@ function valueOrIdentityString(_) {
1790
1793
  return String(valueOrIdentity(_));
1791
1794
  }
1792
1795
  //#endregion
1793
- //#region src/components/legend/utils/label-transform-factory.ts
1794
- /**
1795
- * Returns a function which takes a Datum and index as input, and returns a formatted label object.
1796
- * @param {object} root0 - The object to return the value of.
1797
- * @param {AnyD3Scale} root0.scale - The scale to use.
1798
- * @param {LabelFormatter<ScaleInput<Scale>>} root0.labelFormat - The label format to use.
1799
- * @return {ItemTransformer<ScaleInput<Scale>, ReturnType<Scale>>} The label transform factory.
1800
- */
1801
- function labelTransformFactory({ scale, labelFormat }) {
1802
- return (d, i) => ({
1803
- datum: d,
1804
- index: i,
1805
- text: `${labelFormat(d, i)}`,
1806
- value: scale(d)
1807
- });
1808
- }
1809
- //#endregion
1810
1796
  //#region src/components/legend/private/base-legend.module.scss
1811
1797
  var base_legend_module_default = {
1812
1798
  "legend": "a8ccharts-04TogW-legend",
@@ -1848,7 +1834,7 @@ const getLegendItemAriaLabel = (text, value, visible, interactive) => {
1848
1834
  if (visible) return;
1849
1835
  return sprintf(_x("%s: hidden", "hidden non-interactive legend item", "jetpack-charts"), accessibleText);
1850
1836
  };
1851
- const BaseLegend = forwardRef(({ items, className, orientation = "horizontal", alignment = "center", shape = "rect", fill = valueOrIdentityString, size = valueOrIdentityString, labelFormat = valueOrIdentity, labelTransform = labelTransformFactory, itemStyles, itemClassName, labelStyles, labelClassName, shapeStyles, render, interactive = false, chartId }, ref) => {
1837
+ const BaseLegend = forwardRef(({ items, className, orientation = "horizontal", alignment = "center", shape = "rect", fill = valueOrIdentityString, size = valueOrIdentityString, labelFormat = valueOrIdentity, labelTransform, itemStyles, itemClassName, labelStyles, labelClassName, shapeStyles, render, interactive = false, chartId }, ref) => {
1852
1838
  const { margin: itemMargin = "0", flexDirection: itemDirection = "row" } = itemStyles ?? {};
1853
1839
  const { justifyContent: labelJustifyContent = "flex-start", flex: labelFlex = "0 0 auto", margin: labelMargin = "0 4px", maxWidth, textOverflow = "wrap" } = labelStyles ?? {};
1854
1840
  const { width: shapeWidth = 16, height: shapeHeight = 16, margin: shapeMargin = "2px 4px 2px 0" } = shapeStyles ?? {};
@@ -1856,11 +1842,17 @@ const BaseLegend = forwardRef(({ items, className, orientation = "horizontal", a
1856
1842
  const context = useContext(GlobalChartsContext);
1857
1843
  const chartInstanceContext = useContext(ChartInstanceContext);
1858
1844
  const standaloneScopeClass = useStandaloneScopeClass();
1845
+ const domain = items.map((item) => item.label);
1859
1846
  const legendScale = scaleOrdinal({
1860
- domain: items.map((item) => item.label),
1847
+ domain,
1861
1848
  range: items.map((item) => item.color)
1862
1849
  });
1863
- const domain = legendScale.domain();
1850
+ const defaultLabelTransform = useCallback(({ labelFormat: format }) => (datum, index) => ({
1851
+ datum,
1852
+ index,
1853
+ text: String(format(datum, index)),
1854
+ value: items[index].color
1855
+ }), [items]);
1864
1856
  const getShapeStyle = useCallback(({ index }) => items[index]?.shapeStyle, [items]);
1865
1857
  const handleLegendClick = useCallback((seriesLabels) => {
1866
1858
  if (interactive && chartId && context) {
@@ -1881,24 +1873,26 @@ const BaseLegend = forwardRef(({ items, className, orientation = "horizontal", a
1881
1873
  chartInstanceContext,
1882
1874
  context
1883
1875
  ]);
1884
- const createClickHandler = useCallback((seriesLabels) => {
1885
- if (!interactive) return;
1876
+ const createClickHandler = useCallback((seriesLabels, itemInteractive) => {
1877
+ if (!itemInteractive) return;
1886
1878
  return () => handleLegendClick(seriesLabels);
1887
- }, [interactive, handleLegendClick]);
1888
- const createKeyDownHandler = useCallback((seriesLabels) => {
1889
- if (!interactive) return;
1879
+ }, [handleLegendClick]);
1880
+ const createKeyDownHandler = useCallback((seriesLabels, itemInteractive) => {
1881
+ if (!itemInteractive) return;
1890
1882
  return (event) => {
1891
1883
  if (event.key === "Enter" || event.key === " ") {
1892
1884
  event.preventDefault();
1893
1885
  handleLegendClick(seriesLabels);
1894
1886
  }
1895
1887
  };
1896
- }, [interactive, handleLegendClick]);
1888
+ }, [handleLegendClick]);
1897
1889
  const flexAlignment = ALIGNMENT_TO_FLEX[alignment] ?? "center";
1890
+ const staticItemRole = interactive ? void 0 : "listitem";
1898
1891
  return render ? render(items) : /* @__PURE__ */ jsx(LegendOrdinal, {
1899
1892
  scale: legendScale,
1893
+ domain,
1900
1894
  labelFormat,
1901
- labelTransform,
1895
+ labelTransform: labelTransform ?? defaultLabelTransform,
1902
1896
  children: (labels) => /* @__PURE__ */ jsx(Stack, {
1903
1897
  ref,
1904
1898
  direction: orientation === "vertical" ? "column" : "row",
@@ -1912,19 +1906,20 @@ const BaseLegend = forwardRef(({ items, className, orientation = "horizontal", a
1912
1906
  children: labels.map((label, i) => {
1913
1907
  const matchedItem = items[i];
1914
1908
  const seriesLabels = matchedItem?.seriesLabels?.length ? matchedItem.seriesLabels : [label.text];
1915
- const visible = isSeriesVisible(seriesLabels[0]);
1916
- const handleClick = createClickHandler(seriesLabels);
1917
- const handleKeyDown = createKeyDownHandler(seriesLabels);
1909
+ const visible = matchedItem?.interactive === false || isSeriesVisible(seriesLabels[0]);
1910
+ const itemInteractive = interactive && matchedItem?.interactive !== false;
1911
+ const handleClick = createClickHandler(seriesLabels, itemInteractive);
1912
+ const handleKeyDown = createKeyDownHandler(seriesLabels, itemInteractive);
1918
1913
  return /* @__PURE__ */ jsxs(LegendItem, {
1919
- className: clsx("visx-legend-item", base_legend_module_default["legend-item"], interactive && base_legend_module_default["legend-item--interactive"], !visible && base_legend_module_default["legend-item--inactive"], itemClassName),
1914
+ className: clsx("visx-legend-item", base_legend_module_default["legend-item"], itemInteractive && base_legend_module_default["legend-item--interactive"], !visible && base_legend_module_default["legend-item--inactive"], itemClassName),
1920
1915
  margin: itemMargin,
1921
1916
  flexDirection: orientation === "vertical" && alignment === "end" ? "row-reverse" : itemDirection,
1922
1917
  onClick: handleClick,
1923
1918
  onKeyDown: handleKeyDown,
1924
- role: interactive ? "button" : "listitem",
1925
- tabIndex: interactive ? 0 : void 0,
1926
- "aria-pressed": interactive ? visible : void 0,
1927
- "aria-label": getLegendItemAriaLabel(label.text, matchedItem?.value, visible, interactive),
1919
+ role: itemInteractive ? "button" : staticItemRole,
1920
+ tabIndex: itemInteractive ? 0 : void 0,
1921
+ "aria-pressed": itemInteractive ? visible : void 0,
1922
+ "aria-label": getLegendItemAriaLabel(label.text, matchedItem?.value, visible, itemInteractive),
1928
1923
  children: [items[i]?.renderGlyph ? /* @__PURE__ */ jsx("svg", {
1929
1924
  width: items[i]?.glyphSize * 2,
1930
1925
  height: items[i]?.glyphSize * 2,
@@ -2126,6 +2121,32 @@ function processSeriesData(seriesData, getElementStyles, showValues, withGlyph,
2126
2121
  });
2127
2122
  }
2128
2123
  /**
2124
+ * Builds the static item that names the comparison overlay, styled like its first series.
2125
+ * Skipped when a comparison series already has an item of its own, so nothing is listed twice.
2126
+ * @param seriesData - The series data to search for a comparison series
2127
+ * @param items - The legend items already built from the series
2128
+ * @param label - The item label
2129
+ * @param getElementStyles - Function to get element styles
2130
+ * @param legendShape - The shape type for legend items (string literal or React component)
2131
+ * @return The legend item, or null when it would add nothing
2132
+ */
2133
+ function buildComparisonLegendItem(seriesData, items, label, getElementStyles, legendShape) {
2134
+ const itemLabels = new Set(items.map((item) => item.label));
2135
+ const index = seriesData.findIndex((series) => series.options?.type === "comparison" && !itemLabels.has(series.label));
2136
+ if (index === -1) return null;
2137
+ const { color, shapeStyles } = getElementStyles({
2138
+ data: seriesData[index],
2139
+ index,
2140
+ legendShape
2141
+ });
2142
+ return {
2143
+ label,
2144
+ color,
2145
+ shapeStyle: shapeStyles,
2146
+ interactive: false
2147
+ };
2148
+ }
2149
+ /**
2129
2150
  * Processes point data into legend items
2130
2151
  * @param pointData - The point data to process
2131
2152
  * @param getElementStyles - Function to get element styles
@@ -2161,11 +2182,16 @@ function processPointData(pointData, getElementStyles, showValues, legendValueDi
2161
2182
  * @return Array of legend items ready for display
2162
2183
  */
2163
2184
  function useChartLegendItems(data, options = {}, legendShape) {
2164
- const { showValues = false, legendValueDisplay = "percentage", withGlyph = false, glyphSize = 8, collapseGroups = false, renderGlyph } = options;
2185
+ const { showValues = false, legendValueDisplay = "percentage", withGlyph = false, glyphSize = 8, collapseGroups = false, comparisonItem = false, renderGlyph } = options;
2165
2186
  const { getElementStyles } = useGlobalChartsContext();
2166
2187
  return useMemo(() => {
2167
2188
  if (!data || !Array.isArray(data) || data.length === 0) return [];
2168
- if ("data" in data[0]) return processSeriesData(data, getElementStyles, showValues, withGlyph, glyphSize, collapseGroups, renderGlyph, legendShape);
2189
+ if ("data" in data[0]) {
2190
+ const seriesData = data;
2191
+ const items = processSeriesData(seriesData, getElementStyles, showValues, withGlyph, glyphSize, collapseGroups, renderGlyph, legendShape);
2192
+ const comparison = comparisonItem ? buildComparisonLegendItem(seriesData, items, typeof comparisonItem === "string" ? comparisonItem : __("Comparison period", "jetpack-charts"), getElementStyles, legendShape) : null;
2193
+ return comparison ? [...items, comparison] : items;
2194
+ }
2169
2195
  return processPointData(data, getElementStyles, showValues, legendValueDisplay, withGlyph, glyphSize, renderGlyph, legendShape);
2170
2196
  }, [
2171
2197
  data,
@@ -2175,13 +2201,17 @@ function useChartLegendItems(data, options = {}, legendShape) {
2175
2201
  withGlyph,
2176
2202
  glyphSize,
2177
2203
  collapseGroups,
2204
+ comparisonItem,
2178
2205
  renderGlyph,
2179
2206
  legendShape
2180
2207
  ]);
2181
2208
  }
2182
2209
  //#endregion
2183
2210
  //#region src/components/tooltip/base-tooltip.module.scss
2184
- var base_tooltip_module_default = { "tooltip": "a8ccharts--zY0xG-tooltip" };
2211
+ var base_tooltip_module_default = {
2212
+ "surface": "a8ccharts--zY0xG-surface",
2213
+ "tooltip": "a8ccharts--zY0xG-tooltip"
2214
+ };
2185
2215
  //#endregion
2186
2216
  //#region src/components/tooltip/base-tooltip.tsx
2187
2217
  const DefaultTooltipContent = ({ data }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
@@ -2231,7 +2261,8 @@ const findClippingAncestor = (wrapper) => {
2231
2261
  /**
2232
2262
  * Where the box goes, in wrapper coordinates. Automatic placement flips to
2233
2263
  * the side that clips less, then clamps inside `bounds`; below-axis placement
2234
- * is centered on the anchor and clamped horizontally.
2264
+ * is centered on the anchor and clamped horizontally; beside placement preserves
2265
+ * the anchor's y and only flips horizontally before bounds clamping.
2235
2266
  *
2236
2267
  * The flip measures against the visible part of the wrapper, its intersection
2237
2268
  * with `bounds`. Only the clamp is bound by the page: a box that fits inside the
@@ -2245,7 +2276,7 @@ const findClippingAncestor = (wrapper) => {
2245
2276
  * @param params.offsetTop - Gap between the anchor and the box, vertically.
2246
2277
  * @param params.box - Rendered size of the box.
2247
2278
  * @param params.wrapper - The wrapper's own edges, in wrapper coordinates.
2248
- * @param params.placement - Fixed below-axis placement or automatic flipping.
2279
+ * @param params.placement - Below-axis, beside without vertical flipping, or automatic flipping.
2249
2280
  * @param params.bounds - Edges the box must keep inside, in wrapper coordinates.
2250
2281
  * @return The box's top-left corner; fixed placement preserves the axis's subpixel y.
2251
2282
  */
@@ -2269,7 +2300,8 @@ const getBoundedPosition = ({ left, top, offsetLeft, offsetTop, box, wrapper, bo
2269
2300
  const upY = top - offsetTop - box.height;
2270
2301
  const downOverflow = downY + box.height - fit.bottom;
2271
2302
  const upOverflow = fit.top - upY;
2272
- let y = downOverflow > 0 && downOverflow > upOverflow ? upY : downY;
2303
+ let y = top;
2304
+ if (placement !== "beside") y = downOverflow > 0 && downOverflow > upOverflow ? upY : downY;
2273
2305
  x = clamp(x, bounds.left, bounds.right, box.width);
2274
2306
  y = clamp(y, bounds.top, bounds.bottom, box.height);
2275
2307
  return {
@@ -2285,8 +2317,7 @@ const getBoundedPosition = ({ left, top, offsetLeft, offsetTop, box, wrapper, bo
2285
2317
  * alone would let the box spill into an `overflow: hidden` card and be cut off.
2286
2318
  *
2287
2319
  * Re-measures on every render, so a box whose content changes width between
2288
- * two hovers is placed for its current size. See `getBoundedPosition` for the
2289
- * below-axis exception.
2320
+ * two hovers is placed for its current size.
2290
2321
  *
2291
2322
  * @param props - visx `Tooltip` props.
2292
2323
  * @param props.left - Anchor x, in wrapper coordinates.
@@ -2296,7 +2327,7 @@ const getBoundedPosition = ({ left, top, offsetLeft, offsetTop, box, wrapper, bo
2296
2327
  * @param props.style - Box styles; visx's defaults unless `unstyled`.
2297
2328
  * @param props.unstyled - Skip `style` and leave the box bare.
2298
2329
  * @param props.children - Box content.
2299
- * @param props.placement - Fixed below-axis placement or automatic flipping.
2330
+ * @param props.placement - Below-axis, beside without vertical flipping, or automatic flipping.
2300
2331
  * @return The tooltip box.
2301
2332
  */
2302
2333
  const BoundedTooltip = ({ left = 0, top = 0, offsetLeft = DEFAULT_OFFSET, offsetTop = DEFAULT_OFFSET, style = defaultStyles, unstyled = false, children, placement = "auto", ...rest }) => {
@@ -2399,7 +2430,6 @@ const crosshairPaintProps = (props = {}) => {
2399
2430
  };
2400
2431
  const DEFAULT_GLYPH_RADIUS = 4;
2401
2432
  const FALLBACK_COLOR = "#222";
2402
- const DEFAULT_TOOLTIP_Z_INDEX = 3;
2403
2433
  const PORTAL_OPTIONS = /* @__PURE__ */ new Set([
2404
2434
  "scroll",
2405
2435
  "debounce",
@@ -2427,7 +2457,7 @@ const defaultRenderGlyph$1 = ({ key, ...props }) => /* @__PURE__ */ jsx(DefaultG
2427
2457
  seriesKey: key,
2428
2458
  ...props
2429
2459
  }, key);
2430
- const XyChartTooltipContent = ({ tooltipContext, container, renderTooltip, renderGlyph = defaultRenderGlyph$1, glyphStyle, snapTooltipToDatumX = false, snapTooltipToDatumY = false, showVerticalCrosshair = false, showHorizontalCrosshair = false, showDatumGlyph = false, showSeriesGlyphs = false, verticalCrosshairStyle, horizontalCrosshairStyle, detectBounds = true, tooltipPlacement = "auto", zIndex = DEFAULT_TOOLTIP_Z_INDEX, style, ...rest }) => {
2460
+ const XyChartTooltipContent = ({ tooltipContext, container, renderTooltip, renderGlyph = defaultRenderGlyph$1, glyphStyle, snapTooltipToDatumX = false, snapTooltipToDatumY = false, showVerticalCrosshair = false, showHorizontalCrosshair = false, showDatumGlyph = false, showSeriesGlyphs = false, verticalCrosshairStyle, horizontalCrosshairStyle, detectBounds = true, tooltipPlacement = "auto", tooltipAnchorTop, zIndex = 3, style, ...rest }) => {
2431
2461
  const tooltipProps = Object.fromEntries(Object.entries(rest).filter(([key]) => !PORTAL_OPTIONS.has(key)));
2432
2462
  const { colorScale, theme, innerHeight = 0, innerWidth = 0, margin, xScale, yScale, dataRegistry } = useContext(DataContext) || {};
2433
2463
  const tooltipContent = renderTooltip ? renderTooltip({
@@ -2482,7 +2512,7 @@ const XyChartTooltipContent = ({ tooltipContext, container, renderTooltip, rende
2482
2512
  const crosshairStroke = theme?.gridStyles?.stroke ?? theme?.htmlLabel?.color ?? FALLBACK_COLOR;
2483
2513
  const marginTop = margin?.top ?? 0;
2484
2514
  const marginLeft = margin?.left ?? 0;
2485
- const TooltipComponent = detectBounds || tooltipPlacement === "below-axis" ? BoundedTooltip : Tooltip;
2515
+ const TooltipComponent = detectBounds || tooltipPlacement !== "auto" ? BoundedTooltip : Tooltip;
2486
2516
  const boxStyle = {
2487
2517
  ...defaultStyles,
2488
2518
  zIndex,
@@ -2519,11 +2549,11 @@ const XyChartTooltipContent = ({ tooltipContext, container, renderTooltip, rende
2519
2549
  ]
2520
2550
  }), container && createPortal(/* @__PURE__ */ jsx(TooltipComponent, {
2521
2551
  left: tooltipLeft,
2522
- top: tooltipPlacement === "below-axis" ? marginTop + innerHeight + (margin?.bottom ?? 0) : tooltipTop,
2552
+ top: tooltipPlacement === "below-axis" ? marginTop + innerHeight + (margin?.bottom ?? 0) : tooltipAnchorTop ?? tooltipTop,
2523
2553
  style: boxStyle,
2524
2554
  applyPositionStyle: true,
2525
2555
  ...tooltipProps,
2526
- ...tooltipPlacement === "below-axis" && { placement: tooltipPlacement },
2556
+ ...tooltipPlacement !== "auto" && { placement: tooltipPlacement },
2527
2557
  children: tooltipContent
2528
2558
  }), container)] });
2529
2559
  };
@@ -2589,11 +2619,32 @@ const AccessibleTooltip = ({ renderTooltip, selectedIndex, tooltipRef, keyboardF
2589
2619
  return flattened;
2590
2620
  }, [series, mode]);
2591
2621
  const hasKeyboardSelection = useRef(false);
2622
+ const closeTooltipNow = useCallback(() => {
2623
+ tooltipContext?.updateTooltip({
2624
+ tooltipOpen: false,
2625
+ tooltipLeft: void 0,
2626
+ tooltipTop: void 0,
2627
+ tooltipData: void 0
2628
+ });
2629
+ }, []);
2630
+ const chartId = useContext(ChartInstanceContext)?.chartId;
2631
+ const { getHiddenSeries } = useGlobalChartsContext();
2632
+ const hiddenSeriesKey = chartId ? JSON.stringify([...getHiddenSeries(chartId)].sort()) : void 0;
2633
+ const lastHiddenSeriesKey = useRef(hiddenSeriesKey);
2634
+ useEffect(() => {
2635
+ const changed = lastHiddenSeriesKey.current !== hiddenSeriesKey;
2636
+ lastHiddenSeriesKey.current = hiddenSeriesKey;
2637
+ if (changed && selectedIndex === void 0) closeTooltipNow();
2638
+ }, [
2639
+ hiddenSeriesKey,
2640
+ selectedIndex,
2641
+ closeTooltipNow
2642
+ ]);
2592
2643
  useEffect(() => {
2593
2644
  if (selectedIndex === void 0) {
2594
2645
  if (hasKeyboardSelection.current) {
2595
2646
  hasKeyboardSelection.current = false;
2596
- tooltipContext?.hideTooltip();
2647
+ closeTooltipNow();
2597
2648
  }
2598
2649
  return;
2599
2650
  }
@@ -2614,7 +2665,7 @@ const AccessibleTooltip = ({ renderTooltip, selectedIndex, tooltipRef, keyboardF
2614
2665
  tooltipContext?.showTooltip({
2615
2666
  datum: tooltipItem.datum,
2616
2667
  key: tooltipItem.seriesLabel,
2617
- index: tooltipItem.seriesIndex
2668
+ index: tooltipItem.dataPointIndex
2618
2669
  });
2619
2670
  }
2620
2671
  }
@@ -2662,14 +2713,43 @@ const AccessibleTooltip = ({ renderTooltip, selectedIndex, tooltipRef, keyboardF
2662
2713
  renderTooltip: focusableRenderTooltip
2663
2714
  });
2664
2715
  };
2665
- const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating, setIsNavigating, chartRef, totalPoints, onActivate, preventTooltipScroll = false }) => {
2716
+ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating, setIsNavigating, chartRef, totalPoints, onActivate, preventTooltipScroll = false, visibleSeriesKey }) => {
2717
+ const getChartRoot = useCallback(() => chartRef.current?.closest("[role=\"grid\"]") ?? chartRef.current, [chartRef]);
2718
+ const focusWithoutScrollIfNeeded = useCallback((element) => {
2719
+ if (preventTooltipScroll) element?.focus({ preventScroll: true });
2720
+ else element?.focus();
2721
+ }, [preventTooltipScroll]);
2722
+ const tooltipRef = useCallback((element) => {
2723
+ if (element && selectedIndex !== void 0) focusWithoutScrollIfNeeded(element);
2724
+ }, [focusWithoutScrollIfNeeded, selectedIndex]);
2725
+ const previousTotalPoints = useRef(totalPoints);
2726
+ const previousVisibleSeriesKey = useRef(visibleSeriesKey);
2727
+ useEffect(() => {
2728
+ const countChanged = previousTotalPoints.current !== totalPoints;
2729
+ const seriesChanged = previousVisibleSeriesKey.current !== visibleSeriesKey;
2730
+ previousTotalPoints.current = totalPoints;
2731
+ previousVisibleSeriesKey.current = visibleSeriesKey;
2732
+ if (!countChanged && !seriesChanged || selectedIndex === void 0) return;
2733
+ const { activeElement } = document;
2734
+ const focusIsInChart = activeElement !== null && !!getChartRoot()?.contains(activeElement);
2735
+ if (totalPoints === 0 || !focusIsInChart || !countChanged) {
2736
+ setSelectedIndex(void 0);
2737
+ setIsNavigating(false);
2738
+ if (focusIsInChart) focusWithoutScrollIfNeeded(getChartRoot());
2739
+ return;
2740
+ }
2741
+ setSelectedIndex(Math.min(selectedIndex, totalPoints - 1));
2742
+ }, [
2743
+ selectedIndex,
2744
+ totalPoints,
2745
+ visibleSeriesKey,
2746
+ setSelectedIndex,
2747
+ setIsNavigating,
2748
+ getChartRoot,
2749
+ focusWithoutScrollIfNeeded
2750
+ ]);
2666
2751
  return {
2667
- tooltipRef: useCallback((element) => {
2668
- if (element && selectedIndex !== void 0) {
2669
- if (preventTooltipScroll) element.focus({ preventScroll: true });
2670
- else element.focus();
2671
- }
2672
- }, [preventTooltipScroll, selectedIndex]),
2752
+ tooltipRef,
2673
2753
  onChartFocus: useCallback((event) => {
2674
2754
  if (event.currentTarget.contains(event.relatedTarget)) return;
2675
2755
  if (!isNavigating && selectedIndex !== void 0) setSelectedIndex(0);
@@ -2682,44 +2762,35 @@ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating,
2682
2762
  setIsNavigating(false);
2683
2763
  }, [setIsNavigating]),
2684
2764
  onChartKeyDown: useCallback((event) => {
2685
- if (totalPoints === 0) return;
2686
- const focusChart = () => {
2687
- if (preventTooltipScroll) chartRef.current?.focus({ preventScroll: true });
2688
- else chartRef.current?.focus();
2689
- };
2690
- if (event.key === "Tab") {
2691
- focusChart();
2765
+ if (event.key === "Tab" || event.key === "Escape") {
2766
+ if (event.key === "Escape" && selectedIndex !== void 0) event.preventDefault();
2767
+ focusWithoutScrollIfNeeded(getChartRoot());
2692
2768
  setSelectedIndex(void 0);
2693
2769
  setIsNavigating(false);
2694
2770
  return;
2695
2771
  }
2772
+ if (totalPoints === 0) return;
2696
2773
  const currentSelectedIndex = selectedIndex === void 0 ? -1 : selectedIndex;
2697
- if (currentSelectedIndex + 1 >= totalPoints && ["ArrowRight"].includes(event.key)) {
2698
- focusChart();
2699
- setSelectedIndex(void 0);
2700
- setIsNavigating(false);
2701
- return;
2702
- }
2703
- event.preventDefault();
2704
- if (["ArrowRight"].includes(event.key)) {
2774
+ if (event.key === "ArrowRight") {
2775
+ event.preventDefault();
2705
2776
  setIsNavigating(true);
2706
- setSelectedIndex((currentSelectedIndex + 1) % totalPoints);
2707
- } else if (["ArrowLeft"].includes(event.key)) {
2777
+ setSelectedIndex(Math.min(currentSelectedIndex + 1, totalPoints - 1));
2778
+ } else if (event.key === "ArrowLeft") {
2779
+ event.preventDefault();
2708
2780
  setIsNavigating(true);
2709
- setSelectedIndex((currentSelectedIndex - 1 + totalPoints) % totalPoints);
2710
- } else if (event.key === "Escape") {
2711
- setSelectedIndex(void 0);
2712
- setIsNavigating(false);
2713
- focusChart();
2714
- } else if ((event.key === "Enter" || event.key === " ") && selectedIndex !== void 0) onActivate?.(selectedIndex);
2781
+ setSelectedIndex(Math.max(currentSelectedIndex - 1, 0));
2782
+ } else if ((event.key === "Enter" || event.key === " ") && selectedIndex !== void 0) {
2783
+ event.preventDefault();
2784
+ onActivate?.(selectedIndex);
2785
+ }
2715
2786
  }, [
2716
2787
  totalPoints,
2717
2788
  selectedIndex,
2718
2789
  setSelectedIndex,
2719
2790
  setIsNavigating,
2720
- chartRef,
2721
- onActivate,
2722
- preventTooltipScroll
2791
+ getChartRoot,
2792
+ focusWithoutScrollIfNeeded,
2793
+ onActivate
2723
2794
  ])
2724
2795
  };
2725
2796
  };
@@ -2917,6 +2988,31 @@ const DefaultGlyph = (props) => {
2917
2988
  });
2918
2989
  };
2919
2990
  //#endregion
2991
+ //#region src/charts/private/readings.ts
2992
+ /**
2993
+ * Whether a value is a reading a scale can place.
2994
+ *
2995
+ * @param value - The point's value.
2996
+ * @return True for a finite number.
2997
+ */
2998
+ const isReading = (value) => typeof value === "number" && Number.isFinite(value);
2999
+ /**
3000
+ * Whether a data point's value cannot be drawn.
3001
+ *
3002
+ * @param value - The point's value.
3003
+ * @param options - Validation options.
3004
+ * @param options.allowMissing - Whether `null` is a bucket with no reading rather than a fault.
3005
+ * @return True when the value is invalid.
3006
+ */
3007
+ const isInvalidReading = (value, { allowMissing }) => value === null ? !allowMissing : value === void 0 || isNaN(value);
3008
+ /**
3009
+ * Formats a reading for a tooltip.
3010
+ *
3011
+ * @param value - The reading, or null when the bucket has none.
3012
+ * @return The formatted value.
3013
+ */
3014
+ const formatReading = (value) => value == null ? __("No data", "jetpack-charts") : formatNumber(value);
3015
+ //#endregion
2920
3016
  //#region src/charts/private/center/center.module.scss
2921
3017
  var center_module_default = { "center": "a8ccharts-w3qxlG-center" };
2922
3018
  //#endregion
@@ -3289,6 +3385,54 @@ const buildTimeAxisOptions = ({ dataSorted, width, axisOptions, scaleDomain, zoo
3289
3385
  };
3290
3386
  };
3291
3387
  //#endregion
3388
+ //#region src/charts/private/whole-number-ticks.tsx
3389
+ /**
3390
+ * Whether every reading in the series is a whole number.
3391
+ *
3392
+ * @param series - The series visx renders.
3393
+ * @return True when at least one reading exists and none has a fraction.
3394
+ */
3395
+ const hasOnlyWholeNumbers = (series) => {
3396
+ let sawReading = false;
3397
+ for (const { data } of series) for (const point of data) {
3398
+ const value = point?.value;
3399
+ if (!isReading(value)) continue;
3400
+ if (!Number.isInteger(value)) return false;
3401
+ sawReading = true;
3402
+ }
3403
+ return sawReading;
3404
+ };
3405
+ /**
3406
+ * The scale's ticks with the fractional ones dropped.
3407
+ *
3408
+ * d3 steps by 1, 2 or 5 times a power of ten, so every whole number inside the domain is one of its ticks.
3409
+ *
3410
+ * @param scale - The value scale visx built.
3411
+ * @param count - Ticks to ask the scale for; undefined uses the scale's own default, as visx does.
3412
+ * @return Whole-number ticks, or undefined when visx's own ticks need no change.
3413
+ */
3414
+ const getWholeNumberTickValues = (scale, count) => {
3415
+ const ticks = scale?.ticks?.(count);
3416
+ if (!ticks?.every((tick) => typeof tick === "number")) return;
3417
+ const whole = ticks.filter((tick) => Number.isInteger(tick));
3418
+ return whole.length > 1 && whole.length < ticks.length ? whole : void 0;
3419
+ };
3420
+ /**
3421
+ * Hands its children the whole-number ticks of the chart's value scale.
3422
+ *
3423
+ * @param props - Component props.
3424
+ * @param props.axis - Which scale carries the values.
3425
+ * @param props.numTicks - Ticks to ask the scale for.
3426
+ * @param props.enabled - Whether the plotted values are all whole numbers and the caller pinned neither the ticks nor the value domain.
3427
+ * @param props.children - Renders the axis and grid with the tick values.
3428
+ * @return The rendered children.
3429
+ */
3430
+ const WholeNumberTicks = ({ axis, numTicks, enabled, children }) => {
3431
+ const { xScale, yScale } = useContext(DataContext);
3432
+ const tickValues = enabled ? getWholeNumberTickValues(axis === "x" ? xScale : yScale, numTicks) : void 0;
3433
+ return /* @__PURE__ */ jsx(Fragment$1, { children: children(tickValues) });
3434
+ };
3435
+ //#endregion
3292
3436
  //#region src/charts/private/with-responsive/with-responsive.module.scss
3293
3437
  var with_responsive_module_default = {
3294
3438
  "container": "a8ccharts-sP1gHa-container",
@@ -3955,11 +4099,15 @@ const toNumber$1 = (val) => {
3955
4099
  const LineChartGlyph = ({ data, index, color, glyphStyle, renderGlyph, accessors, position }) => {
3956
4100
  const { xScale, yScale } = useContext(DataContext) || {};
3957
4101
  if (!xScale || !yScale) return null;
3958
- if (data.data.length === 0) return null;
3959
- const point = position === "start" ? data.data[0] : data.data[data.data.length - 1];
4102
+ const hasFiniteY = (datum) => {
4103
+ const scaledY = yScale(accessors.yAccessor(datum));
4104
+ return typeof scaledY === "number" && Number.isFinite(scaledY);
4105
+ };
4106
+ const point = position === "start" ? data.data.find(hasFiniteY) : data.data.findLast(hasFiniteY);
4107
+ if (!point) return null;
3960
4108
  const x = xScale(accessors.xAccessor(point));
3961
4109
  const y = yScale(accessors.yAccessor(point));
3962
- if (typeof x !== "number" || typeof y !== "number") return null;
4110
+ if (typeof x !== "number" || typeof y !== "number" || !Number.isFinite(x) || !Number.isFinite(y)) return null;
3963
4111
  const size = Math.max(0, toNumber$1(glyphStyle?.radius) ?? 4);
3964
4112
  return renderGlyph({
3965
4113
  key: `${position}-glyph-${data.label}`,
@@ -3974,6 +4122,106 @@ const LineChartGlyph = ({ data, index, color, glyphStyle, renderGlyph, accessors
3974
4122
  });
3975
4123
  };
3976
4124
  //#endregion
4125
+ //#region src/charts/line-chart/private/nearest-pointer-events.tsx
4126
+ /**
4127
+ * Find the datum nearest a pointer along x, in one series sorted by x.
4128
+ *
4129
+ * @param data - The series' registered data, in ascending x order.
4130
+ * @param xAccessor - Reads a datum's x value.
4131
+ * @param xScale - The chart's x scale.
4132
+ * @param x - Pointer x in SVG coordinates.
4133
+ * @return Index of the nearest datum and its distance, or index -1 when none has a position.
4134
+ */
4135
+ function nearestByX(data, xAccessor, xScale, x) {
4136
+ return (typeof xScale.invert === "function" ? [bisector(xAccessor).left(data, xScale.invert(x))].flatMap((index) => [index - 1, index]) : data.map((_datum, index) => index)).filter((index) => index >= 0 && index < data.length).reduce((best, index) => {
4137
+ const distance = Math.abs(Number(xScale(xAccessor(data[index]))) - x);
4138
+ return distance < best.distance ? {
4139
+ index,
4140
+ distance
4141
+ } : best;
4142
+ }, {
4143
+ index: -1,
4144
+ distance: Infinity
4145
+ });
4146
+ }
4147
+ /**
4148
+ * Pick the candidate to report: the closest placed reading, else the closest bucket with no reading along x.
4149
+ *
4150
+ * @param candidates - One candidate per series.
4151
+ * @return The chosen candidate's params, or undefined when there are none.
4152
+ */
4153
+ function pickNearest(candidates) {
4154
+ const placed = candidates.map(({ params }) => params).filter((params) => Number.isFinite(params.distanceY));
4155
+ const missing = candidates.filter(({ isMissing }) => isMissing).map(({ params }) => params);
4156
+ const distance = (params) => placed.length ? Math.hypot(params.distanceX ?? 0, params.distanceY ?? 0) : params.distanceX ?? 0;
4157
+ return (placed.length ? placed : missing).reduce((best, params) => !best || distance(params) < distance(best) ? params : best, void 0);
4158
+ }
4159
+ /**
4160
+ * Report pointer events at the nearest datum, including a bucket with no reading.
4161
+ *
4162
+ * visx's own nearest search measures y distance too, which is NaN for a null value, so a pointer
4163
+ * over a bucket where no series has a reading fires no event at all.
4164
+ *
4165
+ * @param props - Handlers to call.
4166
+ * @param props.onPointerDown - Receives the nearest datum on pointer down.
4167
+ * @param props.onPointerMove - Receives the nearest datum on pointer move.
4168
+ * @param props.onPointerUp - Receives the nearest datum on pointer up.
4169
+ * @return No visual content.
4170
+ */
4171
+ function NearestPointerEvents({ onPointerDown, onPointerMove, onPointerUp }) {
4172
+ const { xScale, yScale, dataRegistry } = useContext(DataContext);
4173
+ const findNearest = useCallback((params) => {
4174
+ const point = params?.svgPoint;
4175
+ if (!point || !xScale || !yScale || !dataRegistry) return;
4176
+ const candidates = [];
4177
+ for (const key of dataRegistry.keys()) {
4178
+ const entry = dataRegistry.get(key);
4179
+ if (!entry) continue;
4180
+ const { index, distance } = nearestByX(entry.data, entry.xAccessor, xScale, point.x);
4181
+ if (index < 0) continue;
4182
+ const datum = entry.data[index];
4183
+ const value = entry.yAccessor(datum);
4184
+ candidates.push({
4185
+ params: {
4186
+ event: params.event,
4187
+ svgPoint: point,
4188
+ key,
4189
+ datum,
4190
+ index,
4191
+ distanceX: distance,
4192
+ distanceY: Math.abs(Number(yScale(value)) - point.y)
4193
+ },
4194
+ isMissing: value == null
4195
+ });
4196
+ }
4197
+ return pickNearest(candidates);
4198
+ }, [
4199
+ xScale,
4200
+ yScale,
4201
+ dataRegistry
4202
+ ]);
4203
+ const handlers = useMemo(() => {
4204
+ const report = (handler) => handler && ((params) => {
4205
+ const nearest = findNearest(params);
4206
+ if (nearest) handler(nearest);
4207
+ });
4208
+ return {
4209
+ pointerdown: report(onPointerDown),
4210
+ pointermove: report(onPointerMove),
4211
+ pointerup: report(onPointerUp)
4212
+ };
4213
+ }, [
4214
+ findNearest,
4215
+ onPointerDown,
4216
+ onPointerMove,
4217
+ onPointerUp
4218
+ ]);
4219
+ useEventEmitter("pointerdown", handlers.pointerdown);
4220
+ useEventEmitter("pointermove", handlers.pointermove);
4221
+ useEventEmitter("pointerup", handlers.pointerup);
4222
+ return null;
4223
+ }
4224
+ //#endregion
3977
4225
  //#region src/charts/line-chart/line-chart.tsx
3978
4226
  const defaultRenderGlyph = (props) => {
3979
4227
  return /* @__PURE__ */ createElement(DefaultGlyph, {
@@ -4017,8 +4265,13 @@ const renderDefaultTooltip = (params, contentStyle) => {
4017
4265
  if (!nearestDatum) return null;
4018
4266
  const tooltipPoints = Object.entries(tooltipData?.datumByKey || {}).map(([key, { datum }]) => ({
4019
4267
  key,
4020
- value: datum.value
4021
- })).sort((a, b) => b.value - a.value);
4268
+ value: datum.value ?? null
4269
+ })).sort((a, b) => {
4270
+ if (a.value === null && b.value === null) return 0;
4271
+ if (a.value === null) return 1;
4272
+ if (b.value === null) return -1;
4273
+ return b.value - a.value;
4274
+ });
4022
4275
  return /* @__PURE__ */ jsxs("div", {
4023
4276
  className: line_chart_module_default["line-chart__tooltip"],
4024
4277
  style: contentStyle,
@@ -4038,16 +4291,23 @@ const renderDefaultTooltip = (params, contentStyle) => {
4038
4291
  children: [point.key, ":"]
4039
4292
  }), /* @__PURE__ */ jsx("span", {
4040
4293
  className: line_chart_module_default["line-chart__tooltip-value"],
4041
- children: formatNumber(point.value)
4294
+ children: formatReading(point.value)
4042
4295
  })]
4043
4296
  }, point.key))]
4044
4297
  });
4045
4298
  };
4046
4299
  const validateData$4 = (data) => {
4047
- if (!data?.length) return "No data available";
4048
- if (data.some((series) => series.data.some((point) => isNaN(point.value) || point.value === null || point.value === void 0 || "date" in point && point.date && isNaN(point.date.getTime())))) return "Invalid data";
4300
+ if (!data?.length) return __("No data available", "jetpack-charts");
4301
+ if (data.some((series) => series.data.some((point) => isInvalidReading(point.value, { allowMissing: true }) || "date" in point && point.date && isNaN(point.date.getTime())))) return __("Invalid data", "jetpack-charts");
4049
4302
  return null;
4050
4303
  };
4304
+ const getFallbackYDomain = (readingExtent, isLogScale) => {
4305
+ const emptyDomain = isLogScale ? [1, 10] : [0, 1];
4306
+ if (!readingExtent) return emptyDomain;
4307
+ const [min, max] = readingExtent;
4308
+ if (min !== max || isLogScale) return;
4309
+ return min === 0 ? emptyDomain : [Math.min(0, min), Math.max(0, max)];
4310
+ };
4051
4311
  const LineChartScalesRef = ({ chartRef, width, height, margin }) => {
4052
4312
  const context = useContext(DataContext);
4053
4313
  useImperativeHandle(chartRef, () => ({
@@ -4074,6 +4334,7 @@ const LineChartScalesRef = ({ chartRef, width, height, margin }) => {
4074
4334
  const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, height, className, margin, withTooltips = true, withTooltipCrosshairs, showLegend = false, legend = {}, renderGlyph = defaultRenderGlyph, glyphStyle = {}, withLegendGlyph = false, withGradientFill = false, smoothing = true, curveType, renderTooltip = renderDefaultTooltip, tooltipPlacement, tooltipStyle, withStartGlyphs = false, withEndGlyphs = false, animation, options = {}, onPointerDown = void 0, onPointerUp = void 0, onPointerMove = void 0, onPointerOut = void 0, onDatumActivate = void 0, zoomable = false, rescaleYOnVisibilityChange = true, defaultHiddenSeries, children, gridVisibility, gap = "md" }, ref) => {
4075
4335
  const legendInteractive = legend.interactive ?? false;
4076
4336
  const legendCollapseGroups = legend.collapseGroups ?? false;
4337
+ const legendComparisonItem = legend.comparisonItem ?? false;
4077
4338
  const legendShape = legend.shape ?? "line";
4078
4339
  const legendPosition = legend.position ?? "bottom";
4079
4340
  const formatting = useChartFormatting();
@@ -4120,19 +4381,32 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4120
4381
  const allSeriesHidden = useMemo(() => {
4121
4382
  return seriesWithVisibility.every(({ isVisible }) => !isVisible);
4122
4383
  }, [seriesWithVisibility]);
4123
- const stableYDomain = useMemo(() => {
4124
- if (rescaleYOnVisibilityChange) return;
4384
+ const yDomain = useMemo(() => {
4385
+ const includeZero = options?.yScale?.zero === true && scaleCanBeZeroed({ type: options.yScale.type ?? "linear" });
4386
+ if (rescaleYOnVisibilityChange && !includeZero) return;
4125
4387
  let min = Infinity;
4126
4388
  let max = -Infinity;
4127
- for (const series of dataSorted) for (const point of series.data ?? []) {
4128
- const value = point?.value;
4129
- if (typeof value === "number" && Number.isFinite(value)) {
4130
- min = Math.min(min, value);
4131
- max = Math.max(max, value);
4389
+ for (const series of dataSorted) {
4390
+ if (rescaleYOnVisibilityChange && !isSeriesVisible(series.label)) continue;
4391
+ for (const point of series.data ?? []) {
4392
+ const value = point?.value;
4393
+ if (isReading(value)) {
4394
+ min = Math.min(min, value);
4395
+ max = Math.max(max, value);
4396
+ }
4132
4397
  }
4133
4398
  }
4399
+ if (includeZero) {
4400
+ if (min === Infinity) return;
4401
+ return max === 0 && min === 0 ? [0, 1] : [Math.min(0, min), Math.max(0, max)];
4402
+ }
4134
4403
  return min < max ? [min, max] : void 0;
4135
- }, [rescaleYOnVisibilityChange, dataSorted]);
4404
+ }, [
4405
+ rescaleYOnVisibilityChange,
4406
+ dataSorted,
4407
+ isSeriesVisible,
4408
+ options?.yScale
4409
+ ]);
4136
4410
  const activateSelectedPoint = useCallback((index) => {
4137
4411
  const series = dataSorted[0];
4138
4412
  const datum = series?.data[index];
@@ -4152,7 +4426,24 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4152
4426
  onActivate: activateSelectedPoint,
4153
4427
  preventTooltipScroll: tooltipPlacement === "below-axis"
4154
4428
  });
4429
+ const visibleReadingExtent = useMemo(() => {
4430
+ let min = Infinity;
4431
+ let max = -Infinity;
4432
+ for (const series of dataSorted) {
4433
+ if (!isSeriesVisible(series.label)) continue;
4434
+ for (const point of series.data) {
4435
+ const value = point?.value;
4436
+ if (isReading(value)) {
4437
+ min = Math.min(min, value);
4438
+ max = Math.max(max, value);
4439
+ }
4440
+ }
4441
+ }
4442
+ return min <= max ? [min, max] : void 0;
4443
+ }, [dataSorted, isSeriesVisible]);
4444
+ const hasWholeNumberValues = useMemo(() => hasOnlyWholeNumbers(dataSorted.filter((series) => isSeriesVisible(series.label))), [dataSorted, isSeriesVisible]);
4155
4445
  const chartOptions = useMemo(() => {
4446
+ const fallbackYDomain = getFallbackYDomain(visibleReadingExtent, options?.yScale?.type === "log");
4156
4447
  return {
4157
4448
  axis: {
4158
4449
  x: buildTimeAxisOptions({
@@ -4181,7 +4472,8 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4181
4472
  type: "linear",
4182
4473
  nice: true,
4183
4474
  zero: false,
4184
- ...stableYDomain ? { domain: stableYDomain } : {},
4475
+ ...fallbackYDomain ? { domain: fallbackYDomain } : {},
4476
+ ...yDomain ? { domain: yDomain } : {},
4185
4477
  ...options?.yScale
4186
4478
  }
4187
4479
  };
@@ -4190,7 +4482,8 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4190
4482
  dataSorted,
4191
4483
  width,
4192
4484
  zoom.domain,
4193
- stableYDomain,
4485
+ yDomain,
4486
+ visibleReadingExtent,
4194
4487
  formatting,
4195
4488
  isSeriesVisible
4196
4489
  ]);
@@ -4225,11 +4518,13 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4225
4518
  withGlyph: withLegendGlyph,
4226
4519
  glyphSize: Math.max(0, toNumber(glyphStyle?.radius) ?? 4),
4227
4520
  collapseGroups: legendCollapseGroups,
4521
+ comparisonItem: legendComparisonItem,
4228
4522
  renderGlyph
4229
4523
  }), [
4230
4524
  withLegendGlyph,
4231
4525
  glyphStyle?.radius,
4232
4526
  legendCollapseGroups,
4527
+ legendComparisonItem,
4233
4528
  renderGlyph
4234
4529
  ]), legendShape);
4235
4530
  const chartMetadata = useMemo(() => ({
@@ -4341,18 +4636,26 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4341
4636
  },
4342
4637
  xScale: chartOptions.xScale,
4343
4638
  yScale: chartOptions.yScale,
4344
- onPointerDown: zoom.handlers.onPointerDown,
4345
- onPointerUp: zoom.handlers.onPointerUp,
4346
- onPointerMove: zoom.handlers.onPointerMove,
4347
4639
  onPointerOut,
4348
- pointerEventsDataKey: "nearest",
4349
4640
  children: [
4350
- !allSeriesHidden && gridVisibility !== "none" && /* @__PURE__ */ jsx(Grid, {
4351
- columns: false,
4352
- numTicks: 4
4641
+ /* @__PURE__ */ jsx(NearestPointerEvents, { ...zoom.handlers }),
4642
+ /* @__PURE__ */ jsx(WholeNumberTicks, {
4643
+ axis: "y",
4644
+ numTicks: chartOptions.axis.y.numTicks,
4645
+ enabled: hasWholeNumberValues && !chartOptions.axis.y.tickValues && !options?.yScale?.domain,
4646
+ children: (tickValues) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
4647
+ !allSeriesHidden && gridVisibility !== "none" && /* @__PURE__ */ jsx(Grid, {
4648
+ columns: false,
4649
+ numTicks: chartOptions.axis.y.numTicks,
4650
+ tickValues: tickValues ?? chartOptions.axis.y.tickValues
4651
+ }),
4652
+ !allSeriesHidden && chartOptions.axis.x.display && /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.x }),
4653
+ !allSeriesHidden && chartOptions.axis.y.display && /* @__PURE__ */ jsx(Axis, {
4654
+ ...chartOptions.axis.y,
4655
+ ...tickValues ? { tickValues } : {}
4656
+ })
4657
+ ] })
4353
4658
  }),
4354
- !allSeriesHidden && chartOptions.axis.x.display && /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.x }),
4355
- !allSeriesHidden && chartOptions.axis.y.display && /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.y }),
4356
4659
  allSeriesHidden ? /* @__PURE__ */ jsx(SvgEmptyState, {
4357
4660
  x: width / 2,
4358
4661
  y: chartHeight / 2,
@@ -4493,7 +4796,7 @@ var area_chart_module_default = {
4493
4796
  const validateData$3 = (data) => {
4494
4797
  if (!data?.length) return __("No data available", "jetpack-charts");
4495
4798
  if (data.some((series) => !series.data?.length)) return __("No data available", "jetpack-charts");
4496
- if (data.some((series) => series.data.some((point) => isNaN(point.value) || point.value === null || point.value === void 0 || "date" in point && point.date && isNaN(point.date.getTime())))) return __("Invalid data", "jetpack-charts");
4799
+ if (data.some((series) => series.data.some((point) => isInvalidReading(point.value, { allowMissing: false }) || "date" in point && point.date && isNaN(point.date.getTime())))) return __("Invalid data", "jetpack-charts");
4497
4800
  return null;
4498
4801
  };
4499
4802
  //#endregion
@@ -4609,6 +4912,12 @@ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4609
4912
  }));
4610
4913
  }, [dataSorted, hiddenSeries]);
4611
4914
  const allSeriesHidden = useMemo(() => seriesWithVisibility.every(({ isVisible }) => !isVisible), [seriesWithVisibility]);
4915
+ const hasWholeNumberValues = useMemo(() => (!stacked || stackOffset === "none") && hasOnlyWholeNumbers(dataSorted.filter((series) => isSeriesVisible(series.label))), [
4916
+ dataSorted,
4917
+ isSeriesVisible,
4918
+ stacked,
4919
+ stackOffset
4920
+ ]);
4612
4921
  const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation({
4613
4922
  selectedIndex,
4614
4923
  setSelectedIndex,
@@ -4701,8 +5010,9 @@ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4701
5010
  const legendItems = useChartLegendItems(dataSorted, useMemo(() => ({
4702
5011
  withGlyph: false,
4703
5012
  glyphSize: 0,
4704
- collapseGroups: legend.collapseGroups ?? false
4705
- }), [legend.collapseGroups]), legendShape);
5013
+ collapseGroups: legend.collapseGroups ?? false,
5014
+ comparisonItem: legend.comparisonItem ?? false
5015
+ }), [legend.collapseGroups, legend.comparisonItem]), legendShape);
4706
5016
  const chartMetadata = useMemo(() => ({
4707
5017
  stacked,
4708
5018
  stackOffset,
@@ -4852,12 +5162,23 @@ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4852
5162
  onPointerOut,
4853
5163
  pointerEventsDataKey: "nearest",
4854
5164
  children: [
4855
- !allSeriesHidden && gridVisibility !== "none" && /* @__PURE__ */ jsx(Grid, {
4856
- columns: false,
4857
- numTicks: 4
5165
+ /* @__PURE__ */ jsx(WholeNumberTicks, {
5166
+ axis: "y",
5167
+ numTicks: chartOptions.axis.y.numTicks,
5168
+ enabled: hasWholeNumberValues && !chartOptions.axis.y.tickValues && !options?.yScale?.domain,
5169
+ children: (tickValues) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
5170
+ !allSeriesHidden && gridVisibility !== "none" && /* @__PURE__ */ jsx(Grid, {
5171
+ columns: false,
5172
+ numTicks: chartOptions.axis.y.numTicks,
5173
+ tickValues: tickValues ?? chartOptions.axis.y.tickValues
5174
+ }),
5175
+ !allSeriesHidden && chartOptions.axis.x.display && /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.x }),
5176
+ !allSeriesHidden && chartOptions.axis.y.display && /* @__PURE__ */ jsx(Axis, {
5177
+ ...chartOptions.axis.y,
5178
+ ...tickValues ? { tickValues } : {}
5179
+ })
5180
+ ] })
4858
5181
  }),
4859
- !allSeriesHidden && chartOptions.axis.x.display && /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.x }),
4860
- !allSeriesHidden && chartOptions.axis.y.display && /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.y }),
4861
5182
  allSeriesHidden ? /* @__PURE__ */ jsx(SvgEmptyState, {
4862
5183
  x: width / 2,
4863
5184
  y: chartHeight / 2,
@@ -5046,6 +5367,65 @@ const createTruncatedTickComponent = (axis) => (props) => {
5046
5367
  const TruncatedXTickComponent = createTruncatedTickComponent("x");
5047
5368
  const TruncatedYTickComponent = createTruncatedTickComponent("y");
5048
5369
  //#endregion
5370
+ //#region src/charts/bar-chart/private/value-domain.ts
5371
+ /** Domain for a value axis that has nothing to scale, so its ticks still read 0 to 1. */
5372
+ const EMPTY_DOMAIN = [0, 1];
5373
+ /**
5374
+ * The value a bar is drawn at, preferring `visualValue` so a zero still shows a sliver.
5375
+ *
5376
+ * A missing reading is NaN, not null: `ComparisonBars` coerces the value with `Number()`,
5377
+ * which turns null into 0 and would draw a comparison shadow at the baseline.
5378
+ *
5379
+ * @param point - A data point handed to visx.
5380
+ * @return The bar value, or NaN when the bucket has no reading.
5381
+ */
5382
+ const getBarValue = (point) => {
5383
+ const enhanced = point;
5384
+ return (enhanced?.visualValue !== void 0 ? enhanced.visualValue : point?.value) ?? NaN;
5385
+ };
5386
+ /**
5387
+ * Whether visx renders a bar for a point.
5388
+ *
5389
+ * @param point - A data point handed to visx.
5390
+ * @return True when the point has a finite bar value.
5391
+ */
5392
+ const isBarRendered = (point) => Number.isFinite(getBarValue(point));
5393
+ /**
5394
+ * How many bars visx renders for a list of points.
5395
+ *
5396
+ * @param points - Data points handed to visx.
5397
+ * @return The number of points that get a bar.
5398
+ */
5399
+ const countRenderedBars = (points) => points.filter(isBarRendered).length;
5400
+ /**
5401
+ * An explicit domain for the value scale, or null to let visx fit one to the data.
5402
+ *
5403
+ * Zero goes into the domain here rather than through the scale's `zero` flag: visx applies
5404
+ * `nice` before `zero`, which would leave the axis topping out at a raw maximum.
5405
+ *
5406
+ * @param data - Every series handed to the chart.
5407
+ * @param includeZero - Whether the domain must span zero.
5408
+ * @param isSeriesRendered - Whether visx mounts a series, i.e. the legend shows it.
5409
+ * @return Value domain, or null when visx should fit its own.
5410
+ */
5411
+ const getValueScaleDomain = (data, includeZero, isSeriesRendered) => {
5412
+ let min = Infinity;
5413
+ let max = -Infinity;
5414
+ for (const series of data) {
5415
+ if (!isSeriesRendered(series)) continue;
5416
+ for (const point of series.data) {
5417
+ const value = getBarValue(point);
5418
+ if (!Number.isFinite(value)) continue;
5419
+ min = Math.min(min, value);
5420
+ max = Math.max(max, value);
5421
+ }
5422
+ }
5423
+ if (min === Infinity) return [...EMPTY_DOMAIN];
5424
+ if (min === max) return min === 0 ? [...EMPTY_DOMAIN] : [Math.min(0, min), Math.max(0, max)];
5425
+ if (includeZero) return [Math.min(0, min), Math.max(0, max)];
5426
+ return null;
5427
+ };
5428
+ //#endregion
5049
5429
  //#region src/charts/bar-chart/private/use-bar-chart-options.ts
5050
5430
  /** Outer padding of the category band scale (space at the chart edges). */
5051
5431
  const BASE_BAND_PADDING = .2;
@@ -5170,10 +5550,6 @@ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = A
5170
5550
  const tooltipDatumFormatter = hasLabels ? labelFormatter : byBucket(getTooltipFormatter(data, tickResolution, formatting));
5171
5551
  const valueFormatter = formatNumberCompact;
5172
5552
  const bandDomain = timeTickFormatter ? getBandDomain(data, isSeriesRendered) : null;
5173
- const valueAccessor = (d) => {
5174
- const enhancedPoint = d;
5175
- return enhancedPoint?.visualValue !== void 0 ? enhancedPoint.visualValue : d?.value;
5176
- };
5177
5553
  return {
5178
5554
  timeAxis: bandDomain && timeTickFormatter && {
5179
5555
  domain: bandDomain,
@@ -5184,7 +5560,7 @@ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = A
5184
5560
  yTickFormat: valueFormatter,
5185
5561
  tooltipLabelFormatter: tooltipDatumFormatter,
5186
5562
  xAccessor: bucketAccessor,
5187
- yAccessor: valueAccessor,
5563
+ yAccessor: getBarValue,
5188
5564
  gridVisibility: "x",
5189
5565
  xScale: bandScale,
5190
5566
  yScale: linearScale
@@ -5193,7 +5569,7 @@ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = A
5193
5569
  xTickFormat: valueFormatter,
5194
5570
  yTickFormat: labelFormatter,
5195
5571
  tooltipLabelFormatter: tooltipDatumFormatter,
5196
- xAccessor: valueAccessor,
5572
+ xAccessor: getBarValue,
5197
5573
  yAccessor: bucketAccessor,
5198
5574
  gridVisibility: "y",
5199
5575
  xScale: linearScale,
@@ -5208,20 +5584,10 @@ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = A
5208
5584
  ]);
5209
5585
  return useMemo(() => {
5210
5586
  const { xTickFormat: defaultXTickFormat, yTickFormat: defaultYTickFormat, tooltipLabelFormatter: defaultTooltipLabelFormatter, xAccessor, yAccessor, gridVisibility, xScale: baseXScale, yScale: baseYScale } = defaultOptions[horizontal ? "horizontal" : "vertical"];
5211
- let valueScaleDomainOverride = {};
5212
- if (data.some((s) => s.options?.type === "comparison")) {
5213
- if (!(!horizontal ? stableOptions.yScale?.domain : stableOptions.xScale?.domain)) {
5214
- const allValues = [];
5215
- data.forEach((series) => {
5216
- series.data.forEach((d) => {
5217
- const enhanced = d;
5218
- const v = enhanced.visualValue !== void 0 ? enhanced.visualValue : d.value;
5219
- if (typeof v === "number" && Number.isFinite(v)) allValues.push(v);
5220
- });
5221
- });
5222
- if (allValues.length > 0) valueScaleDomainOverride = { domain: [Math.min(0, ...allValues), Math.max(0, ...allValues)] };
5223
- }
5224
- }
5587
+ const valueScaleOptions = horizontal ? stableOptions.xScale : stableOptions.yScale;
5588
+ const includeZero = data.some((s) => s.options?.type === "comparison") || valueScaleOptions?.zero !== false;
5589
+ const domain = valueScaleOptions?.domain ? null : getValueScaleDomain(data, includeZero, isSeriesRendered);
5590
+ const valueScaleDomainOverride = domain ? { domain } : {};
5225
5591
  const xScale = {
5226
5592
  ...baseXScale,
5227
5593
  ...stableOptions.xScale || {},
@@ -5272,10 +5638,25 @@ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = A
5272
5638
  axisConfig,
5273
5639
  stableOptions,
5274
5640
  horizontal,
5275
- data
5641
+ data,
5642
+ isSeriesRendered
5276
5643
  ]);
5277
5644
  }
5278
5645
  //#endregion
5646
+ //#region src/charts/bar-chart/private/band-scale.ts
5647
+ /**
5648
+ * Match the inner slot scale used by visx BarGroup.
5649
+ * @param keys - Visible primary series keys.
5650
+ * @param bandwidth - Category band width.
5651
+ * @param padding - Padding between slots.
5652
+ * @return The grouped bar scale.
5653
+ */
5654
+ const createGroupScale = (keys, bandwidth, padding) => scaleBand({
5655
+ domain: keys,
5656
+ range: [0, bandwidth],
5657
+ padding
5658
+ });
5659
+ //#endregion
5279
5660
  //#region src/charts/bar-chart/private/comparison-bars-geometry.ts
5280
5661
  /**
5281
5662
  * Output position of a value scale's baseline: zero if in-domain, else the
@@ -5348,11 +5729,7 @@ const ComparisonBars = ({ comparisonEntries, primaryKeys, groupPadding, horizont
5348
5729
  const valueScale = horizontal ? xScale : yScale;
5349
5730
  const bandwidth = bandScale.bandwidth ? bandScale.bandwidth() : 0;
5350
5731
  if (!bandwidth) return null;
5351
- const groupScale = scaleBand({
5352
- domain: primaryKeys,
5353
- range: [0, bandwidth],
5354
- padding: groupPadding
5355
- });
5732
+ const groupScale = createGroupScale(primaryKeys, bandwidth, groupPadding);
5356
5733
  const slotThickness = groupScale.bandwidth();
5357
5734
  const baseline = getValueScaleBaseline(valueScale);
5358
5735
  const bandAccessor = horizontal ? yAccessor : xAccessor;
@@ -5404,10 +5781,169 @@ const ComparisonBars = ({ comparisonEntries, primaryKeys, groupPadding, horizont
5404
5781
  });
5405
5782
  };
5406
5783
  //#endregion
5784
+ //#region src/charts/bar-chart/private/band-highlight.tsx
5785
+ /**
5786
+ * Render and report the active band using the chart’s registered scales.
5787
+ * @param root0 - Highlight options.
5788
+ * @param root0.visible - Whether to paint the band.
5789
+ * @param root0.horizontal - Whether categories run vertically.
5790
+ * @param root0.onChange - Receives selected band geometry.
5791
+ * @return The optional band highlight.
5792
+ */
5793
+ function BandHighlight({ visible, horizontal, onChange }) {
5794
+ const { xScale, yScale, dataRegistry, margin, innerWidth, innerHeight } = useContext(DataContext);
5795
+ const { tooltipOpen, tooltipData } = useContext(TooltipContext);
5796
+ const nearest = tooltipData?.nearestDatum;
5797
+ const selection = useMemo(() => {
5798
+ if (!tooltipOpen || !nearest) return null;
5799
+ const scale = horizontal ? yScale : xScale;
5800
+ const entry = dataRegistry?.get(nearest.key);
5801
+ const accessor = horizontal ? entry?.yAccessor : entry?.xAccessor;
5802
+ if (!scale?.bandwidth || !accessor) return null;
5803
+ const position = scale(accessor(nearest.datum));
5804
+ if (position === void 0 || !Number.isFinite(position)) return null;
5805
+ return {
5806
+ datum: nearest.datum,
5807
+ key: nearest.key,
5808
+ index: nearest.index,
5809
+ x: horizontal ? margin?.left ?? 0 : position,
5810
+ y: horizontal ? position : margin?.top ?? 0,
5811
+ width: horizontal ? innerWidth : scale.bandwidth(),
5812
+ height: horizontal ? scale.bandwidth() : innerHeight
5813
+ };
5814
+ }, [
5815
+ tooltipOpen,
5816
+ nearest,
5817
+ horizontal,
5818
+ xScale,
5819
+ yScale,
5820
+ dataRegistry,
5821
+ margin,
5822
+ innerWidth,
5823
+ innerHeight
5824
+ ]);
5825
+ const stableSelection = useDeepMemo(selection);
5826
+ const onChangeRef = useRef(onChange);
5827
+ onChangeRef.current = onChange;
5828
+ useEffect(() => {
5829
+ onChangeRef.current?.(stableSelection);
5830
+ }, [stableSelection]);
5831
+ useEffect(() => () => onChangeRef.current?.(null), []);
5832
+ if (!visible || !selection) return null;
5833
+ const { x, y, width, height } = selection;
5834
+ return /* @__PURE__ */ jsx("rect", {
5835
+ x,
5836
+ y,
5837
+ width,
5838
+ height,
5839
+ pointerEvents: "none",
5840
+ fill: CATALOG_POINTERS.surfaceSecondary
5841
+ });
5842
+ }
5843
+ //#endregion
5844
+ //#region src/charts/bar-chart/private/band-tooltip.tsx
5845
+ /**
5846
+ * Find a category by its painted center, including outer padding and reversed ranges.
5847
+ * @param data - Registered series points.
5848
+ * @param accessor - Category accessor.
5849
+ * @param scale - Rendered category band scale.
5850
+ * @param position - Pointer coordinate on the category axis.
5851
+ * @return Index of the nearest category, or -1 when none is registered.
5852
+ */
5853
+ function nearestBandIndex(data, accessor, scale, position) {
5854
+ let nearest = -1;
5855
+ let distance = Infinity;
5856
+ data.forEach((datum, index) => {
5857
+ const start = scale(accessor(datum));
5858
+ if (start === void 0) return;
5859
+ const candidate = Math.abs(start + scale.bandwidth() / 2 - position);
5860
+ if (candidate < distance) {
5861
+ distance = candidate;
5862
+ nearest = index;
5863
+ }
5864
+ });
5865
+ return nearest;
5866
+ }
5867
+ /**
5868
+ * Correct visx's range-based band inversion, which omits outer padding.
5869
+ * @param root0 - Registered series selection.
5870
+ * @param root0.keys - Visible series keys.
5871
+ * @param root0.groupPadding - Padding between grouped bars.
5872
+ * @param root0.withTooltips - Whether pointer events update the tooltip.
5873
+ * @param root0.onPointerDown - Receives the corrected pointer-down datum.
5874
+ * @param root0.onPointerUp - Receives the corrected pointer-up datum.
5875
+ * @return No visual content.
5876
+ */
5877
+ function BandTooltip({ keys, groupPadding, withTooltips, onPointerDown, onPointerUp }) {
5878
+ const { xScale, yScale, dataRegistry, horizontal } = useContext(DataContext);
5879
+ const { showTooltip } = useContext(TooltipContext);
5880
+ const scale = horizontal ? yScale : xScale;
5881
+ const bandwidth = scale?.bandwidth?.() ?? 0;
5882
+ const groupScale = useMemo(() => createGroupScale(keys, bandwidth, groupPadding), [
5883
+ keys,
5884
+ bandwidth,
5885
+ groupPadding
5886
+ ]);
5887
+ const getSelections = useCallback((params) => {
5888
+ const point = params?.svgPoint;
5889
+ if (!point || !scale?.bandwidth) return [];
5890
+ const selections = [];
5891
+ for (const key of keys) {
5892
+ const entry = dataRegistry?.get(key);
5893
+ if (!entry) continue;
5894
+ const accessor = horizontal ? entry.yAccessor : entry.xAccessor;
5895
+ const index = nearestBandIndex(entry.data, accessor, scale, horizontal ? point.y : point.x);
5896
+ if (index < 0) continue;
5897
+ const datum = entry.data[index];
5898
+ const start = Number(scale(accessor(datum))) + Number(groupScale(key));
5899
+ const end = start + groupScale.step();
5900
+ const position = horizontal ? point.y : point.x;
5901
+ const distance = position >= start && position <= end ? 0 : Math.abs(position - (start + end) / 2);
5902
+ selections.push({
5903
+ event: params.event,
5904
+ key,
5905
+ datum,
5906
+ index,
5907
+ svgPoint: point,
5908
+ distanceX: horizontal ? 0 : distance,
5909
+ distanceY: horizontal ? distance : 0
5910
+ });
5911
+ }
5912
+ return selections;
5913
+ }, [
5914
+ scale,
5915
+ dataRegistry,
5916
+ horizontal,
5917
+ keys,
5918
+ groupScale
5919
+ ]);
5920
+ const handlePointer = useCallback((params) => {
5921
+ const selections = getSelections(params);
5922
+ if (withTooltips && params?.event.type !== "pointerup") selections.forEach(showTooltip);
5923
+ const callback = params?.event.type === "pointerdown" ? onPointerDown : onPointerUp;
5924
+ if (params?.event.type === "pointermove" || !callback) return;
5925
+ const nearest = selections.reduce((best, selection) => {
5926
+ const distance = (value) => Math.hypot(value.distanceX, value.distanceY);
5927
+ return !best || distance(selection) <= distance(best) ? selection : best;
5928
+ }, void 0);
5929
+ if (nearest) callback(nearest);
5930
+ }, [
5931
+ getSelections,
5932
+ withTooltips,
5933
+ showTooltip,
5934
+ onPointerDown,
5935
+ onPointerUp
5936
+ ]);
5937
+ useEventEmitter("pointermove", withTooltips ? handlePointer : void 0);
5938
+ useEventEmitter("pointerdown", handlePointer);
5939
+ useEventEmitter("pointerup", onPointerUp ? handlePointer : void 0);
5940
+ return null;
5941
+ }
5942
+ //#endregion
5407
5943
  //#region src/charts/bar-chart/bar-chart.tsx
5408
5944
  const validateData$2 = (data) => {
5409
- if (!data?.length) return "No data available";
5410
- if (data.some((series) => series.data.some((point) => isNaN(point.value) || point.value === null || point.value === void 0 || !point.label && (!("date" in point && point.date) || isNaN(point.date.getTime()))))) return "Invalid data";
5945
+ if (!data?.length) return __("No data available", "jetpack-charts");
5946
+ if (data.some((series) => series.data.some((point) => isInvalidReading(point.value, { allowMissing: true }) || !point.label && (!("date" in point && point.date) || isNaN(point.date.getTime()))))) return __("Invalid data", "jetpack-charts");
5411
5947
  return null;
5412
5948
  };
5413
5949
  const getPatternId = (chartId, index) => `bar-pattern-${chartId}-${index}`;
@@ -5415,9 +5951,11 @@ const renderTooltipRow = (label, value) => /* @__PURE__ */ jsx("div", {
5415
5951
  className: bar_chart_module_default["bar-chart__tooltip-row"],
5416
5952
  children: sprintf(__("%1$s: %2$s", "jetpack-charts"), label, value)
5417
5953
  });
5418
- const BarChartInternal = ({ data, chartId: providedChartId, width, height, className, margin, withTooltips = false, showLegend = false, legend = {}, gridVisibility: gridVisibilityProp, renderTooltip, options = {}, orientation = "vertical", withPatterns = false, showZeroValues = false, defaultHiddenSeries, animation, children, gap = "md", onPointerDown, onPointerUp, onDatumActivate }) => {
5954
+ const BarChartInternal = ({ data, chartId: providedChartId, width, height, className, margin, withTooltips = false, showLegend = false, legend = {}, gridVisibility: gridVisibilityProp, renderTooltip, tooltipPlacement, tooltipAnchorTop, options = {}, orientation = "vertical", withPatterns = false, showZeroValues = false, withBandHighlight = false, onBandHighlightChange, defaultHiddenSeries, animation, children, gap = "md", onPointerDown, onPointerUp, onDatumActivate }) => {
5955
+ if (!withTooltips && (withBandHighlight || onBandHighlightChange)) warnOnce("bar-chart-band-highlight-without-tooltips", "BarChart: withBandHighlight and onBandHighlightChange require withTooltips.");
5419
5956
  const legendInteractive = legend.interactive ?? false;
5420
5957
  const legendCollapseGroups = legend.collapseGroups ?? false;
5958
+ const legendComparisonItem = legend.comparisonItem ?? false;
5421
5959
  const horizontal = orientation === "horizontal";
5422
5960
  const chartId = useChartId(providedChartId);
5423
5961
  const hiddenSeries = useDefaultHiddenSeries(chartId, defaultHiddenSeries);
@@ -5428,10 +5966,21 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5428
5966
  enabled: showZeroValues,
5429
5967
  valueAxisLength: horizontal ? width : height
5430
5968
  });
5431
- const legendItems = useChartLegendItems(dataSorted, useMemo(() => ({ collapseGroups: legendCollapseGroups }), [legendCollapseGroups]));
5969
+ const legendItems = useChartLegendItems(dataSorted, useMemo(() => ({
5970
+ collapseGroups: legendCollapseGroups,
5971
+ comparisonItem: legendComparisonItem
5972
+ }), [legendCollapseGroups, legendComparisonItem]));
5432
5973
  const { getElementStyles } = useGlobalChartsContext();
5433
5974
  const isSeriesRendered = useCallback((series) => isSeriesVisible(series.label), [isSeriesVisible]);
5975
+ const hasWholeNumberValues = useMemo(() => hasOnlyWholeNumbers(dataSorted.filter(isSeriesRendered)), [dataSorted, isSeriesRendered]);
5434
5976
  const chartOptions = useBarChartOptions(dataWithVisibleZeros, horizontal, options, isSeriesRendered);
5977
+ const valueAxis = horizontal ? chartOptions.axis.x : chartOptions.axis.y;
5978
+ const callerValueDomain = horizontal ? options.xScale?.domain : options.yScale?.domain;
5979
+ const wholeNumberTicksProps = {
5980
+ axis: horizontal ? "x" : "y",
5981
+ numTicks: valueAxis.numTicks,
5982
+ enabled: hasWholeNumberValues && !valueAxis.tickValues && !callerValueDomain
5983
+ };
5435
5984
  const defaultMargin = useChartMargin(height, chartOptions, dataSorted, theme, horizontal);
5436
5985
  const chartRef = useRef(null);
5437
5986
  const { legendChildren, nonLegendChildren } = useChartChildren(children, "BarChart");
@@ -5441,8 +5990,6 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5441
5990
  }, [height]);
5442
5991
  const [selectedIndex, setSelectedIndex] = useState(void 0);
5443
5992
  const [isNavigating, setIsNavigating] = useState(false);
5444
- const primarySeriesForNav = dataWithVisibleZeros.filter((s) => s.options?.type !== "comparison");
5445
- const totalPoints = Math.max(0, ...primarySeriesForNav.map((s) => s.data?.length || 0)) * primarySeriesForNav.length;
5446
5993
  const seriesWithVisibility = useMemo(() => dataWithVisibleZeros.map((series, index) => ({
5447
5994
  series,
5448
5995
  index,
@@ -5452,6 +5999,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5452
5999
  return seriesWithVisibility.every(({ isVisible }) => !isVisible);
5453
6000
  }, [seriesWithVisibility]);
5454
6001
  const primaryEntries = useMemo(() => seriesWithVisibility.filter(({ isVisible, series }) => isVisible && series.options?.type !== "comparison"), [seriesWithVisibility]);
6002
+ const totalPoints = Math.max(0, ...primaryEntries.map((e) => e.series.data.length)) * primaryEntries.length;
5455
6003
  const primaryKeys = useMemo(() => primaryEntries.map(({ series }) => series.label), [primaryEntries]);
5456
6004
  const primarySeries = useMemo(() => primaryEntries.map(({ series }) => series), [primaryEntries]);
5457
6005
  const activateSelectedBar = useCallback((index) => {
@@ -5466,6 +6014,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5466
6014
  key: series.label
5467
6015
  });
5468
6016
  }, [primaryEntries, onDatumActivate]);
6017
+ const visibleSeriesKey = useMemo(() => JSON.stringify(primaryKeys), [primaryKeys]);
5469
6018
  const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation({
5470
6019
  selectedIndex,
5471
6020
  setSelectedIndex,
@@ -5473,7 +6022,8 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5473
6022
  setIsNavigating,
5474
6023
  chartRef,
5475
6024
  totalPoints,
5476
- onActivate: activateSelectedBar
6025
+ onActivate: activateSelectedBar,
6026
+ visibleSeriesKey
5477
6027
  });
5478
6028
  const comparisonEntries = useMemo(() => {
5479
6029
  const primaryByGroup = new Map(primaryEntries.map(({ series, index }) => [series.group, {
@@ -5536,7 +6086,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5536
6086
  chartOptions.yScale,
5537
6087
  horizontal
5538
6088
  ]);
5539
- const getBarBackground = useCallback((index) => () => withPatterns ? `url(#${getPatternId(chartId, index)})` : getElementStyles({
6089
+ const getBarBackground = useCallback((index) => (datum) => withPatterns ? `url(#${getPatternId(chartId, index)})` : datum.color ?? getElementStyles({
5540
6090
  data: dataSorted[index],
5541
6091
  index
5542
6092
  }).color, [
@@ -5563,15 +6113,15 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5563
6113
  const p = point;
5564
6114
  return nearestDatum.label != null ? p.label === nearestDatum.label : !!nearestDatum.date && !!p.date && p.date.getTime() === nearestDatum.date.getTime();
5565
6115
  });
5566
- if (comparisonEntry && comparisonDatum && comparisonDatum.value != null) return /* @__PURE__ */ jsxs("div", {
6116
+ if (comparisonEntry && comparisonDatum) return /* @__PURE__ */ jsxs("div", {
5567
6117
  className: bar_chart_module_default["bar-chart__tooltip"],
5568
6118
  children: [
5569
6119
  /* @__PURE__ */ jsx("div", {
5570
6120
  className: bar_chart_module_default["bar-chart__tooltip-header"],
5571
6121
  children: categoryLabel
5572
6122
  }),
5573
- renderTooltipRow(primaryKey, formatNumber(nearestDatum.value)),
5574
- renderTooltipRow(comparisonEntry.series.label, formatNumber(comparisonDatum.value))
6123
+ renderTooltipRow(primaryKey, formatReading(nearestDatum.value)),
6124
+ renderTooltipRow(comparisonEntry.series.label, formatReading(comparisonDatum.value))
5575
6125
  ]
5576
6126
  });
5577
6127
  return /* @__PURE__ */ jsxs("div", {
@@ -5579,7 +6129,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5579
6129
  children: [/* @__PURE__ */ jsx("div", {
5580
6130
  className: bar_chart_module_default["bar-chart__tooltip-header"],
5581
6131
  children: primaryKey
5582
- }), renderTooltipRow(categoryLabel, formatNumber(nearestDatum.value))]
6132
+ }), renderTooltipRow(categoryLabel, formatReading(nearestDatum.value))]
5583
6133
  });
5584
6134
  }, [chartOptions.tooltip, comparisonEntries]);
5585
6135
  const renderPattern = useCallback((index, color) => {
@@ -5630,15 +6180,15 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5630
6180
  const createKeyboardHighlightStyle = useCallback(() => {
5631
6181
  if (selectedIndex === void 0) return "";
5632
6182
  const primaryCount = primaryEntries.length;
5633
- const maxDataPoints = Math.max(...primaryEntries.map((e) => e.series.data.length));
6183
+ if (!primaryCount) return "";
5634
6184
  const dataPointIndex = Math.floor(selectedIndex / primaryCount);
5635
6185
  const seriesIndex = selectedIndex % primaryCount;
5636
- if (dataPointIndex >= maxDataPoints || seriesIndex >= primaryCount) return "";
5637
6186
  const seriesData = primaryEntries[seriesIndex]?.series;
5638
- if (!seriesData || dataPointIndex >= seriesData.data.length) return "";
5639
- const actualBarIndex = seriesIndex * maxDataPoints + dataPointIndex;
6187
+ const datum = seriesData?.data[dataPointIndex];
6188
+ if (!seriesData || !datum || !isBarRendered(datum)) return "";
6189
+ const domBarIndex = primaryEntries.slice(0, seriesIndex).reduce((count, { series }) => count + countRenderedBars(series.data), 0) + countRenderedBars(seriesData.data.slice(0, dataPointIndex));
5640
6190
  return `
5641
- .bar-chart[data-chart-id="bar-chart-${chartId}"] .visx-bar-group .visx-bar:nth-child(${actualBarIndex + 1}) {
6191
+ .bar-chart[data-chart-id="bar-chart-${chartId}"] .visx-bar-group .visx-bar:nth-child(${domBarIndex + 1}) {
5642
6192
  stroke: #005fcc;
5643
6193
  stroke-width: 2px;
5644
6194
  }
@@ -5706,13 +6256,13 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5706
6256
  const chartHeight = contentHeight > 0 ? contentHeight : height;
5707
6257
  return /* @__PURE__ */ jsx("div", {
5708
6258
  role: "grid",
6259
+ ref: chartRef,
5709
6260
  "aria-label": __("Bar chart", "jetpack-charts"),
5710
6261
  tabIndex: 0,
5711
6262
  onKeyDown: onChartKeyDown,
5712
6263
  onFocus: onChartFocus,
5713
6264
  onBlur: onChartBlur,
5714
6265
  children: chartHeight > 0 && /* @__PURE__ */ jsx("div", {
5715
- ref: chartRef,
5716
6266
  className: xy_plot_module_default["xy-plot"],
5717
6267
  children: /* @__PURE__ */ jsxs(XYChart, {
5718
6268
  theme,
@@ -5725,14 +6275,26 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5725
6275
  xScale,
5726
6276
  yScale,
5727
6277
  horizontal,
5728
- onPointerDown,
5729
- onPointerUp,
5730
6278
  pointerEventsDataKey: "nearest",
5731
6279
  children: [
5732
- !allSeriesHidden && /* @__PURE__ */ jsx(Grid, {
5733
- columns: gridVisibility.includes("y"),
5734
- rows: gridVisibility.includes("x"),
5735
- numTicks: 4
6280
+ withTooltips && (withBandHighlight || onBandHighlightChange) && /* @__PURE__ */ jsx(BandHighlight, {
6281
+ visible: withBandHighlight,
6282
+ horizontal,
6283
+ onChange: onBandHighlightChange
6284
+ }),
6285
+ !allSeriesHidden && /* @__PURE__ */ jsx(WholeNumberTicks, {
6286
+ ...wholeNumberTicksProps,
6287
+ children: (valueTicks) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Grid, {
6288
+ columns: gridVisibility.includes("y"),
6289
+ rows: false,
6290
+ numTicks: chartOptions.axis.x.numTicks,
6291
+ tickValues: (horizontal ? valueTicks : void 0) ?? chartOptions.axis.x.tickValues
6292
+ }), /* @__PURE__ */ jsx(Grid, {
6293
+ columns: false,
6294
+ rows: gridVisibility.includes("x"),
6295
+ numTicks: chartOptions.axis.y.numTicks,
6296
+ tickValues: (horizontal ? void 0 : valueTicks) ?? chartOptions.axis.y.tickValues
6297
+ })] })
5736
6298
  }),
5737
6299
  withPatterns && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("defs", { children: dataSorted.map((seriesData, index) => renderPattern(index, getElementStyles({
5738
6300
  data: seriesData,
@@ -5769,8 +6331,26 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5769
6331
  colorAccessor: getBarBackground(index)
5770
6332
  }, seriesData?.label))
5771
6333
  }),
5772
- !allSeriesHidden && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.x }), /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.y })] }),
6334
+ (withTooltips || onPointerDown || onPointerUp) && /* @__PURE__ */ jsx(BandTooltip, {
6335
+ keys: primaryKeys,
6336
+ groupPadding,
6337
+ withTooltips,
6338
+ onPointerDown,
6339
+ onPointerUp
6340
+ }),
6341
+ !allSeriesHidden && /* @__PURE__ */ jsx(WholeNumberTicks, {
6342
+ ...wholeNumberTicksProps,
6343
+ children: (valueTicks) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Axis, {
6344
+ ...chartOptions.axis.x,
6345
+ ...horizontal && valueTicks ? { tickValues: valueTicks } : {}
6346
+ }), /* @__PURE__ */ jsx(Axis, {
6347
+ ...chartOptions.axis.y,
6348
+ ...!horizontal && valueTicks ? { tickValues: valueTicks } : {}
6349
+ })] })
6350
+ }),
5773
6351
  withTooltips && /* @__PURE__ */ jsx(AccessibleTooltip, {
6352
+ tooltipPlacement,
6353
+ tooltipAnchorTop,
5774
6354
  detectBounds: true,
5775
6355
  snapTooltipToDatumX: true,
5776
6356
  snapTooltipToDatumY: true,
@@ -6592,8 +7172,11 @@ var heatmap_chart_module_default = {
6592
7172
  "heatmap-chart__gap-start": "a8ccharts-O3YMOW-heatmap-chart__gap-start",
6593
7173
  "heatmap-chart__grid": "a8ccharts-O3YMOW-heatmap-chart__grid",
6594
7174
  "heatmap-chart__grid--compact": "a8ccharts-O3YMOW-heatmap-chart__grid--compact",
7175
+ "heatmap-chart__grid--flex-gaps": "a8ccharts-O3YMOW-heatmap-chart__grid--flex-gaps",
6595
7176
  "heatmap-chart__grid--height-capped": "a8ccharts-O3YMOW-heatmap-chart__grid--height-capped",
7177
+ "heatmap-chart__group-label": "a8ccharts-O3YMOW-heatmap-chart__group-label",
6596
7178
  "heatmap-chart__legend-label": "a8ccharts-O3YMOW-heatmap-chart__legend-label",
7179
+ "heatmap-chart__legend-scale--bar": "a8ccharts-O3YMOW-heatmap-chart__legend-scale--bar",
6597
7180
  "heatmap-chart__legend-swatch": "a8ccharts-O3YMOW-heatmap-chart__legend-swatch",
6598
7181
  "heatmap-chart__row": "a8ccharts-O3YMOW-heatmap-chart__row",
6599
7182
  "heatmap-chart__row-label": "a8ccharts-O3YMOW-heatmap-chart__row-label",
@@ -6643,7 +7226,7 @@ const getNormalizedValue = (value, extent) => {
6643
7226
  const HeatmapContext = createContext(null);
6644
7227
  //#endregion
6645
7228
  //#region src/charts/heatmap-chart/private/heatmap-legend.tsx
6646
- const HeatmapLegend = ({ steps = 5, lessLabel, moreLabel }) => {
7229
+ const HeatmapLegend = ({ steps = 5, variant = "swatches", lessLabel, moreLabel }) => {
6647
7230
  const context = useContext(HeatmapContext);
6648
7231
  const { legend } = useGlobalChartsTheme();
6649
7232
  if (!context) return null;
@@ -6662,11 +7245,12 @@ const HeatmapLegend = ({ steps = 5, lessLabel, moreLabel }) => {
6662
7245
  }),
6663
7246
  /* @__PURE__ */ jsx(Stack, {
6664
7247
  direction: "row",
6665
- gap: "xs",
7248
+ gap: variant === "bar" ? void 0 : "xs",
7249
+ "aria-hidden": "true",
7250
+ className: variant === "bar" ? heatmap_chart_module_default["heatmap-chart__legend-scale--bar"] : void 0,
6666
7251
  children: Array.from({ length: steps }, (_, index) => {
6667
7252
  const intensity = steps <= 1 ? 1 : index / (steps - 1);
6668
7253
  return /* @__PURE__ */ jsx("span", {
6669
- "aria-hidden": "true",
6670
7254
  className: heatmap_chart_module_default["heatmap-chart__legend-swatch"],
6671
7255
  style: {
6672
7256
  "--a8c-charts-color-heatmap-primary": primaryColorHex,
@@ -6685,16 +7269,219 @@ const HeatmapLegend = ({ steps = 5, lessLabel, moreLabel }) => {
6685
7269
  });
6686
7270
  };
6687
7271
  //#endregion
7272
+ //#region src/charts/heatmap-chart/private/column-groups.ts
7273
+ const isSpan = (span) => Number.isInteger(span) && span > 0;
7274
+ /**
7275
+ * Validates column groups and lays them over the columns.
7276
+ *
7277
+ * @param groups - Groups as passed to the chart.
7278
+ * @param columnCount - Number of data columns.
7279
+ * @param firstLine - Grid column line of the first data column.
7280
+ * @return Grid column lines for every column and group; no groups when the input was unusable.
7281
+ */
7282
+ const resolveColumnGroups = (groups, columnCount, firstLine) => {
7283
+ const ungrouped = () => ({
7284
+ columns: Array.from({ length: columnCount }, (_, index) => ({
7285
+ line: firstLine + index,
7286
+ gapBefore: false
7287
+ })),
7288
+ groups: []
7289
+ });
7290
+ if (!groups || groups.length === 0 || columnCount === 0) return ungrouped();
7291
+ const spans = groups.map((group) => group.span);
7292
+ const total = spans.reduce((sum, span) => sum + span, 0);
7293
+ if (!spans.every(isSpan) || total > columnCount) {
7294
+ warnOnce(`heatmap:columnGroups:${JSON.stringify(spans)}:${columnCount}`, `columnGroups spans ${JSON.stringify(spans)} must be positive integers that fit the ${columnCount} columns, so no groups are drawn.`);
7295
+ return ungrouped();
7296
+ }
7297
+ const columns = [];
7298
+ const laidOut = [];
7299
+ let line = firstLine;
7300
+ groups.forEach((group, groupIndex) => {
7301
+ if (groupIndex > 0) line += 1;
7302
+ laidOut.push({
7303
+ ...group,
7304
+ line
7305
+ });
7306
+ for (let offset = 0; offset < group.span; offset++) {
7307
+ columns.push({
7308
+ line,
7309
+ gapBefore: groupIndex > 0 && offset === 0,
7310
+ group: groupIndex
7311
+ });
7312
+ line += 1;
7313
+ }
7314
+ });
7315
+ while (columns.length < columnCount) {
7316
+ columns.push({
7317
+ line,
7318
+ gapBefore: false
7319
+ });
7320
+ line += 1;
7321
+ }
7322
+ return {
7323
+ columns,
7324
+ groups: laidOut
7325
+ };
7326
+ };
7327
+ //#endregion
7328
+ //#region src/charts/heatmap-chart/private/keyboard-navigation.ts
7329
+ const ARROW_KEYS = [
7330
+ "ArrowLeft",
7331
+ "ArrowRight",
7332
+ "ArrowUp",
7333
+ "ArrowDown"
7334
+ ];
7335
+ const PAGE_KEYS = ["PageUp", "PageDown"];
7336
+ const isNavigationKey = (key, mode) => ARROW_KEYS.includes(key) || mode === "calendar" && PAGE_KEYS.includes(key);
7337
+ const inGrid = (grid, { column, row }) => column >= 0 && column < grid.columns && row >= 0 && row < grid.rows;
7338
+ const firstSelectable = (grid, cells) => cells.find((cell) => inGrid(grid, cell) && !grid.isInert(cell.column, cell.row));
7339
+ /**
7340
+ * Column-major scan for the first selectable cell.
7341
+ *
7342
+ * @param grid - The grid.
7343
+ * @return The cell, or undefined when every slot is inert.
7344
+ */
7345
+ const firstGridCell = (grid) => {
7346
+ for (let column = 0; column < grid.columns; column++) for (let row = 0; row < grid.rows; row++) if (!grid.isInert(column, row)) return {
7347
+ column,
7348
+ row
7349
+ };
7350
+ };
7351
+ /**
7352
+ * Steps across the grid in the key's direction, past inert slots, staying put at the edge.
7353
+ *
7354
+ * @param grid - The grid.
7355
+ * @param from - The selected cell.
7356
+ * @param key - The arrow key.
7357
+ * @return The next cell, or undefined to keep the selection.
7358
+ */
7359
+ const stepGridCell = (grid, from, key) => {
7360
+ const step = {
7361
+ ArrowRight: [1, 0],
7362
+ ArrowLeft: [-1, 0],
7363
+ ArrowDown: [0, 1],
7364
+ ArrowUp: [0, -1]
7365
+ }[key];
7366
+ if (!step) return;
7367
+ const [stepColumn, stepRow] = step;
7368
+ let cell = from;
7369
+ do
7370
+ cell = {
7371
+ column: cell.column + stepColumn,
7372
+ row: cell.row + stepRow
7373
+ };
7374
+ while (inGrid(grid, cell) && grid.isInert(cell.column, cell.row));
7375
+ return inGrid(grid, cell) ? cell : void 0;
7376
+ };
7377
+ /**
7378
+ * Every slot of the blocks in reading order: block by block, each a row at a time.
7379
+ *
7380
+ * @param grid - The grid.
7381
+ * @param blocks - The blocks, left to right.
7382
+ * @return The slots in order.
7383
+ */
7384
+ const readingOrder = (grid, blocks) => blocks.flatMap((block) => Array.from({ length: grid.rows }, (_row, row) => Array.from({ length: block.end - block.start }, (_offset, offset) => ({
7385
+ column: block.start + offset,
7386
+ row
7387
+ }))).flat());
7388
+ const blockOf = (blocks, column) => blocks.findIndex((block) => column >= block.start && column < block.end);
7389
+ /**
7390
+ * The slots after a cell down its column (or before it, going up), continuing at the
7391
+ * same column offset of each following block.
7392
+ *
7393
+ * @param grid - The grid.
7394
+ * @param blocks - The blocks, left to right.
7395
+ * @param from - The selected cell.
7396
+ * @param direction - 1 downwards, -1 upwards.
7397
+ * @return The slots in order.
7398
+ */
7399
+ const columnOrderFrom = (grid, blocks, from, direction) => {
7400
+ const blockIndex = blockOf(blocks, from.column);
7401
+ if (blockIndex < 0) return [];
7402
+ const offset = from.column - blocks[blockIndex].start;
7403
+ const cells = [];
7404
+ const pushRows = (column, startRow) => {
7405
+ for (let row = startRow; row >= 0 && row < grid.rows; row += direction) cells.push({
7406
+ column,
7407
+ row
7408
+ });
7409
+ };
7410
+ pushRows(from.column, from.row + direction);
7411
+ for (let index = blockIndex + direction; index >= 0 && index < blocks.length; index += direction) {
7412
+ const column = blocks[index].start + offset;
7413
+ if (column < blocks[index].end) pushRows(column, direction === 1 ? 0 : grid.rows - 1);
7414
+ }
7415
+ return cells;
7416
+ };
7417
+ /**
7418
+ * The first selectable cell in calendar reading order.
7419
+ *
7420
+ * @param grid - The grid.
7421
+ * @param blocks - The blocks, left to right.
7422
+ * @return The cell, or undefined when every slot is inert.
7423
+ */
7424
+ const firstCalendarCell = (grid, blocks) => firstSelectable(grid, readingOrder(grid, blocks));
7425
+ /**
7426
+ * Steps through the blocks as pages of a calendar: Left/Right by one slot in reading
7427
+ * order, Up/Down by one row (into the neighboring block past the edge), Page Up/Down
7428
+ * to the same slot of the neighboring block or its nearest selectable cell.
7429
+ *
7430
+ * @param grid - The grid.
7431
+ * @param blocks - The blocks, left to right.
7432
+ * @param from - The selected cell.
7433
+ * @param key - The navigation key.
7434
+ * @return The next cell, or undefined to keep the selection.
7435
+ */
7436
+ const stepCalendarCell = (grid, blocks, from, key) => {
7437
+ if (key === "ArrowRight" || key === "ArrowLeft") {
7438
+ const order = readingOrder(grid, blocks);
7439
+ const index = order.findIndex((cell) => cell.column === from.column && cell.row === from.row);
7440
+ if (index < 0) return;
7441
+ const ahead = key === "ArrowRight" ? order.slice(index + 1) : order.slice(0, index).reverse();
7442
+ return firstSelectable(grid, ahead);
7443
+ }
7444
+ if (key === "ArrowDown" || key === "ArrowUp") return firstSelectable(grid, columnOrderFrom(grid, blocks, from, key === "ArrowDown" ? 1 : -1));
7445
+ const blockIndex = blockOf(blocks, from.column);
7446
+ const target = blocks[blockIndex + (key === "PageDown" ? 1 : -1)];
7447
+ if (blockIndex < 0 || !target) return;
7448
+ const same = {
7449
+ column: target.start + (from.column - blocks[blockIndex].start),
7450
+ row: from.row
7451
+ };
7452
+ const page = readingOrder(grid, [target]);
7453
+ const at = page.findIndex((cell) => cell.row > same.row || cell.row === same.row && cell.column >= same.column);
7454
+ const after = at === -1 ? [] : page.slice(at);
7455
+ const before = page.slice(0, at === -1 ? page.length : at).reverse();
7456
+ return firstSelectable(grid, after) ?? firstSelectable(grid, before);
7457
+ };
7458
+ //#endregion
6688
7459
  //#region src/charts/heatmap-chart/heatmap-chart.tsx
6689
7460
  const CELL_MIX_FLOOR = .15;
6690
7461
  const NO_ROW_LABELS = [];
6691
- const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, height = 0, className, compact = false, showValues, maxCellWidth, maxCellHeight, minCellWidth, minCellHeight, rowLabels = NO_ROW_LABELS, primaryColor, gap = "md", withTooltips = false, renderTooltip, children }) => {
7462
+ const TOOLTIP_BOX_STYLES = {
7463
+ light: {
7464
+ ...defaultStyles,
7465
+ zIndex: 3
7466
+ },
7467
+ dark: { zIndex: 3 }
7468
+ };
7469
+ const cellName = (info) => info.cellLabel || [
7470
+ info.groupLabel,
7471
+ info.columnLabel,
7472
+ info.rowLabel
7473
+ ].filter(Boolean).join(" ");
7474
+ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, height = 0, className, compact = false, showValues, maxCellWidth, maxCellHeight, minCellWidth, minCellHeight, rowLabels = NO_ROW_LABELS, columnGroups, keyboardNavigation = "grid", ariaLabel, primaryColor, gap = "md", withTooltips = false, renderTooltip, tooltipVariant = "light", tooltipStyle, children }) => {
6692
7475
  const chartId = useChartId(providedChartId);
7476
+ const tooltipBoxStyle = tooltipStyle ? {
7477
+ ...TOOLTIP_BOX_STYLES[tooltipVariant],
7478
+ ...tooltipStyle
7479
+ } : TOOLTIP_BOX_STYLES[tooltipVariant];
6693
7480
  const { getElementStyles, theme } = useGlobalChartsContext();
6694
7481
  const scopeElement = useChartScopeElement();
6695
7482
  const { heatmapChart: heatmapChartSettings } = theme;
6696
7483
  const { nonLegendChildren } = useChartChildren(children, "HeatmapChart");
6697
- const [selectedIndex, setSelectedIndex] = useState();
7484
+ const [selected, setSelected] = useState();
6698
7485
  const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, showTooltip, hideTooltip } = useTooltip();
6699
7486
  const standaloneScopeClass = useStandaloneScopeClass();
6700
7487
  const containerRef = useRef(null);
@@ -6713,69 +7500,75 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6713
7500
  }), [extent, primaryColorHex]);
6714
7501
  const columns = data.length;
6715
7502
  const rows = Math.max(0, ...data.map((column) => column.data.length));
6716
- const { compactCellGap, compactCellSize } = heatmapChartSettings;
7503
+ const groupLayout = useMemo(() => resolveColumnGroups(columnGroups, columns, 2), [columnGroups, columns]);
7504
+ const { compactCellGap, compactCellSize, groupGap } = heatmapChartSettings;
6717
7505
  const drawValues = showValues ?? !compact;
6718
7506
  const buildTooltipData = useCallback((columnIndex, rowIndex) => {
6719
7507
  const cell = data[columnIndex]?.data[rowIndex];
7508
+ const group = groupLayout.columns[columnIndex]?.group;
6720
7509
  return {
6721
7510
  value: cell?.value ?? null,
6722
7511
  rowLabel: rowLabels[rowIndex],
6723
7512
  columnLabel: data[columnIndex]?.label,
7513
+ groupLabel: group === void 0 ? void 0 : groupLayout.groups[group]?.label,
6724
7514
  cellLabel: cell?.label,
6725
7515
  row: rowIndex,
6726
7516
  column: columnIndex
6727
7517
  };
6728
- }, [data, rowLabels]);
7518
+ }, [
7519
+ data,
7520
+ rowLabels,
7521
+ groupLayout
7522
+ ]);
6729
7523
  const onChartBlur = useCallback(() => {
6730
- setSelectedIndex(void 0);
7524
+ setSelected(void 0);
6731
7525
  hideTooltip();
6732
7526
  }, [hideTooltip]);
6733
7527
  const isCellInert = useCallback((col, row) => {
6734
7528
  const cell = data[col]?.data[row];
6735
7529
  return cell?.hidden === true || cell?.placeholder === true;
6736
7530
  }, [data]);
7531
+ const blocks = useMemo(() => {
7532
+ const grouped = groupLayout.groups.map((group, index) => {
7533
+ const start = groupLayout.columns.findIndex((column) => column.group === index);
7534
+ return {
7535
+ start,
7536
+ end: start + group.span
7537
+ };
7538
+ });
7539
+ const groupedEnd = grouped.length ? grouped[grouped.length - 1].end : 0;
7540
+ return groupedEnd < columns ? [...grouped, {
7541
+ start: groupedEnd,
7542
+ end: columns
7543
+ }] : grouped;
7544
+ }, [groupLayout, columns]);
6737
7545
  const onChartKeyDown = useCallback((event) => {
6738
- if (![
6739
- "ArrowLeft",
6740
- "ArrowRight",
6741
- "ArrowUp",
6742
- "ArrowDown",
6743
- "Escape",
6744
- "Tab"
6745
- ].includes(event.key)) return;
6746
7546
  if (event.key === "Tab" || event.key === "Escape") {
6747
- setSelectedIndex(void 0);
7547
+ setSelected(void 0);
6748
7548
  hideTooltip();
6749
7549
  return;
6750
7550
  }
7551
+ if (!isNavigationKey(event.key, keyboardNavigation)) return;
6751
7552
  event.preventDefault();
6752
- if (selectedIndex === void 0) {
6753
- for (let index = 0; index < columns * rows; index++) if (!isCellInert(Math.floor(index / rows), index % rows)) {
6754
- setSelectedIndex(index);
6755
- return;
6756
- }
7553
+ const grid = {
7554
+ columns,
7555
+ rows,
7556
+ isInert: isCellInert
7557
+ };
7558
+ if (selected === void 0) {
7559
+ setSelected(keyboardNavigation === "calendar" ? firstCalendarCell(grid, blocks) : firstGridCell(grid));
6757
7560
  return;
6758
7561
  }
6759
- let stepCol = 0;
6760
- let stepRow = 0;
6761
- if (event.key === "ArrowRight") stepCol = 1;
6762
- else if (event.key === "ArrowLeft") stepCol = -1;
6763
- else if (event.key === "ArrowDown") stepRow = 1;
6764
- else if (event.key === "ArrowUp") stepRow = -1;
6765
- let col = Math.floor(selectedIndex / rows);
6766
- let row = selectedIndex % rows;
6767
- do {
6768
- col += stepCol;
6769
- row += stepRow;
6770
- } while (col >= 0 && col < columns && row >= 0 && row < rows && isCellInert(col, row));
6771
- if (col < 0 || col >= columns || row < 0 || row >= rows) return;
6772
- setSelectedIndex(col * rows + row);
7562
+ const next = keyboardNavigation === "calendar" ? stepCalendarCell(grid, blocks, selected, event.key) : stepGridCell(grid, selected, event.key);
7563
+ if (next) setSelected(next);
6773
7564
  }, [
6774
7565
  rows,
6775
7566
  columns,
6776
- selectedIndex,
7567
+ selected,
6777
7568
  hideTooltip,
6778
- isCellInert
7569
+ isCellInert,
7570
+ keyboardNavigation,
7571
+ blocks
6779
7572
  ]);
6780
7573
  const handleCellMouseMove = useCallback((event) => {
6781
7574
  if (!withTooltips) return;
@@ -6795,35 +7588,47 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6795
7588
  buildTooltipData,
6796
7589
  getTooltipOrigin
6797
7590
  ]);
7591
+ const getCellElement = useCallback((col, row) => typeof document !== "undefined" ? document.getElementById(`${chartId}-cell-${col}-${row}`) : null, [chartId]);
6798
7592
  const handleCellMouseLeave = useCallback(() => {
6799
- if (withTooltips && selectedIndex === void 0) hideTooltip();
7593
+ if (withTooltips && selected === void 0) hideTooltip();
6800
7594
  }, [
6801
7595
  withTooltips,
6802
- selectedIndex,
7596
+ selected,
6803
7597
  hideTooltip
6804
7598
  ]);
6805
7599
  useEffect(() => {
6806
- if (!withTooltips || selectedIndex === void 0) return;
7600
+ if (selected && (selected.column >= columns || selected.row >= rows)) setSelected(void 0);
7601
+ }, [
7602
+ selected,
7603
+ columns,
7604
+ rows
7605
+ ]);
7606
+ useEffect(() => {
7607
+ if (selected === void 0) return;
7608
+ getCellElement(selected.column, selected.row)?.scrollIntoView?.({
7609
+ block: "nearest",
7610
+ inline: "nearest"
7611
+ });
7612
+ }, [selected, getCellElement]);
7613
+ useEffect(() => {
7614
+ if (!withTooltips || selected === void 0) return;
6807
7615
  const origin = getTooltipOrigin();
6808
7616
  if (!origin) return;
6809
- const col = Math.floor(selectedIndex / rows);
6810
- const row = selectedIndex % rows;
6811
- const rect = (typeof document !== "undefined" ? document.getElementById(`${chartId}-cell-${col}-${row}`) : null)?.getBoundingClientRect();
7617
+ const rect = getCellElement(selected.column, selected.row)?.getBoundingClientRect();
6812
7618
  showTooltip({
6813
7619
  tooltipLeft: rect ? rect.left + rect.width / 2 - origin.left : 0,
6814
7620
  tooltipTop: rect ? rect.top + rect.height / 2 - origin.top : 0,
6815
- tooltipData: buildTooltipData(col, row)
7621
+ tooltipData: buildTooltipData(selected.column, selected.row)
6816
7622
  });
6817
7623
  }, [
6818
- selectedIndex,
7624
+ selected,
6819
7625
  withTooltips,
6820
- rows,
6821
- chartId,
7626
+ getCellElement,
6822
7627
  buildTooltipData,
6823
7628
  showTooltip,
6824
7629
  getTooltipOrigin
6825
7630
  ]);
6826
- const defaultRenderTooltip = useCallback((info) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("strong", { children: info.cellLabel || `${info.columnLabel ?? ""} ${info.rowLabel ?? ""}`.trim() }), /* @__PURE__ */ jsx("div", { children: info.value === null ? __("No data", "jetpack-charts") : formatNumber(info.value) })] }), []);
7631
+ const defaultRenderTooltip = useCallback((info) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("strong", { children: cellName(info) }), /* @__PURE__ */ jsx("div", { children: info.value === null ? __("No data", "jetpack-charts") : formatNumber(info.value) })] }), []);
6827
7632
  if (!columns || !rows) return /* @__PURE__ */ jsx(Center, {
6828
7633
  className: clsx("heatmap-chart", heatmap_chart_module_default["heatmap-chart"], className),
6829
7634
  style: {
@@ -6837,10 +7642,16 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6837
7642
  });
6838
7643
  const columnTrack = compact ? "var(--a8c-charts-dimension-heatmap-cell-size)" : `minmax(${minCellWidth ?? 0}px, ${maxCellWidth ? `${maxCellWidth}px` : "1fr"})`;
6839
7644
  const rowTrack = compact ? "var(--a8c-charts-dimension-heatmap-cell-size)" : `minmax(${minCellHeight ?? 0}px, ${maxCellHeight ? `${maxCellHeight}px` : "1fr"})`;
7645
+ const hasColumnLabels = data.some((column) => Boolean(column.label));
7646
+ const hasGroups = groupLayout.groups.length > 0;
7647
+ const columnLine = (columnIndex) => groupLayout.columns[columnIndex].line;
7648
+ const firstDataRow = hasColumnLabels ? 2 : 1;
7649
+ const dataTrack = (column) => column.summary ? "minmax(auto, max-content)" : columnTrack;
7650
+ const gapTrack = compact ? `minmax(${groupGap}px, 1fr)` : `${groupGap}px`;
6840
7651
  const gridStyle = {
6841
7652
  "--a8c-charts-color-heatmap-primary": primaryColorHex,
6842
- gridTemplateColumns: `auto ${data.some((column) => column.summary) ? data.map((column) => column.summary ? "minmax(auto, max-content)" : columnTrack).join(" ") : `repeat(${columns}, ${columnTrack})`}`,
6843
- gridTemplateRows: `auto repeat(${rows}, ${rowTrack})`
7653
+ gridTemplateColumns: `auto ${data.map((column, columnIndex) => groupLayout.columns[columnIndex].gapBefore ? `${gapTrack} ${dataTrack(column)}` : dataTrack(column)).join(" ")}`,
7654
+ gridTemplateRows: `${hasColumnLabels ? "auto " : ""}repeat(${rows}, ${rowTrack})${hasGroups ? " auto" : ""}`
6844
7655
  };
6845
7656
  if (compact) {
6846
7657
  gridStyle["--a8c-charts-dimension-heatmap-cell-gap"] = `${compactCellGap}px`;
@@ -6853,7 +7664,7 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6853
7664
  [heatmap_chart_module_default["heatmap-chart__gap-end"]]: columnIndex < columns - 1 && !data[columnIndex + 1]?.summary
6854
7665
  };
6855
7666
  };
6856
- const activeDescendant = selectedIndex !== void 0 ? `${chartId}-cell-${Math.floor(selectedIndex / rows)}-${selectedIndex % rows}` : void 0;
7667
+ const activeDescendant = selected ? `${chartId}-cell-${selected.column}-${selected.row}` : void 0;
6857
7668
  const heightCapped = !compact && Boolean(maxCellHeight);
6858
7669
  return /* @__PURE__ */ jsx(HeatmapContext.Provider, {
6859
7670
  value: heatmapContext,
@@ -6873,7 +7684,7 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6873
7684
  children: [/* @__PURE__ */ jsxs("div", {
6874
7685
  ref: containerRef,
6875
7686
  role: "grid",
6876
- "aria-label": __("Heatmap chart", "jetpack-charts"),
7687
+ "aria-label": ariaLabel ?? __("Heatmap chart", "jetpack-charts"),
6877
7688
  "aria-rowcount": rows,
6878
7689
  "aria-colcount": columns,
6879
7690
  "aria-activedescendant": activeDescendant,
@@ -6882,76 +7693,115 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6882
7693
  onKeyDown: onChartKeyDown,
6883
7694
  className: clsx(heatmap_chart_module_default["heatmap-chart__grid"], {
6884
7695
  [heatmap_chart_module_default["heatmap-chart__grid--compact"]]: compact,
7696
+ [heatmap_chart_module_default["heatmap-chart__grid--flex-gaps"]]: compact && hasGroups,
6885
7697
  [heatmap_chart_module_default["heatmap-chart__grid--height-capped"]]: heightCapped
6886
7698
  }),
6887
7699
  style: gridStyle,
6888
- children: [/* @__PURE__ */ jsxs("div", {
6889
- role: "row",
6890
- "aria-hidden": "true",
6891
- className: heatmap_chart_module_default["heatmap-chart__row"],
6892
- children: [/* @__PURE__ */ jsx("span", {}), data.map((column, columnIndex) => /* @__PURE__ */ jsx("span", {
6893
- className: clsx(heatmap_chart_module_default["heatmap-chart__col-label"], {
6894
- [heatmap_chart_module_default["heatmap-chart__col-label--summary"]]: column.summary,
6895
- ...summaryGaps(columnIndex)
6896
- }),
6897
- children: column.label
6898
- }, `col-${columnIndex}`))]
6899
- }), Array.from({ length: rows }).map((_row, rowIndex) => {
6900
- const labelVisible = !compact || rowIndex % 2 === 0;
6901
- return /* @__PURE__ */ jsxs("div", {
7700
+ children: [
7701
+ hasColumnLabels && /* @__PURE__ */ jsxs("div", {
6902
7702
  role: "row",
6903
- "aria-rowindex": rowIndex + 1,
7703
+ "aria-hidden": "true",
6904
7704
  className: heatmap_chart_module_default["heatmap-chart__row"],
6905
- children: [/* @__PURE__ */ jsx("span", {
6906
- "aria-hidden": "true",
6907
- className: heatmap_chart_module_default["heatmap-chart__row-label"],
6908
- children: labelVisible ? rowLabels[rowIndex] ?? "" : ""
6909
- }), data.map((column, columnIndex) => {
6910
- const cell = column.data[rowIndex];
6911
- if (cell?.hidden) return /* @__PURE__ */ jsx("div", {
6912
- "aria-hidden": "true",
6913
- className: clsx(heatmap_chart_module_default["heatmap-chart__cell"], heatmap_chart_module_default["heatmap-chart__cell--hidden"])
6914
- }, `cell-${columnIndex}-${rowIndex}`);
6915
- if (cell?.placeholder) return /* @__PURE__ */ jsx("div", {
7705
+ children: [/* @__PURE__ */ jsx("span", { style: {
7706
+ gridColumn: 1,
7707
+ gridRow: 1
7708
+ } }), data.map((column, columnIndex) => /* @__PURE__ */ jsx("span", {
7709
+ style: {
7710
+ gridColumn: columnLine(columnIndex),
7711
+ gridRow: 1
7712
+ },
7713
+ className: clsx(heatmap_chart_module_default["heatmap-chart__col-label"], {
7714
+ [heatmap_chart_module_default["heatmap-chart__col-label--summary"]]: column.summary,
7715
+ ...summaryGaps(columnIndex)
7716
+ }),
7717
+ children: column.label
7718
+ }, `col-${columnIndex}`))]
7719
+ }),
7720
+ Array.from({ length: rows }).map((_row, rowIndex) => {
7721
+ const labelVisible = !compact || rowIndex % 2 === 0;
7722
+ const gridRow = firstDataRow + rowIndex;
7723
+ return /* @__PURE__ */ jsxs("div", {
7724
+ role: "row",
7725
+ "aria-rowindex": rowIndex + 1,
7726
+ className: heatmap_chart_module_default["heatmap-chart__row"],
7727
+ children: [/* @__PURE__ */ jsx("span", {
6916
7728
  "aria-hidden": "true",
6917
- className: clsx(heatmap_chart_module_default["heatmap-chart__cell"], heatmap_chart_module_default["heatmap-chart__cell--placeholder"])
6918
- }, `cell-${columnIndex}-${rowIndex}`);
6919
- const value = cell?.value ?? null;
6920
- const present = isPresent(value);
6921
- const filled = present && !column.summary;
6922
- const normalized = filled ? getNormalizedValue(value, extent) : 0;
6923
- const flatIndex = columnIndex * rows + rowIndex;
6924
- const info = buildTooltipData(columnIndex, rowIndex);
6925
- const accessibleLabel = `${info.cellLabel || `${info.columnLabel ?? ""} ${info.rowLabel ?? ""}`.trim()}: ${info.value === null ? __("No data", "jetpack-charts") : formatNumber(info.value)}`;
6926
- return /* @__PURE__ */ jsx("div", {
6927
- id: `${chartId}-cell-${columnIndex}-${rowIndex}`,
6928
- role: "gridcell",
6929
- tabIndex: -1,
6930
- "aria-colindex": columnIndex + 1,
6931
- "aria-label": accessibleLabel,
6932
- "data-column": columnIndex,
6933
- "data-row": rowIndex,
6934
- className: clsx(heatmap_chart_module_default["heatmap-chart__cell"], {
6935
- [heatmap_chart_module_default["heatmap-chart__cell--filled"]]: filled,
6936
- [heatmap_chart_module_default["heatmap-chart__cell--strong"]]: filled && cellHasLightText(normalized),
6937
- [heatmap_chart_module_default["heatmap-chart__cell--summary"]]: column.summary,
6938
- ...summaryGaps(columnIndex),
6939
- [heatmap_chart_module_default["heatmap-chart__cell--selected"]]: selectedIndex === flatIndex
6940
- }),
6941
- style: filled ? { "--a8c-charts-heatmap-cell-intensity": normalized } : void 0,
6942
- onMouseMove: handleCellMouseMove,
6943
- onMouseLeave: handleCellMouseLeave,
6944
- children: (drawValues || column.summary) && present && /* @__PURE__ */ jsx("span", {
6945
- className: heatmap_chart_module_default["heatmap-chart__cell-value"],
6946
- children: formatNumberCompact(value)
6947
- })
6948
- }, `cell-${columnIndex}-${rowIndex}`);
6949
- })]
6950
- }, `row-${rowIndex}`);
6951
- })]
7729
+ className: heatmap_chart_module_default["heatmap-chart__row-label"],
7730
+ style: {
7731
+ gridColumn: 1,
7732
+ gridRow
7733
+ },
7734
+ children: labelVisible ? rowLabels[rowIndex] ?? "" : ""
7735
+ }), data.map((column, columnIndex) => {
7736
+ const cell = column.data[rowIndex];
7737
+ const placement = {
7738
+ gridColumn: columnLine(columnIndex),
7739
+ gridRow
7740
+ };
7741
+ if (cell?.hidden) return /* @__PURE__ */ jsx("div", {
7742
+ "aria-hidden": "true",
7743
+ style: placement,
7744
+ className: clsx(heatmap_chart_module_default["heatmap-chart__cell"], heatmap_chart_module_default["heatmap-chart__cell--hidden"])
7745
+ }, `cell-${columnIndex}-${rowIndex}`);
7746
+ if (cell?.placeholder) return /* @__PURE__ */ jsx("div", {
7747
+ "aria-hidden": "true",
7748
+ style: placement,
7749
+ className: clsx(heatmap_chart_module_default["heatmap-chart__cell"], heatmap_chart_module_default["heatmap-chart__cell--placeholder"])
7750
+ }, `cell-${columnIndex}-${rowIndex}`);
7751
+ const value = cell?.value ?? null;
7752
+ const present = isPresent(value);
7753
+ const filled = present && !column.summary;
7754
+ const normalized = filled ? getNormalizedValue(value, extent) : 0;
7755
+ const info = buildTooltipData(columnIndex, rowIndex);
7756
+ const accessibleLabel = `${cellName(info)}: ${info.value === null ? __("No data", "jetpack-charts") : formatNumber(info.value)}`;
7757
+ return /* @__PURE__ */ jsx("div", {
7758
+ id: `${chartId}-cell-${columnIndex}-${rowIndex}`,
7759
+ role: "gridcell",
7760
+ tabIndex: -1,
7761
+ "aria-colindex": columnIndex + 1,
7762
+ "aria-label": accessibleLabel,
7763
+ "data-column": columnIndex,
7764
+ "data-row": rowIndex,
7765
+ className: clsx(heatmap_chart_module_default["heatmap-chart__cell"], {
7766
+ [heatmap_chart_module_default["heatmap-chart__cell--filled"]]: filled,
7767
+ [heatmap_chart_module_default["heatmap-chart__cell--strong"]]: filled && cellHasLightText(normalized),
7768
+ [heatmap_chart_module_default["heatmap-chart__cell--summary"]]: column.summary,
7769
+ ...summaryGaps(columnIndex),
7770
+ [heatmap_chart_module_default["heatmap-chart__cell--selected"]]: selected?.column === columnIndex && selected?.row === rowIndex
7771
+ }),
7772
+ style: {
7773
+ ...placement,
7774
+ ...filled ? { "--a8c-charts-heatmap-cell-intensity": normalized } : {}
7775
+ },
7776
+ onMouseMove: handleCellMouseMove,
7777
+ onMouseLeave: handleCellMouseLeave,
7778
+ children: (drawValues || column.summary) && present && /* @__PURE__ */ jsx("span", {
7779
+ className: heatmap_chart_module_default["heatmap-chart__cell-value"],
7780
+ children: formatNumberCompact(value)
7781
+ })
7782
+ }, `cell-${columnIndex}-${rowIndex}`);
7783
+ })]
7784
+ }, `row-${rowIndex}`);
7785
+ }),
7786
+ hasGroups && /* @__PURE__ */ jsx("div", {
7787
+ role: "row",
7788
+ "aria-hidden": "true",
7789
+ className: heatmap_chart_module_default["heatmap-chart__row"],
7790
+ children: groupLayout.groups.map((group, groupIndex) => /* @__PURE__ */ jsx("span", {
7791
+ className: heatmap_chart_module_default["heatmap-chart__group-label"],
7792
+ style: {
7793
+ gridColumn: `${group.line} / span ${group.span}`,
7794
+ gridRow: firstDataRow + rows
7795
+ },
7796
+ children: group.label
7797
+ }, `group-${groupIndex}`))
7798
+ })
7799
+ ]
6952
7800
  }), withTooltips && tooltipOpen && tooltipData && /* @__PURE__ */ jsx(BoundedTooltip, {
6953
7801
  top: tooltipTop,
6954
7802
  left: tooltipLeft,
7803
+ className: tooltipVariant === "dark" ? base_tooltip_module_default.surface : void 0,
7804
+ style: tooltipBoxStyle,
6955
7805
  children: /* @__PURE__ */ jsx("div", {
6956
7806
  className: standaloneScopeClass,
6957
7807
  role: "tooltip",
@@ -6987,6 +7837,43 @@ const KEY_OPTIONS = {
6987
7837
  day: "2-digit"
6988
7838
  };
6989
7839
  const DATE_PREFIX = /^(\d{4})-(\d{2})-(\d{2})/;
7840
+ /**
7841
+ * Label formatters for UTC proxies, so both calendar layouts name a day alike.
7842
+ *
7843
+ * `gregory` keeps a locale whose default calendar is not Gregorian (fa-IR) from
7844
+ * splitting one grid month across two names.
7845
+ *
7846
+ * @param locale - BCP 47 locale.
7847
+ * @return Weekday, month and full-day formatters.
7848
+ */
7849
+ const civilLabelFormatters = (locale) => {
7850
+ const formatting = {
7851
+ locale,
7852
+ timeZone: "UTC"
7853
+ };
7854
+ return {
7855
+ formatWeekday: createDateFormatter({
7856
+ weekday: "short",
7857
+ calendar: "gregory"
7858
+ }, formatting),
7859
+ formatMonth: createDateFormatter({
7860
+ month: "short",
7861
+ calendar: "gregory"
7862
+ }, formatting),
7863
+ formatMonthYear: createDateFormatter({
7864
+ month: "short",
7865
+ year: "numeric",
7866
+ calendar: "gregory"
7867
+ }, formatting),
7868
+ formatDay: createDateFormatter({
7869
+ weekday: "short",
7870
+ month: "short",
7871
+ day: "numeric",
7872
+ year: "numeric",
7873
+ calendar: "gregory"
7874
+ }, formatting)
7875
+ };
7876
+ };
6990
7877
  const pad = (value, length) => String(value).padStart(length, "0");
6991
7878
  /**
6992
7879
  * The day a UTC proxy stands for.
@@ -7152,18 +8039,7 @@ const buildCalendarHeatmapData = (series, options = {}) => {
7152
8039
  const gridMaxDayKey = widenTo(options.gridSpan?.end, maxDayKey, "later");
7153
8040
  const requestedMinDate = civilDate(requestedMinDayKey);
7154
8041
  const gridMaxDate = civilDate(gridMaxDayKey);
7155
- const labelFormatting = {
7156
- locale,
7157
- timeZone: "UTC"
7158
- };
7159
- const formatWeekday = createDateFormatter({ weekday: "short" }, labelFormatting);
7160
- const formatMonth = createDateFormatter({ month: "short" }, labelFormatting);
7161
- const formatDay = createDateFormatter({
7162
- weekday: "short",
7163
- month: "short",
7164
- day: "numeric",
7165
- year: "numeric"
7166
- }, labelFormatting);
8042
+ const { formatWeekday, formatMonth, formatDay } = civilLabelFormatters(locale);
7167
8043
  const gridStart = startOfCivilWeek(requestedMinDate, weekStartsOn);
7168
8044
  const gridMinDayKey = requestedMinDayKey < minDayKey ? civilKey(gridStart) : requestedMinDayKey;
7169
8045
  const weekCount = civilWeekSpan(gridStart, gridMaxDate, weekStartsOn);
@@ -7204,6 +8080,89 @@ const buildCalendarHeatmapData = (series, options = {}) => {
7204
8080
  };
7205
8081
  };
7206
8082
  //#endregion
8083
+ //#region src/charts/heatmap-chart/build-month-calendar-data.ts
8084
+ /** Week rows per month, fixed so every month block is the same height. */
8085
+ const MONTH_ROWS = 6;
8086
+ const DAYS_PER_WEEK = 7;
8087
+ const startOfCivilMonth = (date) => {
8088
+ const first = new Date(date.getTime());
8089
+ first.setUTCDate(1);
8090
+ return first;
8091
+ };
8092
+ const startOfNextCivilMonth = (date) => {
8093
+ const next = startOfCivilMonth(date);
8094
+ next.setUTCMonth(next.getUTCMonth() + 1);
8095
+ return next;
8096
+ };
8097
+ /**
8098
+ * Lay a day-keyed map out as one month calendar per month, weekdays across and
8099
+ * weeks down, for `HeatmapChart` with `columnGroups`.
8100
+ *
8101
+ * @param valueByDay - Value per `yyyy-MM-dd`. A missing day is `null`.
8102
+ * @param range - The measured days; every month from `start`'s through `end`'s is drawn.
8103
+ * @param options - Week start and label locale.
8104
+ * @return Columns and one group per month; empty for an invalid or reversed range.
8105
+ */
8106
+ const buildMonthCalendarHeatmapData = (valueByDay, range, options = {}) => {
8107
+ const start = civilDate(range.start);
8108
+ const end = civilDate(range.end);
8109
+ if (!start || !end || start.getTime() > end.getTime()) {
8110
+ warnOnce(`heatmap:monthRange:${range.start}:${range.end}`, `range ${JSON.stringify(range)} must be two \`yyyy-MM-dd\` days with start on or before end, so the calendar is empty.`);
8111
+ return {
8112
+ data: [],
8113
+ columnGroups: []
8114
+ };
8115
+ }
8116
+ const startKey = civilKey(start);
8117
+ const endKey = civilKey(end);
8118
+ const weekStartsOn = options.weekStartsOn ?? 1;
8119
+ const { locale } = sanitizeFormatting({ locale: options.locale });
8120
+ for (const key of Object.keys(valueByDay)) if (!civilDate(key)) warnOnce(`heatmap:monthKey:${key}`, `${JSON.stringify(key)} is not a \`yyyy-MM-dd\` day, so its value is left out of the calendar.`);
8121
+ const readValue = (key) => {
8122
+ const value = valueByDay[key];
8123
+ return isPresent(value) ? value : null;
8124
+ };
8125
+ const { formatMonth, formatMonthYear, formatDay } = civilLabelFormatters(locale);
8126
+ const data = [];
8127
+ const columnGroups = [];
8128
+ const lastMonthStart = startOfCivilMonth(end);
8129
+ const formatGroup = (lastMonthStart.getUTCFullYear() - start.getUTCFullYear()) * 12 + lastMonthStart.getUTCMonth() - start.getUTCMonth() + 1 > 12 ? formatMonthYear : formatMonth;
8130
+ for (let monthStart = startOfCivilMonth(start); monthStart.getTime() <= lastMonthStart.getTime(); monthStart = startOfNextCivilMonth(monthStart)) {
8131
+ const monthStartKey = civilKey(monthStart);
8132
+ const monthEndKey = civilKey(addCivilDays(startOfNextCivilMonth(monthStart), -1));
8133
+ const gridStart = startOfCivilWeek(monthStart, weekStartsOn);
8134
+ for (let weekday = 0; weekday < DAYS_PER_WEEK; weekday++) {
8135
+ const cells = [];
8136
+ for (let week = 0; week < MONTH_ROWS; week++) {
8137
+ const day = addCivilDays(gridStart, week * DAYS_PER_WEEK + weekday);
8138
+ const key = civilKey(day);
8139
+ if (key < monthStartKey || key > monthEndKey) cells.push({
8140
+ value: null,
8141
+ hidden: true
8142
+ });
8143
+ else if (key < startKey || key > endKey) cells.push({
8144
+ label: formatDay(day),
8145
+ value: null,
8146
+ placeholder: true
8147
+ });
8148
+ else cells.push({
8149
+ label: formatDay(day),
8150
+ value: readValue(key)
8151
+ });
8152
+ }
8153
+ data.push({ data: cells });
8154
+ }
8155
+ columnGroups.push({
8156
+ label: formatGroup(monthStart),
8157
+ span: DAYS_PER_WEEK
8158
+ });
8159
+ }
8160
+ return {
8161
+ data,
8162
+ columnGroups
8163
+ };
8164
+ };
8165
+ //#endregion
7207
8166
  //#region src/charts/heatmap-chart/use-calendar-heatmap-data.ts
7208
8167
  /**
7209
8168
  * `buildCalendarHeatmapData` with the locale and zone taken from
@@ -7240,6 +8199,36 @@ const useCalendarHeatmapData = (series, options = {}) => {
7240
8199
  ]);
7241
8200
  };
7242
8201
  //#endregion
8202
+ //#region src/charts/heatmap-chart/use-month-calendar-heatmap-data.ts
8203
+ /**
8204
+ * `buildMonthCalendarHeatmapData` with the locale taken from `GlobalChartsProvider`
8205
+ * where the caller names none.
8206
+ *
8207
+ * @param valueByDay - Value per `yyyy-MM-dd`. Held by reference, so a caller that rebuilds it each render defeats the memo.
8208
+ * @param range - As for `buildMonthCalendarHeatmapData`.
8209
+ * @param options - As for `buildMonthCalendarHeatmapData`; `locale` wins over the provider.
8210
+ * @return Columns and groups for `HeatmapChart`.
8211
+ */
8212
+ const useMonthCalendarHeatmapData = (valueByDay, range, options = {}) => {
8213
+ const formatting = useChartFormatting();
8214
+ const locale = options.locale ?? formatting.locale;
8215
+ const { weekStartsOn } = options;
8216
+ const { start, end } = range;
8217
+ return useMemo(() => buildMonthCalendarHeatmapData(valueByDay, {
8218
+ start,
8219
+ end
8220
+ }, {
8221
+ locale,
8222
+ weekStartsOn
8223
+ }), [
8224
+ valueByDay,
8225
+ start,
8226
+ end,
8227
+ locale,
8228
+ weekStartsOn
8229
+ ]);
8230
+ };
8231
+ //#endregion
7243
8232
  //#region ../../../node_modules/.pnpm/is-plain-object@5.1.0/node_modules/is-plain-object/dist/is-plain-object.mjs
7244
8233
  /*!
7245
8234
  * is-plain-object <https://github.com/jonschlinkert/is-plain-object>
@@ -7307,7 +8296,24 @@ function paramCase(input, options) {
7307
8296
  return dotCase(input, __assign({ delimiter: "-" }, options));
7308
8297
  }
7309
8298
  //#endregion
7310
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/utils/hooks/use-update-effect.mjs
8299
+ //#region ../../../node_modules/.pnpm/@wordpress+warning@3.55.0/node_modules/@wordpress/warning/build-module/utils.mjs
8300
+ var logged = /* @__PURE__ */ new Set();
8301
+ //#endregion
8302
+ //#region ../../../node_modules/.pnpm/@wordpress+warning@3.55.0/node_modules/@wordpress/warning/build-module/index.mjs
8303
+ function isDev() {
8304
+ return globalThis.SCRIPT_DEBUG === true;
8305
+ }
8306
+ function warning(message) {
8307
+ if (!isDev()) return;
8308
+ if (logged.has(message)) return;
8309
+ console.warn(message);
8310
+ try {
8311
+ throw Error(message);
8312
+ } catch {}
8313
+ logged.add(message);
8314
+ }
8315
+ //#endregion
8316
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/utils/hooks/use-update-effect.mjs
7311
8317
  function useUpdateEffect(effect, deps) {
7312
8318
  const mountedRef = useRef$1(false);
7313
8319
  useEffect$1(() => {
@@ -8877,7 +9883,7 @@ _createEmotion.css;
8877
9883
  _createEmotion.sheet;
8878
9884
  _createEmotion.cache;
8879
9885
  //#endregion
8880
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/utils/hooks/use-cx.mjs
9886
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/utils/hooks/use-cx.mjs
8881
9887
  var isSerializedStyles = (o) => typeof o !== "undefined" && o !== null && ["name", "styles"].every((p) => typeof o[p] !== "undefined");
8882
9888
  var useCx = () => {
8883
9889
  const cache = __unsafe_useEmotionCache();
@@ -8984,7 +9990,7 @@ function memize(fn, options) {
8984
9990
  return memoized;
8985
9991
  }
8986
9992
  //#endregion
8987
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/utils/colors-values.mjs
9993
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/utils/colors-values.mjs
8988
9994
  var white = "#fff";
8989
9995
  var GRAY = {
8990
9996
  900: "#1e1e1e",
@@ -9034,8 +10040,8 @@ var UI = {
9034
10040
  border: THEME.gray[600],
9035
10041
  borderHover: THEME.gray[700],
9036
10042
  borderFocus: THEME.accent,
9037
- borderDisabled: THEME.gray[400],
9038
- textDisabled: THEME.gray[600],
10043
+ borderDisabled: `var(--wpds-color-stroke-interactive-neutral-disabled, #dbdbdb)`,
10044
+ textDisabled: `var(--wpds-color-foreground-interactive-neutral-disabled, #8d8d8d)`,
9039
10045
  darkGrayPlaceholder: `color-mix(in srgb, ${THEME.foreground}, transparent 38%)`,
9040
10046
  lightGrayPlaceholder: `color-mix(in srgb, ${THEME.background}, transparent 35%)`
9041
10047
  };
@@ -9063,7 +10069,7 @@ var COLORS = Object.freeze({
9063
10069
  ui: UI
9064
10070
  });
9065
10071
  //#endregion
9066
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/utils/config-values.mjs
10072
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/utils/config-values.mjs
9067
10073
  var CONTROL_HEIGHT = "36px";
9068
10074
  var CONTROL_PROPS = {
9069
10075
  controlPaddingX: 12,
@@ -9117,8 +10123,8 @@ var config_values_default = Object.assign({}, CONTROL_PROPS, {
9117
10123
  transitionTimingFunctionControl: "cubic-bezier(0.12, 0.8, 0.32, 1)"
9118
10124
  });
9119
10125
  //#endregion
9120
- //#region ../../../node_modules/.pnpm/fast-deep-equal@3.1.3/node_modules/fast-deep-equal/es6/index.js
9121
- var require_es6 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
10126
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/context/context-system-provider.mjs
10127
+ var import_es6 = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
9122
10128
  module.exports = function equal(a, b) {
9123
10129
  if (a === b) return true;
9124
10130
  if (a && b && typeof a == "object" && typeof b == "object") {
@@ -9162,31 +10168,8 @@ var require_es6 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
9162
10168
  }
9163
10169
  return a !== a && b !== b;
9164
10170
  };
9165
- }));
9166
- //#endregion
9167
- //#region ../../../node_modules/.pnpm/@wordpress+warning@3.54.0/node_modules/@wordpress/warning/build-module/utils.mjs
9168
- var logged = /* @__PURE__ */ new Set();
9169
- //#endregion
9170
- //#region ../../../node_modules/.pnpm/@wordpress+warning@3.54.0/node_modules/@wordpress/warning/build-module/index.mjs
9171
- function isDev() {
9172
- return globalThis.SCRIPT_DEBUG === true;
9173
- }
9174
- function warning(message) {
9175
- if (!isDev()) return;
9176
- if (logged.has(message)) return;
9177
- console.warn(message);
9178
- try {
9179
- throw Error(message);
9180
- } catch {}
9181
- logged.add(message);
9182
- }
9183
- //#endregion
9184
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/context/context-system-provider.mjs
9185
- var import_es6 = /* @__PURE__ */ __toESM(require_es6(), 1);
9186
- var ComponentsContext = createContext$1(
9187
- /** @type {Record<string, any>} */
9188
- {}
9189
- );
10171
+ })))(), 1);
10172
+ var ComponentsContext = createContext$1({});
9190
10173
  ComponentsContext.displayName = "ComponentsContext";
9191
10174
  var useComponentsContext = () => useContext$1(ComponentsContext);
9192
10175
  function useContextSystemBridge({ value }) {
@@ -9208,18 +10191,18 @@ var BaseContextSystemProvider = ({ children, value }) => {
9208
10191
  };
9209
10192
  memo(BaseContextSystemProvider);
9210
10193
  //#endregion
9211
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/context/constants.mjs
10194
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/context/constants.mjs
9212
10195
  var COMPONENT_NAMESPACE = "data-wp-component";
9213
10196
  var CONNECTED_NAMESPACE = "data-wp-c16t";
9214
10197
  var CONNECT_STATIC_NAMESPACE = "__contextSystemKey__";
9215
10198
  //#endregion
9216
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/context/get-styled-class-name-from-key.mjs
10199
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/context/get-styled-class-name-from-key.mjs
9217
10200
  function getStyledClassName(namespace) {
9218
10201
  return `components-${paramCase(namespace)}`;
9219
10202
  }
9220
10203
  var getStyledClassNameFromKey = memize(getStyledClassName);
9221
10204
  //#endregion
9222
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/context/context-connect.mjs
10205
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/context/context-connect.mjs
9223
10206
  function contextConnect(Component, namespace) {
9224
10207
  return _contextConnect(Component, namespace, { forwardsRef: true });
9225
10208
  }
@@ -9236,7 +10219,7 @@ function _contextConnect(Component, namespace, options) {
9236
10219
  });
9237
10220
  }
9238
10221
  //#endregion
9239
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/context/utils.mjs
10222
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/context/utils.mjs
9240
10223
  function getNamespace(componentName) {
9241
10224
  return { [COMPONENT_NAMESPACE]: componentName };
9242
10225
  }
@@ -9244,7 +10227,7 @@ function getConnectedNamespace() {
9244
10227
  return { [CONNECTED_NAMESPACE]: true };
9245
10228
  }
9246
10229
  //#endregion
9247
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/context/use-context-system.mjs
10230
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/context/use-context-system.mjs
9248
10231
  function useContextSystem(props, namespace) {
9249
10232
  const contextSystemProps = useComponentsContext();
9250
10233
  if (typeof namespace === "undefined") globalThis.SCRIPT_DEBUG === true && warning("useContextSystem: Please provide a namespace");
@@ -9253,10 +10236,12 @@ function useContextSystem(props, namespace) {
9253
10236
  ...getConnectedNamespace(),
9254
10237
  ...getNamespace(namespace)
9255
10238
  };
9256
- const { _overrides: overrideProps, ...otherContextProps } = contextProps;
10239
+ const { _overrides, ...otherContextProps } = contextProps;
10240
+ const overrideProps = _overrides;
9257
10241
  const initialMergedProps = Object.entries(otherContextProps).length ? Object.assign({}, otherContextProps, props) : props;
10242
+ const propsWithChildren = initialMergedProps;
9258
10243
  const classes = useCx()(getStyledClassNameFromKey(namespace), props.className);
9259
- const rendered = typeof initialMergedProps.renderChildren === "function" ? initialMergedProps.renderChildren(initialMergedProps) : initialMergedProps.children;
10244
+ const rendered = typeof propsWithChildren.renderChildren === "function" ? propsWithChildren.renderChildren(initialMergedProps) : propsWithChildren.children;
9260
10245
  for (const key in initialMergedProps) finalComponentProps[key] = initialMergedProps[key];
9261
10246
  for (const key in overrideProps) finalComponentProps[key] = overrideProps[key];
9262
10247
  if (rendered !== void 0) finalComponentProps.children = rendered;
@@ -9264,7 +10249,7 @@ function useContextSystem(props, namespace) {
9264
10249
  return finalComponentProps;
9265
10250
  }
9266
10251
  //#endregion
9267
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/visually-hidden/styles.mjs
10252
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/visually-hidden/styles.mjs
9268
10253
  var visuallyHidden = {
9269
10254
  border: 0,
9270
10255
  clip: "rect(1px, 1px, 1px, 1px)",
@@ -9280,7 +10265,7 @@ var visuallyHidden = {
9280
10265
  wordBreak: "normal"
9281
10266
  };
9282
10267
  //#endregion
9283
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/utils/polymorphic-element.mjs
10268
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/utils/polymorphic-element.mjs
9284
10269
  var customAttributeRegExp = /^(data|aria|x)-/i;
9285
10270
  var eventHandlerRegExp = /^on[A-Z]/;
9286
10271
  var svgElementNames = new Set(`animate animateMotion animateTransform circle clipPath defs desc ellipse
@@ -9370,7 +10355,7 @@ function UnforwardedPolymorphicElement({ as, ...props }, ref) {
9370
10355
  }
9371
10356
  var PolymorphicElement = forwardRef$1(UnforwardedPolymorphicElement);
9372
10357
  //#endregion
9373
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/view/component.mjs
10358
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/view/component.mjs
9374
10359
  function UnforwardedView({ css, ...restProps }, ref) {
9375
10360
  return /* @__PURE__ */ jsx(PolymorphicElement, {
9376
10361
  ref,
@@ -9379,7 +10364,7 @@ function UnforwardedView({ css, ...restProps }, ref) {
9379
10364
  }
9380
10365
  var component_default$2 = Object.assign(forwardRef$1(UnforwardedView), { selector: ".components-view" });
9381
10366
  //#endregion
9382
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/visually-hidden/component.mjs
10367
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/visually-hidden/component.mjs
9383
10368
  function UnconnectedVisuallyHidden(props, forwardedRef) {
9384
10369
  const { style: styleProp, ...contextProps } = useContextSystem(props, "VisuallyHidden");
9385
10370
  return /* @__PURE__ */ jsx(component_default$2, {
@@ -9394,7 +10379,7 @@ function UnconnectedVisuallyHidden(props, forwardedRef) {
9394
10379
  }
9395
10380
  var component_default$1 = contextConnect(UnconnectedVisuallyHidden, "VisuallyHidden");
9396
10381
  //#endregion
9397
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/utils/use-responsive-value.mjs
10382
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/utils/use-responsive-value.mjs
9398
10383
  var breakpoints = [
9399
10384
  "40em",
9400
10385
  "52em",
@@ -9428,7 +10413,7 @@ function useResponsiveValue(values, options = {}) {
9428
10413
  return array[index >= array.length ? array.length - 1 : index];
9429
10414
  }
9430
10415
  //#endregion
9431
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/grid/utils.mjs
10416
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/grid/utils.mjs
9432
10417
  var ALIGNMENTS = {
9433
10418
  bottom: {
9434
10419
  alignItems: "flex-end",
@@ -9476,7 +10461,7 @@ function getAlignmentProps(alignment) {
9476
10461
  return alignment ? ALIGNMENTS[alignment] : {};
9477
10462
  }
9478
10463
  //#endregion
9479
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/grid/hook.mjs
10464
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/grid/hook.mjs
9480
10465
  function useGrid(props) {
9481
10466
  const { align, alignment, className, columnGap, columns = 2, gap = 3, isInline = false, justify, rowGap, rows, templateColumns, templateRows, ...otherProps } = useContextSystem(props, "Grid");
9482
10467
  const column = useResponsiveValue(Array.isArray(columns) ? columns : [columns]);
@@ -9518,7 +10503,7 @@ function useGrid(props) {
9518
10503
  };
9519
10504
  }
9520
10505
  //#endregion
9521
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/grid/component.mjs
10506
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.1.0_@date-fns+tz@1.5.0_@types+react-dom@18.3.7_@types+react@18_c300d25e93ed6b8d3659e83df7c1508a/node_modules/@wordpress/components/build-module/grid/component.mjs
9522
10507
  function UnconnectedGrid(props, forwardedRef) {
9523
10508
  const gridProps = useGrid(props);
9524
10509
  return /* @__PURE__ */ jsx(component_default$2, {
@@ -10774,6 +11759,6 @@ function TrendIndicator({ direction, value, className, style, showIcon = true })
10774
11759
  });
10775
11760
  }
10776
11761
  //#endregion
10777
- export { AccessibleTooltip, AreaChartResponsive as AreaChart, AreaChart as AreaChartUnresponsive, BarChartResponsive as BarChart, BarChart as BarChartUnresponsive, BarListChartResponsive as BarListChart, BarListChart as BarListChartUnresponsive, BaseTooltip, ConversionFunnelChartWithProvider as ConversionFunnelChart, GeoChartResponsive as GeoChart, GeoChartWithProvider as GeoChartUnresponsive, GlobalChartsContext, GlobalChartsProvider, GlobalChartsProvider as ThemeProvider, GoogleDataTableColumnRoleType, HeatmapChartResponsive as HeatmapChart, HeatmapChart as HeatmapChartUnresponsive, LeaderboardChartResponsive as LeaderboardChart, LeaderboardChart as LeaderboardChartUnresponsive, Legend, LineChartResponsive as LineChart, LineChart as LineChartUnresponsive, PieChartResponsive as PieChart, PieChart as PieChartUnresponsive, PieSemiCircleChartResponsive as PieSemiCircleChart, PieSemiCircleChart as PieSemiCircleChartUnresponsive, Sparkline, SparklineUnresponsive, TrendIndicator, buildCalendarHeatmapData, defaultTheme, formatMetricValue, formatPercentage, getBucketInfo, getColorDistance, hexToRgba, isValidHexColor, lightenHexColor, mergeThemes, mixHexColors, normalizeColorToHex, parseAsLocalDate, parseHslString, prefersLightText, relativeLuminance, resolveCssVariable, useCalendarHeatmapData, useChartFormatting, useChartLegendItems, useChartRegistration, useChartScopeElement, useGlobalChartsContext, useGlobalChartsTheme, useLeaderboardLegendItems, validateHexColor };
11762
+ export { AccessibleTooltip, AreaChartResponsive as AreaChart, AreaChart as AreaChartUnresponsive, BarChartResponsive as BarChart, BarChart as BarChartUnresponsive, BarListChartResponsive as BarListChart, BarListChart as BarListChartUnresponsive, BaseTooltip, ConversionFunnelChartWithProvider as ConversionFunnelChart, GeoChartResponsive as GeoChart, GeoChartWithProvider as GeoChartUnresponsive, GlobalChartsContext, GlobalChartsProvider, GlobalChartsProvider as ThemeProvider, GoogleDataTableColumnRoleType, HeatmapChartResponsive as HeatmapChart, HeatmapChart as HeatmapChartUnresponsive, LeaderboardChartResponsive as LeaderboardChart, LeaderboardChart as LeaderboardChartUnresponsive, Legend, LineChartResponsive as LineChart, LineChart as LineChartUnresponsive, PieChartResponsive as PieChart, PieChart as PieChartUnresponsive, PieSemiCircleChartResponsive as PieSemiCircleChart, PieSemiCircleChart as PieSemiCircleChartUnresponsive, Sparkline, SparklineUnresponsive, TrendIndicator, buildCalendarHeatmapData, buildMonthCalendarHeatmapData, defaultTheme, formatMetricValue, formatPercentage, getBucketInfo, getColorDistance, hexToRgba, isValidHexColor, lightenHexColor, mergeThemes, mixHexColors, normalizeColorToHex, parseAsLocalDate, parseHslString, prefersLightText, relativeLuminance, resolveCssVariable, useCalendarHeatmapData, useChartFormatting, useChartLegendItems, useChartRegistration, useChartScopeElement, useGlobalChartsContext, useGlobalChartsTheme, useLeaderboardLegendItems, useMonthCalendarHeatmapData, validateHexColor };
10778
11763
 
10779
11764
  //# sourceMappingURL=index.js.map