@automattic/charts 4.2.0 → 4.4.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 (48) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/index.cjs +533 -127
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +27 -2
  5. package/dist/index.d.ts +27 -2
  6. package/dist/index.js +535 -129
  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 +135 -96
  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/classified-bar-series.tsx +85 -0
  17. package/src/charts/bar-chart/private/comparison-bars-geometry.ts +3 -6
  18. package/src/charts/bar-chart/private/index.ts +2 -0
  19. package/src/charts/bar-chart/private/test/band-highlight.test.tsx +28 -2
  20. package/src/charts/bar-chart/private/test/comparison-bars-geometry.test.ts +2 -3
  21. package/src/charts/bar-chart/private/test/value-domain.test.ts +100 -0
  22. package/src/charts/bar-chart/private/use-bar-chart-options.ts +12 -39
  23. package/src/charts/bar-chart/private/value-domain.ts +90 -0
  24. package/src/charts/bar-chart/test/bar-chart.test.tsx +614 -10
  25. package/src/charts/bar-chart/types.ts +43 -0
  26. package/src/charts/line-chart/line-chart.tsx +148 -35
  27. package/src/charts/line-chart/private/index.ts +1 -0
  28. package/src/charts/line-chart/private/line-chart-glyph.tsx +17 -3
  29. package/src/charts/line-chart/private/nearest-pointer-events.tsx +155 -0
  30. package/src/charts/line-chart/test/line-chart.test.tsx +604 -9
  31. package/src/charts/line-chart/types.ts +2 -1
  32. package/src/charts/private/readings.ts +34 -0
  33. package/src/charts/private/test/readings.test.ts +29 -0
  34. package/src/charts/private/test/whole-number-ticks.test.tsx +57 -0
  35. package/src/charts/private/whole-number-ticks.tsx +79 -0
  36. package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +124 -0
  37. package/src/components/legend/hooks/use-chart-legend-items.ts +60 -2
  38. package/src/components/legend/private/base-legend.tsx +36 -21
  39. package/src/components/legend/test/legend.test.tsx +84 -0
  40. package/src/components/legend/types.ts +2 -0
  41. package/src/components/legend/utils/index.ts +0 -1
  42. package/src/components/tooltip/accessible-tooltip.tsx +1 -1
  43. package/src/hooks/test/use-chart-margin.test.tsx +35 -0
  44. package/src/hooks/use-chart-margin.tsx +7 -2
  45. package/src/providers/chart-context/global-charts-provider.tsx +1 -1
  46. package/src/providers/chart-context/test/chart-context.test.tsx +4 -4
  47. package/src/types.ts +8 -0
  48. 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
@@ -5339,9 +5661,8 @@ const createGroupScale = (keys, bandwidth, padding) => (0, _visx_scale.scaleBand
5339
5661
  //#endregion
5340
5662
  //#region src/charts/bar-chart/private/comparison-bars-geometry.ts
5341
5663
  /**
5342
- * Output position of a value scale's baseline: zero if in-domain, else the
5343
- * nearest range edge. Mirrors visx's getScaleBaseline so comparison shadows
5344
- * sit on the same baseline as primary bars.
5664
+ * Mirrors visx's getScaleBaseline so comparison shadows and primary bars
5665
+ * share the same baseline, including out-of-domain zero values.
5345
5666
  *
5346
5667
  * @param {ValueScale} scale - The continuous value scale.
5347
5668
  * @return {number} The baseline output position in pixels.
@@ -5352,7 +5673,7 @@ function getValueScaleBaseline(scale) {
5352
5673
  const maybeZero = scale(0);
5353
5674
  const [minOutput, maxOutput] = isDescending ? [b, a] : [a, b];
5354
5675
  if (isDescending) return Number.isFinite(maybeZero) ? Math.min(Math.max(minOutput, maybeZero), maxOutput) : maxOutput;
5355
- return Number.isFinite(maybeZero) ? Math.min(Math.max(maybeZero, minOutput), maxOutput) : minOutput;
5676
+ return Number.isFinite(maybeZero) ? Math.max(maybeZero, minOutput) : minOutput;
5356
5677
  }
5357
5678
  /**
5358
5679
  * Compute the rect for a comparison "shadow" bar, centered on the paired
@@ -5388,6 +5709,51 @@ function computeComparisonRect(params) {
5388
5709
  height: valueLength
5389
5710
  };
5390
5711
  }
5712
+ //#endregion
5713
+ //#region src/charts/bar-chart/private/classified-bar-series.tsx
5714
+ const ClassifiedBarSeries = ({ barClassName, primaryKeys, groupPadding, ...props }) => {
5715
+ const { xScale, yScale, horizontal } = (0, react.useContext)(_visx_xychart.DataContext);
5716
+ const bandScale = horizontal ? yScale : xScale;
5717
+ const valueScale = horizontal ? xScale : yScale;
5718
+ const groupScale = (0, react.useMemo)(() => createGroupScale(primaryKeys, bandScale?.bandwidth?.() ?? 0, groupPadding), [
5719
+ primaryKeys,
5720
+ bandScale,
5721
+ groupPadding
5722
+ ]);
5723
+ const renderGlyph = (0, react.useCallback)(({ datum, x, y, color, onBlur, onFocus, onPointerMove, onPointerOut, onPointerUp }) => {
5724
+ if (!bandScale?.bandwidth || !valueScale) return null;
5725
+ const baseline = getValueScaleBaseline(valueScale);
5726
+ const position = Number(bandScale((horizontal ? props.yAccessor : props.xAccessor)(datum))) + (groupScale(props.dataKey) ?? 0);
5727
+ const value = horizontal ? x : y;
5728
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
5729
+ className: (0, clsx.default)("visx-bar", barClassName(datum)),
5730
+ tabIndex: onFocus || onBlur ? 0 : void 0,
5731
+ onBlur,
5732
+ onFocus,
5733
+ onPointerMove,
5734
+ onPointerOut,
5735
+ onPointerUp,
5736
+ x: horizontal ? baseline + Math.min(0, value - baseline) : position,
5737
+ y: horizontal ? position : baseline + Math.min(0, value - baseline),
5738
+ width: horizontal ? Math.abs(value - baseline) : groupScale.bandwidth(),
5739
+ height: horizontal ? groupScale.bandwidth() : Math.abs(value - baseline),
5740
+ fill: color
5741
+ });
5742
+ }, [
5743
+ bandScale,
5744
+ valueScale,
5745
+ horizontal,
5746
+ groupScale,
5747
+ props.dataKey,
5748
+ props.xAccessor,
5749
+ props.yAccessor,
5750
+ barClassName
5751
+ ]);
5752
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.GlyphSeries, {
5753
+ ...props,
5754
+ renderGlyph
5755
+ });
5756
+ };
5391
5757
  /**
5392
5758
  * Upper clamp on the computed group padding, so bars can never collapse to zero width
5393
5759
  * even at very large `widthFactor` values.
@@ -5484,6 +5850,8 @@ function BandHighlight({ visible, horizontal, onChange }) {
5484
5850
  if (position === void 0 || !Number.isFinite(position)) return null;
5485
5851
  return {
5486
5852
  datum: nearest.datum,
5853
+ key: nearest.key,
5854
+ index: nearest.index,
5487
5855
  x: horizontal ? margin?.left ?? 0 : position,
5488
5856
  y: horizontal ? position : margin?.top ?? 0,
5489
5857
  width: horizontal ? innerWidth : scale.bandwidth(),
@@ -5620,8 +5988,8 @@ function BandTooltip({ keys, groupPadding, withTooltips, onPointerDown, onPointe
5620
5988
  //#endregion
5621
5989
  //#region src/charts/bar-chart/bar-chart.tsx
5622
5990
  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";
5991
+ if (!data?.length) return (0, _wordpress_i18n.__)("No data available", "jetpack-charts");
5992
+ 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
5993
  return null;
5626
5994
  };
5627
5995
  const getPatternId = (chartId, index) => `bar-pattern-${chartId}-${index}`;
@@ -5629,9 +5997,11 @@ const renderTooltipRow = (label, value) => /* @__PURE__ */ (0, react_jsx_runtime
5629
5997
  className: bar_chart_module_default["bar-chart__tooltip-row"],
5630
5998
  children: (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n.__)("%1$s: %2$s", "jetpack-charts"), label, value)
5631
5999
  });
5632
- 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 }) => {
6000
+ const BarChartInternal = ({ data, chartId: providedChartId, width, height, className, margin, withTooltips = false, showLegend = false, legend = {}, gridVisibility: gridVisibilityProp, renderTooltip, tooltipPlacement, tooltipAnchorTop, tooltipStyle, barClassName, options = {}, orientation = "vertical", withPatterns = false, showZeroValues = false, withBandHighlight = false, onBandHighlightChange, defaultHiddenSeries, animation, children, gap = "md", onPointerDown, onPointerUp, onDatumActivate }) => {
6001
+ if (!withTooltips && (withBandHighlight || onBandHighlightChange)) warnOnce("bar-chart-band-highlight-without-tooltips", "BarChart: withBandHighlight and onBandHighlightChange require withTooltips.");
5633
6002
  const legendInteractive = legend.interactive ?? false;
5634
6003
  const legendCollapseGroups = legend.collapseGroups ?? false;
6004
+ const legendComparisonItem = legend.comparisonItem ?? false;
5635
6005
  const horizontal = orientation === "horizontal";
5636
6006
  const chartId = useChartId(providedChartId);
5637
6007
  const hiddenSeries = useDefaultHiddenSeries(chartId, defaultHiddenSeries);
@@ -5642,10 +6012,21 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5642
6012
  enabled: showZeroValues,
5643
6013
  valueAxisLength: horizontal ? width : height
5644
6014
  });
5645
- const legendItems = useChartLegendItems(dataSorted, (0, react.useMemo)(() => ({ collapseGroups: legendCollapseGroups }), [legendCollapseGroups]));
6015
+ const legendItems = useChartLegendItems(dataSorted, (0, react.useMemo)(() => ({
6016
+ collapseGroups: legendCollapseGroups,
6017
+ comparisonItem: legendComparisonItem
6018
+ }), [legendCollapseGroups, legendComparisonItem]));
5646
6019
  const { getElementStyles } = useGlobalChartsContext();
5647
6020
  const isSeriesRendered = (0, react.useCallback)((series) => isSeriesVisible(series.label), [isSeriesVisible]);
6021
+ const hasWholeNumberValues = (0, react.useMemo)(() => hasOnlyWholeNumbers(dataSorted.filter(isSeriesRendered)), [dataSorted, isSeriesRendered]);
5648
6022
  const chartOptions = useBarChartOptions(dataWithVisibleZeros, horizontal, options, isSeriesRendered);
6023
+ const valueAxis = horizontal ? chartOptions.axis.x : chartOptions.axis.y;
6024
+ const callerValueDomain = horizontal ? options.xScale?.domain : options.yScale?.domain;
6025
+ const wholeNumberTicksProps = {
6026
+ axis: horizontal ? "x" : "y",
6027
+ numTicks: valueAxis.numTicks,
6028
+ enabled: hasWholeNumberValues && !valueAxis.tickValues && !callerValueDomain
6029
+ };
5649
6030
  const defaultMargin = useChartMargin(height, chartOptions, dataSorted, theme, horizontal);
5650
6031
  const chartRef = (0, react.useRef)(null);
5651
6032
  const { legendChildren, nonLegendChildren } = useChartChildren(children, "BarChart");
@@ -5778,15 +6159,15 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5778
6159
  const p = point;
5779
6160
  return nearestDatum.label != null ? p.label === nearestDatum.label : !!nearestDatum.date && !!p.date && p.date.getTime() === nearestDatum.date.getTime();
5780
6161
  });
5781
- if (comparisonEntry && comparisonDatum && comparisonDatum.value != null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6162
+ if (comparisonEntry && comparisonDatum) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5782
6163
  className: bar_chart_module_default["bar-chart__tooltip"],
5783
6164
  children: [
5784
6165
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5785
6166
  className: bar_chart_module_default["bar-chart__tooltip-header"],
5786
6167
  children: categoryLabel
5787
6168
  }),
5788
- renderTooltipRow(primaryKey, (0, _automattic_number_formatters.formatNumber)(nearestDatum.value)),
5789
- renderTooltipRow(comparisonEntry.series.label, (0, _automattic_number_formatters.formatNumber)(comparisonDatum.value))
6169
+ renderTooltipRow(primaryKey, formatReading(nearestDatum.value)),
6170
+ renderTooltipRow(comparisonEntry.series.label, formatReading(comparisonDatum.value))
5790
6171
  ]
5791
6172
  });
5792
6173
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -5794,7 +6175,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5794
6175
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5795
6176
  className: bar_chart_module_default["bar-chart__tooltip-header"],
5796
6177
  children: primaryKey
5797
- }), renderTooltipRow(categoryLabel, (0, _automattic_number_formatters.formatNumber)(nearestDatum.value))]
6178
+ }), renderTooltipRow(categoryLabel, formatReading(nearestDatum.value))]
5798
6179
  });
5799
6180
  }, [chartOptions.tooltip, comparisonEntries]);
5800
6181
  const renderPattern = (0, react.useCallback)((index, color) => {
@@ -5845,15 +6226,15 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5845
6226
  const createKeyboardHighlightStyle = (0, react.useCallback)(() => {
5846
6227
  if (selectedIndex === void 0) return "";
5847
6228
  const primaryCount = primaryEntries.length;
5848
- const maxDataPoints = Math.max(...primaryEntries.map((e) => e.series.data.length));
6229
+ if (!primaryCount) return "";
5849
6230
  const dataPointIndex = Math.floor(selectedIndex / primaryCount);
5850
6231
  const seriesIndex = selectedIndex % primaryCount;
5851
- if (dataPointIndex >= maxDataPoints || seriesIndex >= primaryCount) return "";
5852
6232
  const seriesData = primaryEntries[seriesIndex]?.series;
5853
- if (!seriesData || dataPointIndex >= seriesData.data.length) return "";
5854
- const actualBarIndex = seriesIndex * maxDataPoints + dataPointIndex;
6233
+ const datum = seriesData?.data[dataPointIndex];
6234
+ if (!seriesData || !datum || !isBarRendered(datum)) return "";
6235
+ const domBarIndex = primaryEntries.slice(0, seriesIndex).reduce((count, { series }) => count + countRenderedBars(series.data), 0) + countRenderedBars(seriesData.data.slice(0, dataPointIndex));
5855
6236
  return `
5856
- .bar-chart[data-chart-id="bar-chart-${chartId}"] .visx-bar-group .visx-bar:nth-child(${actualBarIndex + 1}) {
6237
+ .bar-chart[data-chart-id="bar-chart-${chartId}"] .visx-bar-group .visx-bar:nth-child(${domBarIndex + 1}) {
5857
6238
  stroke: #005fcc;
5858
6239
  stroke-width: 2px;
5859
6240
  }
@@ -5947,17 +6328,20 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5947
6328
  horizontal,
5948
6329
  onChange: onBandHighlightChange
5949
6330
  }),
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
- })] }),
6331
+ !allSeriesHidden && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WholeNumberTicks, {
6332
+ ...wholeNumberTicksProps,
6333
+ children: (valueTicks) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Grid, {
6334
+ columns: gridVisibility.includes("y"),
6335
+ rows: false,
6336
+ numTicks: chartOptions.axis.x.numTicks,
6337
+ tickValues: (horizontal ? valueTicks : void 0) ?? chartOptions.axis.x.tickValues
6338
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Grid, {
6339
+ columns: false,
6340
+ rows: gridVisibility.includes("x"),
6341
+ numTicks: chartOptions.axis.y.numTicks,
6342
+ tickValues: (horizontal ? void 0 : valueTicks) ?? chartOptions.axis.y.tickValues
6343
+ })] })
6344
+ }),
5961
6345
  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
6346
  data: seriesData,
5963
6347
  index
@@ -5983,7 +6367,19 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5983
6367
  getElementStyles,
5984
6368
  resolveFill: resolveComparisonFill
5985
6369
  }),
5986
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.BarGroup, {
6370
+ barClassName ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", {
6371
+ className: "visx-bar-group",
6372
+ children: primaryEntries.map(({ series: seriesData, index }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClassifiedBarSeries, {
6373
+ dataKey: seriesData.label,
6374
+ data: seriesData.data,
6375
+ xAccessor: chartOptions.accessors.xAccessor,
6376
+ yAccessor: chartOptions.accessors.yAccessor,
6377
+ colorAccessor: getBarBackground(index),
6378
+ barClassName,
6379
+ primaryKeys,
6380
+ groupPadding
6381
+ }, seriesData.label))
6382
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.BarGroup, {
5987
6383
  padding: groupPadding,
5988
6384
  children: primaryEntries.map(({ series: seriesData, index }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.BarSeries, {
5989
6385
  dataKey: seriesData?.label,
@@ -6000,10 +6396,20 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
6000
6396
  onPointerDown,
6001
6397
  onPointerUp
6002
6398
  }),
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 })] }),
6399
+ !allSeriesHidden && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WholeNumberTicks, {
6400
+ ...wholeNumberTicksProps,
6401
+ children: (valueTicks) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, {
6402
+ ...chartOptions.axis.x,
6403
+ ...horizontal && valueTicks ? { tickValues: valueTicks } : {}
6404
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, {
6405
+ ...chartOptions.axis.y,
6406
+ ...!horizontal && valueTicks ? { tickValues: valueTicks } : {}
6407
+ })] })
6408
+ }),
6004
6409
  withTooltips && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccessibleTooltip, {
6005
6410
  tooltipPlacement,
6006
6411
  tooltipAnchorTop,
6412
+ style: tooltipStyle,
6007
6413
  detectBounds: true,
6008
6414
  snapTooltipToDatumX: true,
6009
6415
  snapTooltipToDatumY: true,