@automattic/charts 4.2.0 → 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 (45) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/index.cjs +470 -121
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +23 -2
  5. package/dist/index.d.ts +23 -2
  6. package/dist/index.js +471 -122
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/charts/area-chart/area-chart.tsx +42 -10
  10. package/src/charts/area-chart/private/validate-data.ts +2 -3
  11. package/src/charts/area-chart/test/area-chart.test.tsx +72 -1
  12. package/src/charts/bar-chart/bar-chart.tsx +100 -83
  13. package/src/charts/bar-chart/index.ts +1 -1
  14. package/src/charts/bar-chart/private/band-highlight.tsx +3 -1
  15. package/src/charts/bar-chart/private/band-tooltip.tsx +1 -1
  16. package/src/charts/bar-chart/private/index.ts +1 -0
  17. package/src/charts/bar-chart/private/test/band-highlight.test.tsx +28 -2
  18. package/src/charts/bar-chart/private/test/value-domain.test.ts +100 -0
  19. package/src/charts/bar-chart/private/use-bar-chart-options.ts +12 -39
  20. package/src/charts/bar-chart/private/value-domain.ts +90 -0
  21. package/src/charts/bar-chart/test/bar-chart.test.tsx +484 -9
  22. package/src/charts/bar-chart/types.ts +39 -0
  23. package/src/charts/line-chart/line-chart.tsx +148 -35
  24. package/src/charts/line-chart/private/index.ts +1 -0
  25. package/src/charts/line-chart/private/line-chart-glyph.tsx +17 -3
  26. package/src/charts/line-chart/private/nearest-pointer-events.tsx +155 -0
  27. package/src/charts/line-chart/test/line-chart.test.tsx +604 -9
  28. package/src/charts/line-chart/types.ts +2 -1
  29. package/src/charts/private/readings.ts +34 -0
  30. package/src/charts/private/test/readings.test.ts +29 -0
  31. package/src/charts/private/test/whole-number-ticks.test.tsx +57 -0
  32. package/src/charts/private/whole-number-ticks.tsx +79 -0
  33. package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +124 -0
  34. package/src/components/legend/hooks/use-chart-legend-items.ts +60 -2
  35. package/src/components/legend/private/base-legend.tsx +36 -21
  36. package/src/components/legend/test/legend.test.tsx +84 -0
  37. package/src/components/legend/types.ts +2 -0
  38. package/src/components/legend/utils/index.ts +0 -1
  39. package/src/components/tooltip/accessible-tooltip.tsx +1 -1
  40. package/src/hooks/test/use-chart-margin.test.tsx +35 -0
  41. package/src/hooks/use-chart-margin.tsx +7 -2
  42. package/src/providers/chart-context/global-charts-provider.tsx +1 -1
  43. package/src/providers/chart-context/test/chart-context.test.tsx +4 -4
  44. package/src/types.ts +8 -0
  45. package/src/components/legend/utils/label-transform-factory.ts +0 -24
package/dist/index.cjs CHANGED
@@ -46,6 +46,7 @@ let _visx_gradient = require("@visx/gradient");
46
46
  let _visx_curve = require("@visx/curve");
47
47
  let _visx_responsive = require("@visx/responsive");
48
48
  let _visx_annotation = require("@visx/annotation");
49
+ let _visx_vendor_d3_array = require("@visx/vendor/d3-array");
49
50
  let _visx_pattern = require("@visx/pattern");
50
51
  let react_google_charts = require("react-google-charts");
51
52
  let dompurify = require("dompurify");
@@ -1076,7 +1077,7 @@ const GlobalChartsProvider = ({ children, theme, locale, timeZone }) => {
1076
1077
  return getChartColor(index, colorCache);
1077
1078
  }, [colorCache, groupToColorMap]);
1078
1079
  const getElementStyles = (0, react.useCallback)(({ data, index, overrideColor, legendShape }) => {
1079
- const isSeriesData = data && typeof data === "object" && "data" in data && "options" in data;
1080
+ const isSeriesData = data && typeof data === "object" && "data" in data;
1080
1081
  const isPointPercentageData = data && typeof data === "object" && "value" in data && typeof data.value === "number" && !("data" in data);
1081
1082
  return {
1082
1083
  color: resolveColor({
@@ -1457,8 +1458,9 @@ const useChartMargin = (height, options, data, theme, horizontal = false) => {
1457
1458
  const allDataPoints = data.flatMap((series) => series.data);
1458
1459
  if (horizontal) return allDataPoints.map((d) => d.label || d.date?.getTime());
1459
1460
  if (options.axis?.y?.tickValues?.length) return options.axis.y.tickValues;
1460
- const minY = Math.min(...allDataPoints.map((d) => d.value));
1461
- const maxY = Math.max(...allDataPoints.map((d) => d.value));
1461
+ const values = allDataPoints.map((d) => d.value).filter((value) => typeof value === "number" && Number.isFinite(value));
1462
+ const minY = values.length ? Math.min(...values) : 0;
1463
+ const maxY = values.length ? Math.max(...values) : 1;
1462
1464
  const yScale = (0, _visx_scale.createScale)({
1463
1465
  ...options.yScale,
1464
1466
  domain: options.yScale?.domain ?? [minY, maxY],
@@ -1793,23 +1795,6 @@ function valueOrIdentityString(_) {
1793
1795
  return String(valueOrIdentity(_));
1794
1796
  }
1795
1797
  //#endregion
1796
- //#region src/components/legend/utils/label-transform-factory.ts
1797
- /**
1798
- * Returns a function which takes a Datum and index as input, and returns a formatted label object.
1799
- * @param {object} root0 - The object to return the value of.
1800
- * @param {AnyD3Scale} root0.scale - The scale to use.
1801
- * @param {LabelFormatter<ScaleInput<Scale>>} root0.labelFormat - The label format to use.
1802
- * @return {ItemTransformer<ScaleInput<Scale>, ReturnType<Scale>>} The label transform factory.
1803
- */
1804
- function labelTransformFactory({ scale, labelFormat }) {
1805
- return (d, i) => ({
1806
- datum: d,
1807
- index: i,
1808
- text: `${labelFormat(d, i)}`,
1809
- value: scale(d)
1810
- });
1811
- }
1812
- //#endregion
1813
1798
  //#region src/components/legend/private/base-legend.module.scss
1814
1799
  var base_legend_module_default = {
1815
1800
  "legend": "a8ccharts-04TogW-legend",
@@ -1851,7 +1836,7 @@ const getLegendItemAriaLabel = (text, value, visible, interactive) => {
1851
1836
  if (visible) return;
1852
1837
  return (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n._x)("%s: hidden", "hidden non-interactive legend item", "jetpack-charts"), accessibleText);
1853
1838
  };
1854
- const BaseLegend = (0, react.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) => {
1839
+ const BaseLegend = (0, react.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) => {
1855
1840
  const { margin: itemMargin = "0", flexDirection: itemDirection = "row" } = itemStyles ?? {};
1856
1841
  const { justifyContent: labelJustifyContent = "flex-start", flex: labelFlex = "0 0 auto", margin: labelMargin = "0 4px", maxWidth, textOverflow = "wrap" } = labelStyles ?? {};
1857
1842
  const { width: shapeWidth = 16, height: shapeHeight = 16, margin: shapeMargin = "2px 4px 2px 0" } = shapeStyles ?? {};
@@ -1859,11 +1844,17 @@ const BaseLegend = (0, react.forwardRef)(({ items, className, orientation = "hor
1859
1844
  const context = (0, react.useContext)(GlobalChartsContext);
1860
1845
  const chartInstanceContext = (0, react.useContext)(ChartInstanceContext);
1861
1846
  const standaloneScopeClass = useStandaloneScopeClass();
1847
+ const domain = items.map((item) => item.label);
1862
1848
  const legendScale = (0, _visx_scale.scaleOrdinal)({
1863
- domain: items.map((item) => item.label),
1849
+ domain,
1864
1850
  range: items.map((item) => item.color)
1865
1851
  });
1866
- const domain = legendScale.domain();
1852
+ const defaultLabelTransform = (0, react.useCallback)(({ labelFormat: format }) => (datum, index) => ({
1853
+ datum,
1854
+ index,
1855
+ text: String(format(datum, index)),
1856
+ value: items[index].color
1857
+ }), [items]);
1867
1858
  const getShapeStyle = (0, react.useCallback)(({ index }) => items[index]?.shapeStyle, [items]);
1868
1859
  const handleLegendClick = (0, react.useCallback)((seriesLabels) => {
1869
1860
  if (interactive && chartId && context) {
@@ -1884,24 +1875,26 @@ const BaseLegend = (0, react.forwardRef)(({ items, className, orientation = "hor
1884
1875
  chartInstanceContext,
1885
1876
  context
1886
1877
  ]);
1887
- const createClickHandler = (0, react.useCallback)((seriesLabels) => {
1888
- if (!interactive) return;
1878
+ const createClickHandler = (0, react.useCallback)((seriesLabels, itemInteractive) => {
1879
+ if (!itemInteractive) return;
1889
1880
  return () => handleLegendClick(seriesLabels);
1890
- }, [interactive, handleLegendClick]);
1891
- const createKeyDownHandler = (0, react.useCallback)((seriesLabels) => {
1892
- if (!interactive) return;
1881
+ }, [handleLegendClick]);
1882
+ const createKeyDownHandler = (0, react.useCallback)((seriesLabels, itemInteractive) => {
1883
+ if (!itemInteractive) return;
1893
1884
  return (event) => {
1894
1885
  if (event.key === "Enter" || event.key === " ") {
1895
1886
  event.preventDefault();
1896
1887
  handleLegendClick(seriesLabels);
1897
1888
  }
1898
1889
  };
1899
- }, [interactive, handleLegendClick]);
1890
+ }, [handleLegendClick]);
1900
1891
  const flexAlignment = ALIGNMENT_TO_FLEX[alignment] ?? "center";
1892
+ const staticItemRole = interactive ? void 0 : "listitem";
1901
1893
  return render ? render(items) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_legend.LegendOrdinal, {
1902
1894
  scale: legendScale,
1895
+ domain,
1903
1896
  labelFormat,
1904
- labelTransform,
1897
+ labelTransform: labelTransform ?? defaultLabelTransform,
1905
1898
  children: (labels) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_wordpress_ui.Stack, {
1906
1899
  ref,
1907
1900
  direction: orientation === "vertical" ? "column" : "row",
@@ -1915,19 +1908,20 @@ const BaseLegend = (0, react.forwardRef)(({ items, className, orientation = "hor
1915
1908
  children: labels.map((label, i) => {
1916
1909
  const matchedItem = items[i];
1917
1910
  const seriesLabels = matchedItem?.seriesLabels?.length ? matchedItem.seriesLabels : [label.text];
1918
- const visible = isSeriesVisible(seriesLabels[0]);
1919
- const handleClick = createClickHandler(seriesLabels);
1920
- const handleKeyDown = createKeyDownHandler(seriesLabels);
1911
+ const visible = matchedItem?.interactive === false || isSeriesVisible(seriesLabels[0]);
1912
+ const itemInteractive = interactive && matchedItem?.interactive !== false;
1913
+ const handleClick = createClickHandler(seriesLabels, itemInteractive);
1914
+ const handleKeyDown = createKeyDownHandler(seriesLabels, itemInteractive);
1921
1915
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_visx_legend.LegendItem, {
1922
- className: (0, clsx.default)("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),
1916
+ className: (0, clsx.default)("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),
1923
1917
  margin: itemMargin,
1924
1918
  flexDirection: orientation === "vertical" && alignment === "end" ? "row-reverse" : itemDirection,
1925
1919
  onClick: handleClick,
1926
1920
  onKeyDown: handleKeyDown,
1927
- role: interactive ? "button" : "listitem",
1928
- tabIndex: interactive ? 0 : void 0,
1929
- "aria-pressed": interactive ? visible : void 0,
1930
- "aria-label": getLegendItemAriaLabel(label.text, matchedItem?.value, visible, interactive),
1921
+ role: itemInteractive ? "button" : staticItemRole,
1922
+ tabIndex: itemInteractive ? 0 : void 0,
1923
+ "aria-pressed": itemInteractive ? visible : void 0,
1924
+ "aria-label": getLegendItemAriaLabel(label.text, matchedItem?.value, visible, itemInteractive),
1931
1925
  children: [items[i]?.renderGlyph ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1932
1926
  width: items[i]?.glyphSize * 2,
1933
1927
  height: items[i]?.glyphSize * 2,
@@ -2129,6 +2123,32 @@ function processSeriesData(seriesData, getElementStyles, showValues, withGlyph,
2129
2123
  });
2130
2124
  }
2131
2125
  /**
2126
+ * Builds the static item that names the comparison overlay, styled like its first series.
2127
+ * Skipped when a comparison series already has an item of its own, so nothing is listed twice.
2128
+ * @param seriesData - The series data to search for a comparison series
2129
+ * @param items - The legend items already built from the series
2130
+ * @param label - The item label
2131
+ * @param getElementStyles - Function to get element styles
2132
+ * @param legendShape - The shape type for legend items (string literal or React component)
2133
+ * @return The legend item, or null when it would add nothing
2134
+ */
2135
+ function buildComparisonLegendItem(seriesData, items, label, getElementStyles, legendShape) {
2136
+ const itemLabels = new Set(items.map((item) => item.label));
2137
+ const index = seriesData.findIndex((series) => series.options?.type === "comparison" && !itemLabels.has(series.label));
2138
+ if (index === -1) return null;
2139
+ const { color, shapeStyles } = getElementStyles({
2140
+ data: seriesData[index],
2141
+ index,
2142
+ legendShape
2143
+ });
2144
+ return {
2145
+ label,
2146
+ color,
2147
+ shapeStyle: shapeStyles,
2148
+ interactive: false
2149
+ };
2150
+ }
2151
+ /**
2132
2152
  * Processes point data into legend items
2133
2153
  * @param pointData - The point data to process
2134
2154
  * @param getElementStyles - Function to get element styles
@@ -2164,11 +2184,16 @@ function processPointData(pointData, getElementStyles, showValues, legendValueDi
2164
2184
  * @return Array of legend items ready for display
2165
2185
  */
2166
2186
  function useChartLegendItems(data, options = {}, legendShape) {
2167
- const { showValues = false, legendValueDisplay = "percentage", withGlyph = false, glyphSize = 8, collapseGroups = false, renderGlyph } = options;
2187
+ const { showValues = false, legendValueDisplay = "percentage", withGlyph = false, glyphSize = 8, collapseGroups = false, comparisonItem = false, renderGlyph } = options;
2168
2188
  const { getElementStyles } = useGlobalChartsContext();
2169
2189
  return (0, react.useMemo)(() => {
2170
2190
  if (!data || !Array.isArray(data) || data.length === 0) return [];
2171
- if ("data" in data[0]) return processSeriesData(data, getElementStyles, showValues, withGlyph, glyphSize, collapseGroups, renderGlyph, legendShape);
2191
+ if ("data" in data[0]) {
2192
+ const seriesData = data;
2193
+ const items = processSeriesData(seriesData, getElementStyles, showValues, withGlyph, glyphSize, collapseGroups, renderGlyph, legendShape);
2194
+ const comparison = comparisonItem ? buildComparisonLegendItem(seriesData, items, typeof comparisonItem === "string" ? comparisonItem : (0, _wordpress_i18n.__)("Comparison period", "jetpack-charts"), getElementStyles, legendShape) : null;
2195
+ return comparison ? [...items, comparison] : items;
2196
+ }
2172
2197
  return processPointData(data, getElementStyles, showValues, legendValueDisplay, withGlyph, glyphSize, renderGlyph, legendShape);
2173
2198
  }, [
2174
2199
  data,
@@ -2178,6 +2203,7 @@ function useChartLegendItems(data, options = {}, legendShape) {
2178
2203
  withGlyph,
2179
2204
  glyphSize,
2180
2205
  collapseGroups,
2206
+ comparisonItem,
2181
2207
  renderGlyph,
2182
2208
  legendShape
2183
2209
  ]);
@@ -2641,7 +2667,7 @@ const AccessibleTooltip = ({ renderTooltip, selectedIndex, tooltipRef, keyboardF
2641
2667
  tooltipContext?.showTooltip({
2642
2668
  datum: tooltipItem.datum,
2643
2669
  key: tooltipItem.seriesLabel,
2644
- index: tooltipItem.seriesIndex
2670
+ index: tooltipItem.dataPointIndex
2645
2671
  });
2646
2672
  }
2647
2673
  }
@@ -2964,6 +2990,31 @@ const DefaultGlyph = (props) => {
2964
2990
  });
2965
2991
  };
2966
2992
  //#endregion
2993
+ //#region src/charts/private/readings.ts
2994
+ /**
2995
+ * Whether a value is a reading a scale can place.
2996
+ *
2997
+ * @param value - The point's value.
2998
+ * @return True for a finite number.
2999
+ */
3000
+ const isReading = (value) => typeof value === "number" && Number.isFinite(value);
3001
+ /**
3002
+ * Whether a data point's value cannot be drawn.
3003
+ *
3004
+ * @param value - The point's value.
3005
+ * @param options - Validation options.
3006
+ * @param options.allowMissing - Whether `null` is a bucket with no reading rather than a fault.
3007
+ * @return True when the value is invalid.
3008
+ */
3009
+ const isInvalidReading = (value, { allowMissing }) => value === null ? !allowMissing : value === void 0 || isNaN(value);
3010
+ /**
3011
+ * Formats a reading for a tooltip.
3012
+ *
3013
+ * @param value - The reading, or null when the bucket has none.
3014
+ * @return The formatted value.
3015
+ */
3016
+ const formatReading = (value) => value == null ? (0, _wordpress_i18n.__)("No data", "jetpack-charts") : (0, _automattic_number_formatters.formatNumber)(value);
3017
+ //#endregion
2967
3018
  //#region src/charts/private/center/center.module.scss
2968
3019
  var center_module_default = { "center": "a8ccharts-w3qxlG-center" };
2969
3020
  //#endregion
@@ -3336,6 +3387,54 @@ const buildTimeAxisOptions = ({ dataSorted, width, axisOptions, scaleDomain, zoo
3336
3387
  };
3337
3388
  };
3338
3389
  //#endregion
3390
+ //#region src/charts/private/whole-number-ticks.tsx
3391
+ /**
3392
+ * Whether every reading in the series is a whole number.
3393
+ *
3394
+ * @param series - The series visx renders.
3395
+ * @return True when at least one reading exists and none has a fraction.
3396
+ */
3397
+ const hasOnlyWholeNumbers = (series) => {
3398
+ let sawReading = false;
3399
+ for (const { data } of series) for (const point of data) {
3400
+ const value = point?.value;
3401
+ if (!isReading(value)) continue;
3402
+ if (!Number.isInteger(value)) return false;
3403
+ sawReading = true;
3404
+ }
3405
+ return sawReading;
3406
+ };
3407
+ /**
3408
+ * The scale's ticks with the fractional ones dropped.
3409
+ *
3410
+ * d3 steps by 1, 2 or 5 times a power of ten, so every whole number inside the domain is one of its ticks.
3411
+ *
3412
+ * @param scale - The value scale visx built.
3413
+ * @param count - Ticks to ask the scale for; undefined uses the scale's own default, as visx does.
3414
+ * @return Whole-number ticks, or undefined when visx's own ticks need no change.
3415
+ */
3416
+ const getWholeNumberTickValues = (scale, count) => {
3417
+ const ticks = scale?.ticks?.(count);
3418
+ if (!ticks?.every((tick) => typeof tick === "number")) return;
3419
+ const whole = ticks.filter((tick) => Number.isInteger(tick));
3420
+ return whole.length > 1 && whole.length < ticks.length ? whole : void 0;
3421
+ };
3422
+ /**
3423
+ * Hands its children the whole-number ticks of the chart's value scale.
3424
+ *
3425
+ * @param props - Component props.
3426
+ * @param props.axis - Which scale carries the values.
3427
+ * @param props.numTicks - Ticks to ask the scale for.
3428
+ * @param props.enabled - Whether the plotted values are all whole numbers and the caller pinned neither the ticks nor the value domain.
3429
+ * @param props.children - Renders the axis and grid with the tick values.
3430
+ * @return The rendered children.
3431
+ */
3432
+ const WholeNumberTicks = ({ axis, numTicks, enabled, children }) => {
3433
+ const { xScale, yScale } = (0, react.useContext)(_visx_xychart.DataContext);
3434
+ const tickValues = enabled ? getWholeNumberTickValues(axis === "x" ? xScale : yScale, numTicks) : void 0;
3435
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: children(tickValues) });
3436
+ };
3437
+ //#endregion
3339
3438
  //#region src/charts/private/with-responsive/with-responsive.module.scss
3340
3439
  var with_responsive_module_default = {
3341
3440
  "container": "a8ccharts-sP1gHa-container",
@@ -4002,11 +4101,15 @@ const toNumber$1 = (val) => {
4002
4101
  const LineChartGlyph = ({ data, index, color, glyphStyle, renderGlyph, accessors, position }) => {
4003
4102
  const { xScale, yScale } = (0, react.useContext)(_visx_xychart.DataContext) || {};
4004
4103
  if (!xScale || !yScale) return null;
4005
- if (data.data.length === 0) return null;
4006
- const point = position === "start" ? data.data[0] : data.data[data.data.length - 1];
4104
+ const hasFiniteY = (datum) => {
4105
+ const scaledY = yScale(accessors.yAccessor(datum));
4106
+ return typeof scaledY === "number" && Number.isFinite(scaledY);
4107
+ };
4108
+ const point = position === "start" ? data.data.find(hasFiniteY) : data.data.findLast(hasFiniteY);
4109
+ if (!point) return null;
4007
4110
  const x = xScale(accessors.xAccessor(point));
4008
4111
  const y = yScale(accessors.yAccessor(point));
4009
- if (typeof x !== "number" || typeof y !== "number") return null;
4112
+ if (typeof x !== "number" || typeof y !== "number" || !Number.isFinite(x) || !Number.isFinite(y)) return null;
4010
4113
  const size = Math.max(0, toNumber$1(glyphStyle?.radius) ?? 4);
4011
4114
  return renderGlyph({
4012
4115
  key: `${position}-glyph-${data.label}`,
@@ -4021,6 +4124,106 @@ const LineChartGlyph = ({ data, index, color, glyphStyle, renderGlyph, accessors
4021
4124
  });
4022
4125
  };
4023
4126
  //#endregion
4127
+ //#region src/charts/line-chart/private/nearest-pointer-events.tsx
4128
+ /**
4129
+ * Find the datum nearest a pointer along x, in one series sorted by x.
4130
+ *
4131
+ * @param data - The series' registered data, in ascending x order.
4132
+ * @param xAccessor - Reads a datum's x value.
4133
+ * @param xScale - The chart's x scale.
4134
+ * @param x - Pointer x in SVG coordinates.
4135
+ * @return Index of the nearest datum and its distance, or index -1 when none has a position.
4136
+ */
4137
+ function nearestByX(data, xAccessor, xScale, x) {
4138
+ return (typeof xScale.invert === "function" ? [(0, _visx_vendor_d3_array.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) => {
4139
+ const distance = Math.abs(Number(xScale(xAccessor(data[index]))) - x);
4140
+ return distance < best.distance ? {
4141
+ index,
4142
+ distance
4143
+ } : best;
4144
+ }, {
4145
+ index: -1,
4146
+ distance: Infinity
4147
+ });
4148
+ }
4149
+ /**
4150
+ * Pick the candidate to report: the closest placed reading, else the closest bucket with no reading along x.
4151
+ *
4152
+ * @param candidates - One candidate per series.
4153
+ * @return The chosen candidate's params, or undefined when there are none.
4154
+ */
4155
+ function pickNearest(candidates) {
4156
+ const placed = candidates.map(({ params }) => params).filter((params) => Number.isFinite(params.distanceY));
4157
+ const missing = candidates.filter(({ isMissing }) => isMissing).map(({ params }) => params);
4158
+ const distance = (params) => placed.length ? Math.hypot(params.distanceX ?? 0, params.distanceY ?? 0) : params.distanceX ?? 0;
4159
+ return (placed.length ? placed : missing).reduce((best, params) => !best || distance(params) < distance(best) ? params : best, void 0);
4160
+ }
4161
+ /**
4162
+ * Report pointer events at the nearest datum, including a bucket with no reading.
4163
+ *
4164
+ * visx's own nearest search measures y distance too, which is NaN for a null value, so a pointer
4165
+ * over a bucket where no series has a reading fires no event at all.
4166
+ *
4167
+ * @param props - Handlers to call.
4168
+ * @param props.onPointerDown - Receives the nearest datum on pointer down.
4169
+ * @param props.onPointerMove - Receives the nearest datum on pointer move.
4170
+ * @param props.onPointerUp - Receives the nearest datum on pointer up.
4171
+ * @return No visual content.
4172
+ */
4173
+ function NearestPointerEvents({ onPointerDown, onPointerMove, onPointerUp }) {
4174
+ const { xScale, yScale, dataRegistry } = (0, react.useContext)(_visx_xychart.DataContext);
4175
+ const findNearest = (0, react.useCallback)((params) => {
4176
+ const point = params?.svgPoint;
4177
+ if (!point || !xScale || !yScale || !dataRegistry) return;
4178
+ const candidates = [];
4179
+ for (const key of dataRegistry.keys()) {
4180
+ const entry = dataRegistry.get(key);
4181
+ if (!entry) continue;
4182
+ const { index, distance } = nearestByX(entry.data, entry.xAccessor, xScale, point.x);
4183
+ if (index < 0) continue;
4184
+ const datum = entry.data[index];
4185
+ const value = entry.yAccessor(datum);
4186
+ candidates.push({
4187
+ params: {
4188
+ event: params.event,
4189
+ svgPoint: point,
4190
+ key,
4191
+ datum,
4192
+ index,
4193
+ distanceX: distance,
4194
+ distanceY: Math.abs(Number(yScale(value)) - point.y)
4195
+ },
4196
+ isMissing: value == null
4197
+ });
4198
+ }
4199
+ return pickNearest(candidates);
4200
+ }, [
4201
+ xScale,
4202
+ yScale,
4203
+ dataRegistry
4204
+ ]);
4205
+ const handlers = (0, react.useMemo)(() => {
4206
+ const report = (handler) => handler && ((params) => {
4207
+ const nearest = findNearest(params);
4208
+ if (nearest) handler(nearest);
4209
+ });
4210
+ return {
4211
+ pointerdown: report(onPointerDown),
4212
+ pointermove: report(onPointerMove),
4213
+ pointerup: report(onPointerUp)
4214
+ };
4215
+ }, [
4216
+ findNearest,
4217
+ onPointerDown,
4218
+ onPointerMove,
4219
+ onPointerUp
4220
+ ]);
4221
+ (0, _visx_xychart.useEventEmitter)("pointerdown", handlers.pointerdown);
4222
+ (0, _visx_xychart.useEventEmitter)("pointermove", handlers.pointermove);
4223
+ (0, _visx_xychart.useEventEmitter)("pointerup", handlers.pointerup);
4224
+ return null;
4225
+ }
4226
+ //#endregion
4024
4227
  //#region src/charts/line-chart/line-chart.tsx
4025
4228
  const defaultRenderGlyph = (props) => {
4026
4229
  return /* @__PURE__ */ (0, react.createElement)(DefaultGlyph, {
@@ -4064,8 +4267,13 @@ const renderDefaultTooltip = (params, contentStyle) => {
4064
4267
  if (!nearestDatum) return null;
4065
4268
  const tooltipPoints = Object.entries(tooltipData?.datumByKey || {}).map(([key, { datum }]) => ({
4066
4269
  key,
4067
- value: datum.value
4068
- })).sort((a, b) => b.value - a.value);
4270
+ value: datum.value ?? null
4271
+ })).sort((a, b) => {
4272
+ if (a.value === null && b.value === null) return 0;
4273
+ if (a.value === null) return 1;
4274
+ if (b.value === null) return -1;
4275
+ return b.value - a.value;
4276
+ });
4069
4277
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4070
4278
  className: line_chart_module_default["line-chart__tooltip"],
4071
4279
  style: contentStyle,
@@ -4085,16 +4293,23 @@ const renderDefaultTooltip = (params, contentStyle) => {
4085
4293
  children: [point.key, ":"]
4086
4294
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4087
4295
  className: line_chart_module_default["line-chart__tooltip-value"],
4088
- children: (0, _automattic_number_formatters.formatNumber)(point.value)
4296
+ children: formatReading(point.value)
4089
4297
  })]
4090
4298
  }, point.key))]
4091
4299
  });
4092
4300
  };
4093
4301
  const validateData$4 = (data) => {
4094
- if (!data?.length) return "No data available";
4095
- 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";
4302
+ if (!data?.length) return (0, _wordpress_i18n.__)("No data available", "jetpack-charts");
4303
+ if (data.some((series) => series.data.some((point) => isInvalidReading(point.value, { allowMissing: true }) || "date" in point && point.date && isNaN(point.date.getTime())))) return (0, _wordpress_i18n.__)("Invalid data", "jetpack-charts");
4096
4304
  return null;
4097
4305
  };
4306
+ const getFallbackYDomain = (readingExtent, isLogScale) => {
4307
+ const emptyDomain = isLogScale ? [1, 10] : [0, 1];
4308
+ if (!readingExtent) return emptyDomain;
4309
+ const [min, max] = readingExtent;
4310
+ if (min !== max || isLogScale) return;
4311
+ return min === 0 ? emptyDomain : [Math.min(0, min), Math.max(0, max)];
4312
+ };
4098
4313
  const LineChartScalesRef = ({ chartRef, width, height, margin }) => {
4099
4314
  const context = (0, react.useContext)(_visx_xychart.DataContext);
4100
4315
  (0, react.useImperativeHandle)(chartRef, () => ({
@@ -4121,6 +4336,7 @@ const LineChartScalesRef = ({ chartRef, width, height, margin }) => {
4121
4336
  const LineChartInternal = (0, react.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) => {
4122
4337
  const legendInteractive = legend.interactive ?? false;
4123
4338
  const legendCollapseGroups = legend.collapseGroups ?? false;
4339
+ const legendComparisonItem = legend.comparisonItem ?? false;
4124
4340
  const legendShape = legend.shape ?? "line";
4125
4341
  const legendPosition = legend.position ?? "bottom";
4126
4342
  const formatting = useChartFormatting();
@@ -4167,19 +4383,32 @@ const LineChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
4167
4383
  const allSeriesHidden = (0, react.useMemo)(() => {
4168
4384
  return seriesWithVisibility.every(({ isVisible }) => !isVisible);
4169
4385
  }, [seriesWithVisibility]);
4170
- const stableYDomain = (0, react.useMemo)(() => {
4171
- if (rescaleYOnVisibilityChange) return;
4386
+ const yDomain = (0, react.useMemo)(() => {
4387
+ const includeZero = options?.yScale?.zero === true && (0, _visx_scale.scaleCanBeZeroed)({ type: options.yScale.type ?? "linear" });
4388
+ if (rescaleYOnVisibilityChange && !includeZero) return;
4172
4389
  let min = Infinity;
4173
4390
  let max = -Infinity;
4174
- for (const series of dataSorted) for (const point of series.data ?? []) {
4175
- const value = point?.value;
4176
- if (typeof value === "number" && Number.isFinite(value)) {
4177
- min = Math.min(min, value);
4178
- max = Math.max(max, value);
4391
+ for (const series of dataSorted) {
4392
+ if (rescaleYOnVisibilityChange && !isSeriesVisible(series.label)) continue;
4393
+ for (const point of series.data ?? []) {
4394
+ const value = point?.value;
4395
+ if (isReading(value)) {
4396
+ min = Math.min(min, value);
4397
+ max = Math.max(max, value);
4398
+ }
4179
4399
  }
4180
4400
  }
4401
+ if (includeZero) {
4402
+ if (min === Infinity) return;
4403
+ return max === 0 && min === 0 ? [0, 1] : [Math.min(0, min), Math.max(0, max)];
4404
+ }
4181
4405
  return min < max ? [min, max] : void 0;
4182
- }, [rescaleYOnVisibilityChange, dataSorted]);
4406
+ }, [
4407
+ rescaleYOnVisibilityChange,
4408
+ dataSorted,
4409
+ isSeriesVisible,
4410
+ options?.yScale
4411
+ ]);
4183
4412
  const activateSelectedPoint = (0, react.useCallback)((index) => {
4184
4413
  const series = dataSorted[0];
4185
4414
  const datum = series?.data[index];
@@ -4199,7 +4428,24 @@ const LineChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
4199
4428
  onActivate: activateSelectedPoint,
4200
4429
  preventTooltipScroll: tooltipPlacement === "below-axis"
4201
4430
  });
4431
+ const visibleReadingExtent = (0, react.useMemo)(() => {
4432
+ let min = Infinity;
4433
+ let max = -Infinity;
4434
+ for (const series of dataSorted) {
4435
+ if (!isSeriesVisible(series.label)) continue;
4436
+ for (const point of series.data) {
4437
+ const value = point?.value;
4438
+ if (isReading(value)) {
4439
+ min = Math.min(min, value);
4440
+ max = Math.max(max, value);
4441
+ }
4442
+ }
4443
+ }
4444
+ return min <= max ? [min, max] : void 0;
4445
+ }, [dataSorted, isSeriesVisible]);
4446
+ const hasWholeNumberValues = (0, react.useMemo)(() => hasOnlyWholeNumbers(dataSorted.filter((series) => isSeriesVisible(series.label))), [dataSorted, isSeriesVisible]);
4202
4447
  const chartOptions = (0, react.useMemo)(() => {
4448
+ const fallbackYDomain = getFallbackYDomain(visibleReadingExtent, options?.yScale?.type === "log");
4203
4449
  return {
4204
4450
  axis: {
4205
4451
  x: buildTimeAxisOptions({
@@ -4228,7 +4474,8 @@ const LineChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
4228
4474
  type: "linear",
4229
4475
  nice: true,
4230
4476
  zero: false,
4231
- ...stableYDomain ? { domain: stableYDomain } : {},
4477
+ ...fallbackYDomain ? { domain: fallbackYDomain } : {},
4478
+ ...yDomain ? { domain: yDomain } : {},
4232
4479
  ...options?.yScale
4233
4480
  }
4234
4481
  };
@@ -4237,7 +4484,8 @@ const LineChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
4237
4484
  dataSorted,
4238
4485
  width,
4239
4486
  zoom.domain,
4240
- stableYDomain,
4487
+ yDomain,
4488
+ visibleReadingExtent,
4241
4489
  formatting,
4242
4490
  isSeriesVisible
4243
4491
  ]);
@@ -4272,11 +4520,13 @@ const LineChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
4272
4520
  withGlyph: withLegendGlyph,
4273
4521
  glyphSize: Math.max(0, toNumber(glyphStyle?.radius) ?? 4),
4274
4522
  collapseGroups: legendCollapseGroups,
4523
+ comparisonItem: legendComparisonItem,
4275
4524
  renderGlyph
4276
4525
  }), [
4277
4526
  withLegendGlyph,
4278
4527
  glyphStyle?.radius,
4279
4528
  legendCollapseGroups,
4529
+ legendComparisonItem,
4280
4530
  renderGlyph
4281
4531
  ]), legendShape);
4282
4532
  const chartMetadata = (0, react.useMemo)(() => ({
@@ -4388,18 +4638,26 @@ const LineChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
4388
4638
  },
4389
4639
  xScale: chartOptions.xScale,
4390
4640
  yScale: chartOptions.yScale,
4391
- onPointerDown: zoom.handlers.onPointerDown,
4392
- onPointerUp: zoom.handlers.onPointerUp,
4393
- onPointerMove: zoom.handlers.onPointerMove,
4394
4641
  onPointerOut,
4395
- pointerEventsDataKey: "nearest",
4396
4642
  children: [
4397
- !allSeriesHidden && gridVisibility !== "none" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Grid, {
4398
- columns: false,
4399
- numTicks: 4
4643
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NearestPointerEvents, { ...zoom.handlers }),
4644
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WholeNumberTicks, {
4645
+ axis: "y",
4646
+ numTicks: chartOptions.axis.y.numTicks,
4647
+ enabled: hasWholeNumberValues && !chartOptions.axis.y.tickValues && !options?.yScale?.domain,
4648
+ children: (tickValues) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4649
+ !allSeriesHidden && gridVisibility !== "none" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Grid, {
4650
+ columns: false,
4651
+ numTicks: chartOptions.axis.y.numTicks,
4652
+ tickValues: tickValues ?? chartOptions.axis.y.tickValues
4653
+ }),
4654
+ !allSeriesHidden && chartOptions.axis.x.display && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, { ...chartOptions.axis.x }),
4655
+ !allSeriesHidden && chartOptions.axis.y.display && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, {
4656
+ ...chartOptions.axis.y,
4657
+ ...tickValues ? { tickValues } : {}
4658
+ })
4659
+ ] })
4400
4660
  }),
4401
- !allSeriesHidden && chartOptions.axis.x.display && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, { ...chartOptions.axis.x }),
4402
- !allSeriesHidden && chartOptions.axis.y.display && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, { ...chartOptions.axis.y }),
4403
4661
  allSeriesHidden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SvgEmptyState, {
4404
4662
  x: width / 2,
4405
4663
  y: chartHeight / 2,
@@ -4540,7 +4798,7 @@ var area_chart_module_default = {
4540
4798
  const validateData$3 = (data) => {
4541
4799
  if (!data?.length) return (0, _wordpress_i18n.__)("No data available", "jetpack-charts");
4542
4800
  if (data.some((series) => !series.data?.length)) return (0, _wordpress_i18n.__)("No data available", "jetpack-charts");
4543
- 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 (0, _wordpress_i18n.__)("Invalid data", "jetpack-charts");
4801
+ if (data.some((series) => series.data.some((point) => isInvalidReading(point.value, { allowMissing: false }) || "date" in point && point.date && isNaN(point.date.getTime())))) return (0, _wordpress_i18n.__)("Invalid data", "jetpack-charts");
4544
4802
  return null;
4545
4803
  };
4546
4804
  //#endregion
@@ -4656,6 +4914,12 @@ const AreaChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
4656
4914
  }));
4657
4915
  }, [dataSorted, hiddenSeries]);
4658
4916
  const allSeriesHidden = (0, react.useMemo)(() => seriesWithVisibility.every(({ isVisible }) => !isVisible), [seriesWithVisibility]);
4917
+ const hasWholeNumberValues = (0, react.useMemo)(() => (!stacked || stackOffset === "none") && hasOnlyWholeNumbers(dataSorted.filter((series) => isSeriesVisible(series.label))), [
4918
+ dataSorted,
4919
+ isSeriesVisible,
4920
+ stacked,
4921
+ stackOffset
4922
+ ]);
4659
4923
  const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation({
4660
4924
  selectedIndex,
4661
4925
  setSelectedIndex,
@@ -4748,8 +5012,9 @@ const AreaChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
4748
5012
  const legendItems = useChartLegendItems(dataSorted, (0, react.useMemo)(() => ({
4749
5013
  withGlyph: false,
4750
5014
  glyphSize: 0,
4751
- collapseGroups: legend.collapseGroups ?? false
4752
- }), [legend.collapseGroups]), legendShape);
5015
+ collapseGroups: legend.collapseGroups ?? false,
5016
+ comparisonItem: legend.comparisonItem ?? false
5017
+ }), [legend.collapseGroups, legend.comparisonItem]), legendShape);
4753
5018
  const chartMetadata = (0, react.useMemo)(() => ({
4754
5019
  stacked,
4755
5020
  stackOffset,
@@ -4899,12 +5164,23 @@ const AreaChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
4899
5164
  onPointerOut,
4900
5165
  pointerEventsDataKey: "nearest",
4901
5166
  children: [
4902
- !allSeriesHidden && gridVisibility !== "none" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Grid, {
4903
- columns: false,
4904
- numTicks: 4
5167
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WholeNumberTicks, {
5168
+ axis: "y",
5169
+ numTicks: chartOptions.axis.y.numTicks,
5170
+ enabled: hasWholeNumberValues && !chartOptions.axis.y.tickValues && !options?.yScale?.domain,
5171
+ children: (tickValues) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
5172
+ !allSeriesHidden && gridVisibility !== "none" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Grid, {
5173
+ columns: false,
5174
+ numTicks: chartOptions.axis.y.numTicks,
5175
+ tickValues: tickValues ?? chartOptions.axis.y.tickValues
5176
+ }),
5177
+ !allSeriesHidden && chartOptions.axis.x.display && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, { ...chartOptions.axis.x }),
5178
+ !allSeriesHidden && chartOptions.axis.y.display && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, {
5179
+ ...chartOptions.axis.y,
5180
+ ...tickValues ? { tickValues } : {}
5181
+ })
5182
+ ] })
4905
5183
  }),
4906
- !allSeriesHidden && chartOptions.axis.x.display && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, { ...chartOptions.axis.x }),
4907
- !allSeriesHidden && chartOptions.axis.y.display && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, { ...chartOptions.axis.y }),
4908
5184
  allSeriesHidden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SvgEmptyState, {
4909
5185
  x: width / 2,
4910
5186
  y: chartHeight / 2,
@@ -5093,6 +5369,65 @@ const createTruncatedTickComponent = (axis) => (props) => {
5093
5369
  const TruncatedXTickComponent = createTruncatedTickComponent("x");
5094
5370
  const TruncatedYTickComponent = createTruncatedTickComponent("y");
5095
5371
  //#endregion
5372
+ //#region src/charts/bar-chart/private/value-domain.ts
5373
+ /** Domain for a value axis that has nothing to scale, so its ticks still read 0 to 1. */
5374
+ const EMPTY_DOMAIN = [0, 1];
5375
+ /**
5376
+ * The value a bar is drawn at, preferring `visualValue` so a zero still shows a sliver.
5377
+ *
5378
+ * A missing reading is NaN, not null: `ComparisonBars` coerces the value with `Number()`,
5379
+ * which turns null into 0 and would draw a comparison shadow at the baseline.
5380
+ *
5381
+ * @param point - A data point handed to visx.
5382
+ * @return The bar value, or NaN when the bucket has no reading.
5383
+ */
5384
+ const getBarValue = (point) => {
5385
+ const enhanced = point;
5386
+ return (enhanced?.visualValue !== void 0 ? enhanced.visualValue : point?.value) ?? NaN;
5387
+ };
5388
+ /**
5389
+ * Whether visx renders a bar for a point.
5390
+ *
5391
+ * @param point - A data point handed to visx.
5392
+ * @return True when the point has a finite bar value.
5393
+ */
5394
+ const isBarRendered = (point) => Number.isFinite(getBarValue(point));
5395
+ /**
5396
+ * How many bars visx renders for a list of points.
5397
+ *
5398
+ * @param points - Data points handed to visx.
5399
+ * @return The number of points that get a bar.
5400
+ */
5401
+ const countRenderedBars = (points) => points.filter(isBarRendered).length;
5402
+ /**
5403
+ * An explicit domain for the value scale, or null to let visx fit one to the data.
5404
+ *
5405
+ * Zero goes into the domain here rather than through the scale's `zero` flag: visx applies
5406
+ * `nice` before `zero`, which would leave the axis topping out at a raw maximum.
5407
+ *
5408
+ * @param data - Every series handed to the chart.
5409
+ * @param includeZero - Whether the domain must span zero.
5410
+ * @param isSeriesRendered - Whether visx mounts a series, i.e. the legend shows it.
5411
+ * @return Value domain, or null when visx should fit its own.
5412
+ */
5413
+ const getValueScaleDomain = (data, includeZero, isSeriesRendered) => {
5414
+ let min = Infinity;
5415
+ let max = -Infinity;
5416
+ for (const series of data) {
5417
+ if (!isSeriesRendered(series)) continue;
5418
+ for (const point of series.data) {
5419
+ const value = getBarValue(point);
5420
+ if (!Number.isFinite(value)) continue;
5421
+ min = Math.min(min, value);
5422
+ max = Math.max(max, value);
5423
+ }
5424
+ }
5425
+ if (min === Infinity) return [...EMPTY_DOMAIN];
5426
+ if (min === max) return min === 0 ? [...EMPTY_DOMAIN] : [Math.min(0, min), Math.max(0, max)];
5427
+ if (includeZero) return [Math.min(0, min), Math.max(0, max)];
5428
+ return null;
5429
+ };
5430
+ //#endregion
5096
5431
  //#region src/charts/bar-chart/private/use-bar-chart-options.ts
5097
5432
  /** Outer padding of the category band scale (space at the chart edges). */
5098
5433
  const BASE_BAND_PADDING = .2;
@@ -5217,10 +5552,6 @@ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = A
5217
5552
  const tooltipDatumFormatter = hasLabels ? labelFormatter : byBucket(getTooltipFormatter(data, tickResolution, formatting));
5218
5553
  const valueFormatter = _automattic_number_formatters.formatNumberCompact;
5219
5554
  const bandDomain = timeTickFormatter ? getBandDomain(data, isSeriesRendered) : null;
5220
- const valueAccessor = (d) => {
5221
- const enhancedPoint = d;
5222
- return enhancedPoint?.visualValue !== void 0 ? enhancedPoint.visualValue : d?.value;
5223
- };
5224
5555
  return {
5225
5556
  timeAxis: bandDomain && timeTickFormatter && {
5226
5557
  domain: bandDomain,
@@ -5231,7 +5562,7 @@ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = A
5231
5562
  yTickFormat: valueFormatter,
5232
5563
  tooltipLabelFormatter: tooltipDatumFormatter,
5233
5564
  xAccessor: bucketAccessor,
5234
- yAccessor: valueAccessor,
5565
+ yAccessor: getBarValue,
5235
5566
  gridVisibility: "x",
5236
5567
  xScale: bandScale,
5237
5568
  yScale: linearScale
@@ -5240,7 +5571,7 @@ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = A
5240
5571
  xTickFormat: valueFormatter,
5241
5572
  yTickFormat: labelFormatter,
5242
5573
  tooltipLabelFormatter: tooltipDatumFormatter,
5243
- xAccessor: valueAccessor,
5574
+ xAccessor: getBarValue,
5244
5575
  yAccessor: bucketAccessor,
5245
5576
  gridVisibility: "y",
5246
5577
  xScale: linearScale,
@@ -5255,20 +5586,10 @@ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = A
5255
5586
  ]);
5256
5587
  return (0, react.useMemo)(() => {
5257
5588
  const { xTickFormat: defaultXTickFormat, yTickFormat: defaultYTickFormat, tooltipLabelFormatter: defaultTooltipLabelFormatter, xAccessor, yAccessor, gridVisibility, xScale: baseXScale, yScale: baseYScale } = defaultOptions[horizontal ? "horizontal" : "vertical"];
5258
- let valueScaleDomainOverride = {};
5259
- if (data.some((s) => s.options?.type === "comparison")) {
5260
- if (!(!horizontal ? stableOptions.yScale?.domain : stableOptions.xScale?.domain)) {
5261
- const allValues = [];
5262
- data.forEach((series) => {
5263
- series.data.forEach((d) => {
5264
- const enhanced = d;
5265
- const v = enhanced.visualValue !== void 0 ? enhanced.visualValue : d.value;
5266
- if (typeof v === "number" && Number.isFinite(v)) allValues.push(v);
5267
- });
5268
- });
5269
- if (allValues.length > 0) valueScaleDomainOverride = { domain: [Math.min(0, ...allValues), Math.max(0, ...allValues)] };
5270
- }
5271
- }
5589
+ const valueScaleOptions = horizontal ? stableOptions.xScale : stableOptions.yScale;
5590
+ const includeZero = data.some((s) => s.options?.type === "comparison") || valueScaleOptions?.zero !== false;
5591
+ const domain = valueScaleOptions?.domain ? null : getValueScaleDomain(data, includeZero, isSeriesRendered);
5592
+ const valueScaleDomainOverride = domain ? { domain } : {};
5272
5593
  const xScale = {
5273
5594
  ...baseXScale,
5274
5595
  ...stableOptions.xScale || {},
@@ -5319,7 +5640,8 @@ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = A
5319
5640
  axisConfig,
5320
5641
  stableOptions,
5321
5642
  horizontal,
5322
- data
5643
+ data,
5644
+ isSeriesRendered
5323
5645
  ]);
5324
5646
  }
5325
5647
  //#endregion
@@ -5484,6 +5806,8 @@ function BandHighlight({ visible, horizontal, onChange }) {
5484
5806
  if (position === void 0 || !Number.isFinite(position)) return null;
5485
5807
  return {
5486
5808
  datum: nearest.datum,
5809
+ key: nearest.key,
5810
+ index: nearest.index,
5487
5811
  x: horizontal ? margin?.left ?? 0 : position,
5488
5812
  y: horizontal ? position : margin?.top ?? 0,
5489
5813
  width: horizontal ? innerWidth : scale.bandwidth(),
@@ -5620,8 +5944,8 @@ function BandTooltip({ keys, groupPadding, withTooltips, onPointerDown, onPointe
5620
5944
  //#endregion
5621
5945
  //#region src/charts/bar-chart/bar-chart.tsx
5622
5946
  const validateData$2 = (data) => {
5623
- if (!data?.length) return "No data available";
5624
- 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";
5947
+ if (!data?.length) return (0, _wordpress_i18n.__)("No data available", "jetpack-charts");
5948
+ 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 (0, _wordpress_i18n.__)("Invalid data", "jetpack-charts");
5625
5949
  return null;
5626
5950
  };
5627
5951
  const getPatternId = (chartId, index) => `bar-pattern-${chartId}-${index}`;
@@ -5630,8 +5954,10 @@ const renderTooltipRow = (label, value) => /* @__PURE__ */ (0, react_jsx_runtime
5630
5954
  children: (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n.__)("%1$s: %2$s", "jetpack-charts"), label, value)
5631
5955
  });
5632
5956
  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 }) => {
5957
+ if (!withTooltips && (withBandHighlight || onBandHighlightChange)) warnOnce("bar-chart-band-highlight-without-tooltips", "BarChart: withBandHighlight and onBandHighlightChange require withTooltips.");
5633
5958
  const legendInteractive = legend.interactive ?? false;
5634
5959
  const legendCollapseGroups = legend.collapseGroups ?? false;
5960
+ const legendComparisonItem = legend.comparisonItem ?? false;
5635
5961
  const horizontal = orientation === "horizontal";
5636
5962
  const chartId = useChartId(providedChartId);
5637
5963
  const hiddenSeries = useDefaultHiddenSeries(chartId, defaultHiddenSeries);
@@ -5642,10 +5968,21 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5642
5968
  enabled: showZeroValues,
5643
5969
  valueAxisLength: horizontal ? width : height
5644
5970
  });
5645
- const legendItems = useChartLegendItems(dataSorted, (0, react.useMemo)(() => ({ collapseGroups: legendCollapseGroups }), [legendCollapseGroups]));
5971
+ const legendItems = useChartLegendItems(dataSorted, (0, react.useMemo)(() => ({
5972
+ collapseGroups: legendCollapseGroups,
5973
+ comparisonItem: legendComparisonItem
5974
+ }), [legendCollapseGroups, legendComparisonItem]));
5646
5975
  const { getElementStyles } = useGlobalChartsContext();
5647
5976
  const isSeriesRendered = (0, react.useCallback)((series) => isSeriesVisible(series.label), [isSeriesVisible]);
5977
+ const hasWholeNumberValues = (0, react.useMemo)(() => hasOnlyWholeNumbers(dataSorted.filter(isSeriesRendered)), [dataSorted, isSeriesRendered]);
5648
5978
  const chartOptions = useBarChartOptions(dataWithVisibleZeros, horizontal, options, isSeriesRendered);
5979
+ const valueAxis = horizontal ? chartOptions.axis.x : chartOptions.axis.y;
5980
+ const callerValueDomain = horizontal ? options.xScale?.domain : options.yScale?.domain;
5981
+ const wholeNumberTicksProps = {
5982
+ axis: horizontal ? "x" : "y",
5983
+ numTicks: valueAxis.numTicks,
5984
+ enabled: hasWholeNumberValues && !valueAxis.tickValues && !callerValueDomain
5985
+ };
5649
5986
  const defaultMargin = useChartMargin(height, chartOptions, dataSorted, theme, horizontal);
5650
5987
  const chartRef = (0, react.useRef)(null);
5651
5988
  const { legendChildren, nonLegendChildren } = useChartChildren(children, "BarChart");
@@ -5778,15 +6115,15 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5778
6115
  const p = point;
5779
6116
  return nearestDatum.label != null ? p.label === nearestDatum.label : !!nearestDatum.date && !!p.date && p.date.getTime() === nearestDatum.date.getTime();
5780
6117
  });
5781
- if (comparisonEntry && comparisonDatum && comparisonDatum.value != null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6118
+ if (comparisonEntry && comparisonDatum) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5782
6119
  className: bar_chart_module_default["bar-chart__tooltip"],
5783
6120
  children: [
5784
6121
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5785
6122
  className: bar_chart_module_default["bar-chart__tooltip-header"],
5786
6123
  children: categoryLabel
5787
6124
  }),
5788
- renderTooltipRow(primaryKey, (0, _automattic_number_formatters.formatNumber)(nearestDatum.value)),
5789
- renderTooltipRow(comparisonEntry.series.label, (0, _automattic_number_formatters.formatNumber)(comparisonDatum.value))
6125
+ renderTooltipRow(primaryKey, formatReading(nearestDatum.value)),
6126
+ renderTooltipRow(comparisonEntry.series.label, formatReading(comparisonDatum.value))
5790
6127
  ]
5791
6128
  });
5792
6129
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -5794,7 +6131,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5794
6131
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5795
6132
  className: bar_chart_module_default["bar-chart__tooltip-header"],
5796
6133
  children: primaryKey
5797
- }), renderTooltipRow(categoryLabel, (0, _automattic_number_formatters.formatNumber)(nearestDatum.value))]
6134
+ }), renderTooltipRow(categoryLabel, formatReading(nearestDatum.value))]
5798
6135
  });
5799
6136
  }, [chartOptions.tooltip, comparisonEntries]);
5800
6137
  const renderPattern = (0, react.useCallback)((index, color) => {
@@ -5845,15 +6182,15 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5845
6182
  const createKeyboardHighlightStyle = (0, react.useCallback)(() => {
5846
6183
  if (selectedIndex === void 0) return "";
5847
6184
  const primaryCount = primaryEntries.length;
5848
- const maxDataPoints = Math.max(...primaryEntries.map((e) => e.series.data.length));
6185
+ if (!primaryCount) return "";
5849
6186
  const dataPointIndex = Math.floor(selectedIndex / primaryCount);
5850
6187
  const seriesIndex = selectedIndex % primaryCount;
5851
- if (dataPointIndex >= maxDataPoints || seriesIndex >= primaryCount) return "";
5852
6188
  const seriesData = primaryEntries[seriesIndex]?.series;
5853
- if (!seriesData || dataPointIndex >= seriesData.data.length) return "";
5854
- const actualBarIndex = seriesIndex * maxDataPoints + dataPointIndex;
6189
+ const datum = seriesData?.data[dataPointIndex];
6190
+ if (!seriesData || !datum || !isBarRendered(datum)) return "";
6191
+ const domBarIndex = primaryEntries.slice(0, seriesIndex).reduce((count, { series }) => count + countRenderedBars(series.data), 0) + countRenderedBars(seriesData.data.slice(0, dataPointIndex));
5855
6192
  return `
5856
- .bar-chart[data-chart-id="bar-chart-${chartId}"] .visx-bar-group .visx-bar:nth-child(${actualBarIndex + 1}) {
6193
+ .bar-chart[data-chart-id="bar-chart-${chartId}"] .visx-bar-group .visx-bar:nth-child(${domBarIndex + 1}) {
5857
6194
  stroke: #005fcc;
5858
6195
  stroke-width: 2px;
5859
6196
  }
@@ -5947,17 +6284,20 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5947
6284
  horizontal,
5948
6285
  onChange: onBandHighlightChange
5949
6286
  }),
5950
- !allSeriesHidden && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Grid, {
5951
- columns: gridVisibility.includes("y"),
5952
- rows: false,
5953
- numTicks: 4,
5954
- tickValues: chartOptions.axis.x.tickValues
5955
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Grid, {
5956
- columns: false,
5957
- rows: gridVisibility.includes("x"),
5958
- numTicks: 4,
5959
- tickValues: chartOptions.axis.y.tickValues
5960
- })] }),
6287
+ !allSeriesHidden && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WholeNumberTicks, {
6288
+ ...wholeNumberTicksProps,
6289
+ children: (valueTicks) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Grid, {
6290
+ columns: gridVisibility.includes("y"),
6291
+ rows: false,
6292
+ numTicks: chartOptions.axis.x.numTicks,
6293
+ tickValues: (horizontal ? valueTicks : void 0) ?? chartOptions.axis.x.tickValues
6294
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Grid, {
6295
+ columns: false,
6296
+ rows: gridVisibility.includes("x"),
6297
+ numTicks: chartOptions.axis.y.numTicks,
6298
+ tickValues: (horizontal ? void 0 : valueTicks) ?? chartOptions.axis.y.tickValues
6299
+ })] })
6300
+ }),
5961
6301
  withPatterns && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("defs", { children: dataSorted.map((seriesData, index) => renderPattern(index, getElementStyles({
5962
6302
  data: seriesData,
5963
6303
  index
@@ -6000,7 +6340,16 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
6000
6340
  onPointerDown,
6001
6341
  onPointerUp
6002
6342
  }),
6003
- !allSeriesHidden && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, { ...chartOptions.axis.x }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, { ...chartOptions.axis.y })] }),
6343
+ !allSeriesHidden && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WholeNumberTicks, {
6344
+ ...wholeNumberTicksProps,
6345
+ children: (valueTicks) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, {
6346
+ ...chartOptions.axis.x,
6347
+ ...horizontal && valueTicks ? { tickValues: valueTicks } : {}
6348
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, {
6349
+ ...chartOptions.axis.y,
6350
+ ...!horizontal && valueTicks ? { tickValues: valueTicks } : {}
6351
+ })] })
6352
+ }),
6004
6353
  withTooltips && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessibleTooltip, {
6005
6354
  tooltipPlacement,
6006
6355
  tooltipAnchorTop,