@automattic/charts 2.0.0 → 2.0.1

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.0.1] - 2026-08-19
9
+ ### Changed
10
+ - Charts: adapt bar chart time-axis tick labels to the data's bucket resolution and time span, support the `tickResolution` hint, and name each bar's bucket in its tooltip. [#51274]
11
+ - Resolve an XY chart theme's color roles from a single computed-style snapshot, and rebuild the theme only when a series color changes rather than on every render. [#51342]
12
+
13
+ ### Fixed
14
+ - Charts: keep a bar chart's time axis on the buckets it actually draws, so a labelled bar, a comparison series, or a series hidden from the legend can no longer strand a tick at the axis origin, and label such a bar by its label in both the axis and the tooltip. [#51343]
15
+ - Charts: keep the year and the day on a bar chart's time axis, which could previously skip the tick that named them, and stop the same label falling on two ticks in a row. [#51339]
16
+ - Charts: measure a horizontal chart's left margin from the labels it will actually draw, so a caller's tick formatter is applied exactly once and time-of-day labels stop reserving room for the string "Invalid Date". [#51272]
17
+ - Charts: stop a bar chart's time axis dropping to a couple of ticks when it reaches for the year, and speed up how it picks them. [#51339]
18
+
8
19
  ## [2.0.0] - 2026-08-10
9
20
  ### Added
10
21
  - Time axis: Accept a tickResolution hint on the x-axis options so callers that know the data's bucket size can set tick formats directly instead of relying on inference. [#51017]
@@ -965,6 +976,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
965
976
  - Fixed lints following ESLint rule changes for TS [#40584]
966
977
  - Fixing a bug in Chart storybook data. [#40640]
967
978
 
979
+ [2.0.1]: https://github.com/Automattic/charts/compare/v2.0.0...v2.0.1
968
980
  [2.0.0]: https://github.com/Automattic/charts/compare/v1.12.0...v2.0.0
969
981
  [1.12.0]: https://github.com/Automattic/charts/compare/v1.11.0...v1.12.0
970
982
  [1.11.0]: https://github.com/Automattic/charts/compare/v1.10.3...v1.11.0
package/dist/index.cjs CHANGED
@@ -510,15 +510,31 @@ const CSS_VAR_NAME_PATTERN = /^--[\w-]+$/;
510
510
  * @param element - Optional DOM element to resolve the variable from (defaults to document.documentElement)
511
511
  * @return The resolved value, fallback value, or null if unresolvable
512
512
  */
513
- const resolveCssVariable = (value, element) => {
514
- if (!value) return null;
515
- if (value.startsWith("var(") && value.endsWith(")")) {
516
- const parsed = parseVarExpression(value);
517
- if (parsed) return resolveVariableName(parsed.varName, element) || parsed.fallback;
518
- }
519
- if (value.startsWith("--")) return resolveVariableName(value, element);
520
- return value;
521
- };
513
+ const resolveCssVariable = (value, element) => createCssVariableResolver(element)(value);
514
+ /**
515
+ * Builds a resolver that reads every value from one `getComputedStyle` snapshot.
516
+ *
517
+ * `getComputedStyle` is the expensive half of resolving a token: each call can force the browser to flush pending style, and a chart theme resolves five roles at once. Taking the snapshot once per build turns that into a single query. The reads that share a snapshot happen synchronously inside one memo body, so no style change can land between them — the resolver is single-pass by design and is not meant to be held across renders.
518
+ *
519
+ * @param element - The element to resolve against, or null/undefined for the document root.
520
+ * @return A resolver that resolves each value exactly as `resolveCssVariable` does.
521
+ */
522
+ function createCssVariableResolver(element) {
523
+ let styles = null;
524
+ const getStyles = () => {
525
+ if (!styles) styles = computedStyleFor(element);
526
+ return styles;
527
+ };
528
+ return (value) => {
529
+ if (!value) return null;
530
+ if (value.startsWith("var(") && value.endsWith(")")) {
531
+ const parsed = parseVarExpression(value);
532
+ if (parsed) return readCustomProperty(parsed.varName, getStyles()) || parsed.fallback;
533
+ }
534
+ if (value.startsWith("--")) return readCustomProperty(value, getStyles());
535
+ return value;
536
+ };
537
+ }
522
538
  /**
523
539
  * Parses a var() expression into its variable name and optional fallback.
524
540
  * Uses string manipulation instead of complex regex to avoid ReDoS.
@@ -546,17 +562,30 @@ function parseVarExpression(expr) {
546
562
  };
547
563
  }
548
564
  /**
549
- * Resolves a plain CSS variable name to its computed value.
565
+ * Takes one computed-style snapshot, or null where there is nothing to read (SSR, or a detached element).
566
+ *
567
+ * @param element - The element to read from, or null/undefined for the document root.
568
+ * @return The computed styles, or null.
569
+ */
570
+ function computedStyleFor(element) {
571
+ if (typeof window === "undefined" || typeof document === "undefined") return null;
572
+ try {
573
+ return getComputedStyle(element || document.documentElement);
574
+ } catch {
575
+ return null;
576
+ }
577
+ }
578
+ /**
579
+ * Reads one custom property from a computed-style snapshot.
550
580
  *
551
581
  * @param varName - A CSS variable name like '--my-color'
552
- * @param element - Optional DOM element to resolve from
582
+ * @param styles - The snapshot to read from, or null
553
583
  * @return The computed value or null
554
584
  */
555
- function resolveVariableName(varName, element) {
556
- if (typeof window === "undefined" || typeof document === "undefined") return null;
585
+ function readCustomProperty(varName, styles) {
586
+ if (!styles) return null;
557
587
  try {
558
- const targetElement = element || document.documentElement;
559
- return getComputedStyle(targetElement).getPropertyValue(varName).trim() || null;
588
+ return styles.getPropertyValue(varName).trim() || null;
560
589
  } catch {
561
590
  return null;
562
591
  }
@@ -1025,11 +1054,13 @@ const useDeepMemo = (value) => {
1025
1054
  };
1026
1055
  //#endregion
1027
1056
  //#region src/hooks/use-xychart-theme.ts
1028
- const resolveColor = (value) => value ? resolveCssVariable(value) ?? value : value;
1029
1057
  const useXYChartTheme = (data) => {
1030
1058
  const theme = useGlobalChartsTheme();
1059
+ const seriesColorKey = JSON.stringify((data ?? []).map((series) => series.options?.stroke).filter((color) => Boolean(color)));
1031
1060
  return (0, react.useMemo)(() => {
1032
- const seriesColors = (data ?? []).map((series) => series.options?.stroke).filter((color) => Boolean(color));
1061
+ const resolve = createCssVariableResolver();
1062
+ const resolveColor = (value) => value ? resolve(value) ?? value : value;
1063
+ const seriesColors = JSON.parse(seriesColorKey);
1033
1064
  return (0, _visx_xychart.buildChartTheme)({
1034
1065
  ...theme,
1035
1066
  colors: [...seriesColors, ...theme.colors ?? []],
@@ -1051,7 +1082,7 @@ const useXYChartTheme = (data) => {
1051
1082
  fill: resolveColor(theme.svgLabelSmall.fill)
1052
1083
  }
1053
1084
  });
1054
- }, [theme, data]);
1085
+ }, [theme, seriesColorKey]);
1055
1086
  };
1056
1087
  //#endregion
1057
1088
  //#region src/hooks/use-chart-data-transform.ts
@@ -1138,7 +1169,7 @@ const getXAxisLabelMetrics = (theme, orientation) => {
1138
1169
  const useChartMargin = (height, options, data, theme, horizontal = false) => {
1139
1170
  const yTicks = (0, react.useMemo)(() => {
1140
1171
  const allDataPoints = data.flatMap((series) => series.data);
1141
- if (horizontal) return allDataPoints.map((d) => d.label || options.axis?.y?.tickFormat(d.date.getTime(), 0, []));
1172
+ if (horizontal) return allDataPoints.map((d) => d.label || d.date?.getTime());
1142
1173
  if (options.axis?.y?.tickValues?.length) return options.axis.y.tickValues;
1143
1174
  const minY = Math.min(...allDataPoints.map((d) => d.value));
1144
1175
  const maxY = Math.max(...allDataPoints.map((d) => d.value));
@@ -2233,28 +2264,38 @@ const getCurveType = (type, smoothing) => {
2233
2264
  default: return _visx_curve.curveLinear;
2234
2265
  }
2235
2266
  };
2236
- const formatYearTick = (timestamp) => {
2237
- return new Date(timestamp).toLocaleDateString(void 0, { year: "numeric" });
2238
- };
2239
- const formatDateTick$1 = (timestamp) => {
2240
- return new Date(timestamp).toLocaleDateString(void 0, {
2241
- month: "short",
2242
- day: "numeric"
2243
- });
2244
- };
2245
- const formatHourTick = (timestamp) => {
2246
- return new Date(timestamp).toLocaleTimeString(void 0, {
2247
- hour: "numeric",
2248
- hour12: true
2249
- });
2250
- };
2251
- const formatDateOrHourTick = (timestamp) => {
2252
- const date = new Date(timestamp);
2253
- return date.getHours() === 0 && date.getMinutes() === 0 ? formatDateTick$1(timestamp) : formatHourTick(timestamp);
2267
+ const dateTimeFormat = (options) => {
2268
+ let formatter;
2269
+ return (timestamp) => {
2270
+ if (!Number.isFinite(Number(timestamp))) return new Date(timestamp).toLocaleDateString();
2271
+ formatter = formatter ?? new Intl.DateTimeFormat(void 0, options);
2272
+ return formatter.format(timestamp);
2273
+ };
2254
2274
  };
2255
- const formatMonthOrYearTick = (timestamp) => {
2256
- const date = new Date(timestamp);
2257
- return date.getMonth() === 0 ? formatYearTick(timestamp) : date.toLocaleDateString(void 0, { month: "short" });
2275
+ const formatYearTick = dateTimeFormat({ year: "numeric" });
2276
+ const formatDateTick = dateTimeFormat({
2277
+ month: "short",
2278
+ day: "numeric"
2279
+ });
2280
+ const formatHourTick = dateTimeFormat({
2281
+ hour: "numeric",
2282
+ hour12: true
2283
+ });
2284
+ const formatMonthTick = dateTimeFormat({ month: "short" });
2285
+ const boundaryFormat = (isAnchor, atBoundary, between) => {
2286
+ const format = (timestamp) => isAnchor(new Date(timestamp)) ? atBoundary(timestamp) : between(timestamp);
2287
+ format.isAnchor = isAnchor;
2288
+ return format;
2289
+ };
2290
+ const formatDateOrHourTick = boundaryFormat((date) => date.getHours() === 0 && date.getMinutes() === 0, formatDateTick, formatHourTick);
2291
+ const formatMonthOrYearTick = boundaryFormat((date) => date.getMonth() === 0, formatYearTick, formatMonthTick);
2292
+ const getSpan = (sortedData) => {
2293
+ const bounds = sortedData.map((datom) => [datom.data.at(0)?.date, datom.data.at(-1)?.date]).filter(([first, last]) => first !== void 0 && last !== void 0);
2294
+ if (!bounds.length) return null;
2295
+ return {
2296
+ minX: Math.min(...bounds.map(([first]) => Number(first))),
2297
+ maxX: Math.max(...bounds.map(([, last]) => Number(last)))
2298
+ };
2258
2299
  };
2259
2300
  const getPointSpacingInHours = (sortedData) => {
2260
2301
  return sortedData.reduce((spacing, datom) => datom.data.reduce((seriesSpacing, point, index) => {
@@ -2263,26 +2304,90 @@ const getPointSpacingInHours = (sortedData) => {
2263
2304
  return Math.min(seriesSpacing, Math.abs((0, date_fns.differenceInHours)(point.date, previous.date)));
2264
2305
  }, spacing), Number.POSITIVE_INFINITY);
2265
2306
  };
2266
- const SPACING_BY_RESOLUTION = {
2267
- hour: 1,
2268
- day: 24,
2269
- week: 168,
2270
- month: 672
2307
+ const SUB_DAILY_SPACING_HOURS = 23;
2308
+ const MONTHLY_SPACING_HOURS = 672;
2309
+ const getBucketResolution = (sortedData, tickResolution) => {
2310
+ if (tickResolution) return tickResolution === "week" ? "day" : tickResolution;
2311
+ const spacingInHours = getPointSpacingInHours(sortedData);
2312
+ if (spacingInHours < SUB_DAILY_SPACING_HOURS) return "hour";
2313
+ if (!Number.isFinite(spacingInHours) || spacingInHours < MONTHLY_SPACING_HOURS) return "day";
2314
+ return spacingInHours < 12 * MONTHLY_SPACING_HOURS ? "month" : "year";
2271
2315
  };
2272
2316
  const getFormatter = (sortedData, tickResolution) => {
2273
- if (tickResolution === "year") return formatYearTick;
2274
- const minX = Math.min(...sortedData.map((datom) => datom.data.at(0)?.date));
2275
- const maxX = Math.max(...sortedData.map((datom) => datom.data.at(-1)?.date));
2276
- const spacingInHours = tickResolution ? SPACING_BY_RESOLUTION[tickResolution] : getPointSpacingInHours(sortedData);
2277
- const isSubDaily = spacingInHours < 23;
2278
- const diffInHours = Math.abs((0, date_fns.differenceInHours)(maxX, minX));
2279
- if (diffInHours <= 24 && isSubDaily) return formatHourTick;
2280
- if (diffInHours <= 168 && isSubDaily) return formatDateOrHourTick;
2281
- if (Math.abs((0, date_fns.differenceInYears)(maxX, minX)) <= 1) return Number.isFinite(spacingInHours) && spacingInHours >= 672 ? formatMonthOrYearTick : formatDateTick$1;
2282
- return formatYearTick;
2317
+ const resolution = getBucketResolution(sortedData, tickResolution);
2318
+ if (resolution === "year") return formatYearTick;
2319
+ if (resolution === "month") return formatMonthOrYearTick;
2320
+ const span = getSpan(sortedData);
2321
+ if (!span) return formatDateTick;
2322
+ const diffInHours = Math.abs((0, date_fns.differenceInHours)(span.maxX, span.minX));
2323
+ if (resolution === "hour") {
2324
+ if (diffInHours <= 24) return formatHourTick;
2325
+ if (diffInHours <= 168) return formatDateOrHourTick;
2326
+ }
2327
+ return Math.abs((0, date_fns.differenceInYears)(span.maxX, span.minX)) <= 1 ? formatDateTick : formatYearTick;
2328
+ };
2329
+ const getAnchorIndices = (domain, isAnchor) => domain.reduce((indices, date, index) => {
2330
+ if (isAnchor(date)) indices.push(index);
2331
+ return indices;
2332
+ }, []);
2333
+ const getCandidateSteps = (length, maxTicks, anchorSpacing) => {
2334
+ const steps = /* @__PURE__ */ new Set();
2335
+ for (let count = maxTicks; count > 1; count--) {
2336
+ steps.add(Math.max(1, Math.floor((length - 1) / (count - 1))));
2337
+ steps.add(Math.max(1, Math.ceil((length - 1) / (count - 1))));
2338
+ }
2339
+ if (anchorSpacing) {
2340
+ for (let divisor = 1; divisor <= anchorSpacing; divisor++) if (anchorSpacing % divisor === 0) steps.add(anchorSpacing / divisor);
2341
+ const minStep = Math.floor((length - 1) / Math.max(1, maxTicks)) + 1;
2342
+ steps.add(Math.ceil(minStep / anchorSpacing) * anchorSpacing);
2343
+ }
2344
+ return steps;
2345
+ };
2346
+ /**
2347
+ * Tick values for a band time axis.
2348
+ *
2349
+ * visx samples a band domain by index from offset zero, blind to which labels
2350
+ * carry a boundary and without collapsing repeats — so the tick that prints the
2351
+ * year or the date often isn't sampled at all, and a long series can show the
2352
+ * same label twice. Choose the values instead: sweep the evenly spaced
2353
+ * candidates, including those that step from anchor to anchor, and keep the one
2354
+ * that reaches the most anchors without thinning the axis or putting two
2355
+ * identical labels side by side.
2356
+ *
2357
+ * @param domain - Band domain, in axis order.
2358
+ * @param tickFormatter - Formatter the axis will render these values with.
2359
+ * @param maxTicks - Most ticks the axis should carry.
2360
+ * @return Values to hand the axis as `tickValues`.
2361
+ */
2362
+ const getBandTickValues = (domain, tickFormatter, maxTicks) => {
2363
+ if (!domain.length) return [];
2364
+ const { isAnchor } = tickFormatter;
2365
+ const anchorIndices = isAnchor ? getAnchorIndices(domain, isAnchor) : [];
2366
+ const domainLabels = domain.map((date) => tickFormatter(date.getTime()));
2367
+ const candidates = [];
2368
+ const consider = (indices) => {
2369
+ if (!indices.length || indices.length > maxTicks) return;
2370
+ if (!indices.some((index, position) => position > 0 && domainLabels[index] === domainLabels[indices[position - 1]])) candidates.push(indices);
2371
+ };
2372
+ const anchorSpacing = anchorIndices.length > 1 ? anchorIndices[1] - anchorIndices[0] : null;
2373
+ for (const step of getCandidateSteps(domain.length, maxTicks, anchorSpacing)) for (let offset = 0; offset < step; offset++) {
2374
+ const indices = [];
2375
+ for (let index = offset; index < domain.length; index += step) indices.push(index);
2376
+ consider(indices);
2377
+ }
2378
+ for (let stride = 1; stride <= anchorIndices.length; stride++) consider(anchorIndices.filter((_, position) => position % stride === 0));
2379
+ if (!candidates.length) return [domain[0]];
2380
+ const densest = candidates.reduce((most, indices) => Math.max(most, indices.length), 0);
2381
+ const anchorsIn = (indices) => isAnchor ? indices.filter((index) => isAnchor(domain[index])).length : 0;
2382
+ return candidates.filter((indices) => indices.length >= densest - 1).reduce((chosen, indices) => {
2383
+ const gain = anchorsIn(indices) - anchorsIn(chosen);
2384
+ return gain > 0 || gain === 0 && indices.length > chosen.length ? indices : chosen;
2385
+ }).map((index) => domain[index]);
2283
2386
  };
2284
2387
  const guessOptimalNumTicks = (data, chartWidth, tickFormatter) => {
2285
- const xScale = (0, _visx_scale.scaleTime)({ domain: [Math.min(...data.map((datom) => datom.data.at(0)?.date)), Math.max(...data.map((datom) => datom.data.at(-1)?.date))] });
2388
+ const span = getSpan(data);
2389
+ if (!span) return 1;
2390
+ const xScale = (0, _visx_scale.scaleTime)({ domain: [span.minX, span.maxX] });
2286
2391
  const upperBound = Math.min(data[0]?.data.length || 3, Math.ceil(chartWidth / X_TICK_WIDTH));
2287
2392
  let secondBestGuess = 1;
2288
2393
  for (let numTicks = upperBound; numTicks > 1; --numTicks) {
@@ -3060,8 +3165,8 @@ const LineChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
3060
3165
  const legendShape = legend.shape ?? "line";
3061
3166
  const legendPosition = legend.position ?? "bottom";
3062
3167
  const providerTheme = useGlobalChartsTheme();
3063
- const resolvedBackgroundColor = resolveCssVariable(providerTheme.backgroundColor) ?? providerTheme.backgroundColor;
3064
3168
  const theme = useXYChartTheme(data);
3169
+ const resolvedBackgroundColor = theme.backgroundColor ?? providerTheme.backgroundColor;
3065
3170
  const chartId = useChartId(providedChartId);
3066
3171
  const chartRef = (0, react.useRef)(null);
3067
3172
  const [selectedIndex, setSelectedIndex] = (0, react.useState)(void 0);
@@ -3837,7 +3942,7 @@ const AreaChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
3837
3942
  stacked,
3838
3943
  stackOffset,
3839
3944
  getElementStyles,
3840
- strokeColor: resolveCssVariable(providerTheme.backgroundColor) ?? providerTheme.backgroundColor
3945
+ strokeColor: theme.backgroundColor ?? providerTheme.backgroundColor
3841
3946
  })] }),
3842
3947
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AreaChartScalesRef, {
3843
3948
  chartRef: internalChartRef,
@@ -3997,11 +4102,73 @@ const TruncatedYTickComponent = createTruncatedTickComponent("y");
3997
4102
  const BASE_BAND_PADDING = .2;
3998
4103
  /** Inner padding of the category band scale (the base gap between ticks). */
3999
4104
  const BASE_BAND_PADDING_INNER = .1;
4000
- const formatDateTick = (timestamp) => {
4001
- return new Date(timestamp).toLocaleDateString(void 0, {
4002
- month: "short",
4105
+ /** Ticks each axis carries unless the caller asks for a different count. */
4106
+ const DEFAULT_NUM_TICKS = 4;
4107
+ const ALL_RENDERED = () => true;
4108
+ const TOOLTIP_FORMAT_BY_RESOLUTION = {
4109
+ hour: {
4110
+ year: "numeric",
4111
+ month: "long",
4112
+ day: "numeric",
4113
+ hour: "numeric",
4114
+ hour12: true
4115
+ },
4116
+ day: {
4117
+ year: "numeric",
4118
+ month: "long",
4003
4119
  day: "numeric"
4004
- });
4120
+ },
4121
+ month: {
4122
+ year: "numeric",
4123
+ month: "long"
4124
+ },
4125
+ year: { year: "numeric" }
4126
+ };
4127
+ /**
4128
+ * Labels one bar's bucket, at the bucket's own granularity — finer than the
4129
+ * ticks whenever the overall time span coarsens the axis.
4130
+ *
4131
+ * @param data - Date-based series, already parsed and sorted by `useChartDataTransform`.
4132
+ * @param tickResolution - Caller-declared bucket resolution, when known.
4133
+ * @return Tooltip label formatter.
4134
+ */
4135
+ const getTooltipFormatter = (data, tickResolution) => {
4136
+ if (tickResolution === "week") return (timestamp) => (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n.__)("Week of %s", "jetpack-charts"), new Date(timestamp).toLocaleDateString(void 0, {
4137
+ year: "numeric",
4138
+ month: "long",
4139
+ day: "numeric"
4140
+ }));
4141
+ const format = TOOLTIP_FORMAT_BY_RESOLUTION[getBucketResolution(data, tickResolution)] ?? TOOLTIP_FORMAT_BY_RESOLUTION.day;
4142
+ return (timestamp) => new Date(timestamp).toLocaleString(void 0, format);
4143
+ };
4144
+ const identity = (label) => label;
4145
+ const byBucket = (format) => (bucket) => typeof bucket === "string" ? bucket : format(Number(bucket));
4146
+ const bucketAccessor = (d) => d?.label || d?.date;
4147
+ /**
4148
+ * The buckets the band axis can put a tick on.
4149
+ *
4150
+ * Built from the accessor visx is given, over the same series in the same order,
4151
+ * so that a tick value is a key the scale actually holds; `scaleBand` returns
4152
+ * `undefined` for one it does not, which parks the tick at the origin. Null
4153
+ * rather than a partial list when any bucket is labelled instead of dated.
4154
+ *
4155
+ * @param data - Every series handed to the chart.
4156
+ * @param isSeriesRendered - Whether visx mounts a series, i.e. the legend shows it.
4157
+ * @return Bucket dates in axis order, or null if the axis cannot be ticked by date.
4158
+ */
4159
+ const getBandDomain = (data, isSeriesRendered) => {
4160
+ const registry = {};
4161
+ for (const series of data) {
4162
+ if (series.options?.type === "comparison" || !isSeriesRendered(series)) continue;
4163
+ registry[series.label] = series;
4164
+ }
4165
+ const byTimestamp = /* @__PURE__ */ new Map();
4166
+ for (const key of Object.keys(registry)) for (const point of registry[key].data) {
4167
+ const bucket = bucketAccessor(point);
4168
+ if (!(bucket instanceof Date)) return null;
4169
+ if (!byTimestamp.has(bucket.getTime())) byTimestamp.set(bucket.getTime(), bucket);
4170
+ }
4171
+ return byTimestamp.size ? [...byTimestamp.values()] : null;
4005
4172
  };
4006
4173
  /**
4007
4174
  * Get the group padding of a scale.
@@ -4015,12 +4182,26 @@ const getGroupPadding = (scale) => {
4015
4182
  /**
4016
4183
  * Returns the merged options for the bar chart, including axis and scale configuration based on the orientation.
4017
4184
  *
4018
- * @param data - The data to be displayed in the chart.
4019
- * @param horizontal - Whether the chart is horizontal or vertical.
4020
- * @param options - The options for the chart.
4185
+ * @param data - The data to be displayed in the chart.
4186
+ * @param horizontal - Whether the chart is horizontal or vertical.
4187
+ * @param options - The options for the chart.
4188
+ * @param isSeriesRendered - Whether visx mounts a series, i.e. the legend shows it.
4021
4189
  * @return The merged options for the chart.
4022
4190
  */
4023
- function useBarChartOptions(data, horizontal, options = {}) {
4191
+ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = ALL_RENDERED) {
4192
+ const stableOptions = useDeepMemo(options);
4193
+ const axisConfig = (0, react.useMemo)(() => {
4194
+ const { labelOverflow: xLabelOverflow, tickResolution: xTickResolution, ...xAxisOptions } = stableOptions.axis?.x || {};
4195
+ const { labelOverflow: yLabelOverflow, tickResolution: yTickResolution, ...yAxisOptions } = stableOptions.axis?.y || {};
4196
+ return {
4197
+ xLabelOverflow,
4198
+ yLabelOverflow,
4199
+ xAxisOptions,
4200
+ yAxisOptions,
4201
+ tickResolution: horizontal ? yTickResolution : xTickResolution
4202
+ };
4203
+ }, [stableOptions, horizontal]);
4204
+ const { tickResolution } = axisConfig;
4024
4205
  const defaultOptions = (0, react.useMemo)(() => {
4025
4206
  const bandScale = {
4026
4207
  type: "band",
@@ -4032,19 +4213,26 @@ function useBarChartOptions(data, horizontal, options = {}) {
4032
4213
  nice: true,
4033
4214
  zero: false
4034
4215
  };
4035
- const labelFormatter = data?.[0]?.data?.[0]?.label ? (label) => label : formatDateTick;
4216
+ const hasLabels = Boolean(data?.[0]?.data?.[0]?.label);
4217
+ const timeTickFormatter = hasLabels ? null : getFormatter(data, tickResolution);
4218
+ const labelFormatter = timeTickFormatter ? byBucket(timeTickFormatter) : identity;
4219
+ const tooltipDatumFormatter = hasLabels ? labelFormatter : byBucket(getTooltipFormatter(data, tickResolution));
4036
4220
  const valueFormatter = _automattic_number_formatters.formatNumberCompact;
4037
- const labelAccessor = (d) => d?.label || d?.date;
4221
+ const bandDomain = timeTickFormatter ? getBandDomain(data, isSeriesRendered) : null;
4038
4222
  const valueAccessor = (d) => {
4039
4223
  const enhancedPoint = d;
4040
4224
  return enhancedPoint?.visualValue !== void 0 ? enhancedPoint.visualValue : d?.value;
4041
4225
  };
4042
4226
  return {
4227
+ timeAxis: bandDomain && timeTickFormatter && {
4228
+ domain: bandDomain,
4229
+ tickFormatter: timeTickFormatter
4230
+ },
4043
4231
  vertical: {
4044
4232
  xTickFormat: labelFormatter,
4045
4233
  yTickFormat: valueFormatter,
4046
- tooltipLabelFormatter: labelFormatter,
4047
- xAccessor: labelAccessor,
4234
+ tooltipLabelFormatter: tooltipDatumFormatter,
4235
+ xAccessor: bucketAccessor,
4048
4236
  yAccessor: valueAccessor,
4049
4237
  gridVisibility: "x",
4050
4238
  xScale: bandScale,
@@ -4053,20 +4241,24 @@ function useBarChartOptions(data, horizontal, options = {}) {
4053
4241
  horizontal: {
4054
4242
  xTickFormat: valueFormatter,
4055
4243
  yTickFormat: labelFormatter,
4056
- tooltipLabelFormatter: labelFormatter,
4244
+ tooltipLabelFormatter: tooltipDatumFormatter,
4057
4245
  xAccessor: valueAccessor,
4058
- yAccessor: labelAccessor,
4246
+ yAccessor: bucketAccessor,
4059
4247
  gridVisibility: "y",
4060
4248
  xScale: linearScale,
4061
4249
  yScale: bandScale
4062
4250
  }
4063
4251
  };
4064
- }, [data]);
4252
+ }, [
4253
+ data,
4254
+ tickResolution,
4255
+ isSeriesRendered
4256
+ ]);
4065
4257
  return (0, react.useMemo)(() => {
4066
4258
  const { xTickFormat, yTickFormat, tooltipLabelFormatter: defaultTooltipLabelFormatter, xAccessor, yAccessor, gridVisibility, xScale: baseXScale, yScale: baseYScale } = defaultOptions[horizontal ? "horizontal" : "vertical"];
4067
4259
  let valueScaleDomainOverride = {};
4068
4260
  if (data.some((s) => s.options?.type === "comparison")) {
4069
- if (!(!horizontal ? options.yScale?.domain : options.xScale?.domain)) {
4261
+ if (!(!horizontal ? stableOptions.yScale?.domain : stableOptions.xScale?.domain)) {
4070
4262
  const allValues = [];
4071
4263
  data.forEach((series) => {
4072
4264
  series.data.forEach((d) => {
@@ -4080,17 +4272,20 @@ function useBarChartOptions(data, horizontal, options = {}) {
4080
4272
  }
4081
4273
  const xScale = {
4082
4274
  ...baseXScale,
4083
- ...options.xScale || {},
4275
+ ...stableOptions.xScale || {},
4084
4276
  ...horizontal ? valueScaleDomainOverride : {}
4085
4277
  };
4086
4278
  const yScale = {
4087
4279
  ...baseYScale,
4088
- ...options.yScale || {},
4280
+ ...stableOptions.yScale || {},
4089
4281
  ...!horizontal ? valueScaleDomainOverride : {}
4090
4282
  };
4091
- const providedToolTipLabelFormatter = horizontal ? options.axis?.y?.tickFormat : options.axis?.x?.tickFormat;
4092
- const { labelOverflow: xLabelOverflow, ...xAxisOptions } = options.axis?.x || {};
4093
- const { labelOverflow: yLabelOverflow, ...yAxisOptions } = options.axis?.y || {};
4283
+ const { xLabelOverflow, yLabelOverflow, xAxisOptions, yAxisOptions } = axisConfig;
4284
+ const providedToolTipLabelFormatter = horizontal ? yAxisOptions.tickFormat : xAxisOptions.tickFormat;
4285
+ const { timeAxis } = defaultOptions;
4286
+ const dateAxisOptions = horizontal ? yAxisOptions : xAxisOptions;
4287
+ const bandTickValues = timeAxis && !dateAxisOptions.tickFormat ? getBandTickValues(timeAxis.domain, timeAxis.tickFormatter, dateAxisOptions.numTicks ?? DEFAULT_NUM_TICKS) : null;
4288
+ const dateAxisTickValues = bandTickValues ? { tickValues: bandTickValues } : {};
4094
4289
  return {
4095
4290
  gridVisibility,
4096
4291
  xScale,
@@ -4102,15 +4297,17 @@ function useBarChartOptions(data, horizontal, options = {}) {
4102
4297
  axis: {
4103
4298
  x: {
4104
4299
  orientation: "bottom",
4105
- numTicks: 4,
4300
+ numTicks: DEFAULT_NUM_TICKS,
4106
4301
  tickFormat: xTickFormat,
4302
+ ...horizontal ? {} : dateAxisTickValues,
4107
4303
  ...xLabelOverflow === "ellipsis" ? { tickComponent: TruncatedXTickComponent } : {},
4108
4304
  ...xAxisOptions
4109
4305
  },
4110
4306
  y: {
4111
4307
  orientation: "left",
4112
- numTicks: 4,
4308
+ numTicks: DEFAULT_NUM_TICKS,
4113
4309
  tickFormat: yTickFormat,
4310
+ ...horizontal ? dateAxisTickValues : {},
4114
4311
  ...yLabelOverflow === "ellipsis" ? { tickComponent: TruncatedYTickComponent } : {},
4115
4312
  ...yAxisOptions
4116
4313
  }
@@ -4120,7 +4317,8 @@ function useBarChartOptions(data, horizontal, options = {}) {
4120
4317
  };
4121
4318
  }, [
4122
4319
  defaultOptions,
4123
- options,
4320
+ axisConfig,
4321
+ stableOptions,
4124
4322
  horizontal,
4125
4323
  data
4126
4324
  ]);
@@ -4282,7 +4480,13 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4282
4480
  valueAxisLength: horizontal ? width : height
4283
4481
  });
4284
4482
  const legendItems = useChartLegendItems(dataSorted, (0, react.useMemo)(() => ({ collapseGroups: legendCollapseGroups }), [legendCollapseGroups]));
4285
- const chartOptions = useBarChartOptions(dataWithVisibleZeros, horizontal, options);
4483
+ const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
4484
+ const isSeriesRendered = (0, react.useCallback)((series) => !chartId || !legendInteractive || isSeriesVisible(chartId, series.label), [
4485
+ chartId,
4486
+ legendInteractive,
4487
+ isSeriesVisible
4488
+ ]);
4489
+ const chartOptions = useBarChartOptions(dataWithVisibleZeros, horizontal, options, isSeriesRendered);
4286
4490
  const defaultMargin = useChartMargin(height, chartOptions, dataSorted, theme, horizontal);
4287
4491
  const chartRef = (0, react.useRef)(null);
4288
4492
  const { legendChildren, nonLegendChildren } = useChartChildren(children, "BarChart");
@@ -4301,24 +4505,11 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4301
4505
  chartRef,
4302
4506
  totalPoints: Math.max(0, ...primarySeriesForNav.map((s) => s.data?.length || 0)) * primarySeriesForNav.length
4303
4507
  });
4304
- const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
4305
- const seriesWithVisibility = (0, react.useMemo)(() => {
4306
- if (!chartId || !legendInteractive) return dataWithVisibleZeros.map((series, index) => ({
4307
- series,
4308
- index,
4309
- isVisible: true
4310
- }));
4311
- return dataWithVisibleZeros.map((series, index) => ({
4312
- series,
4313
- index,
4314
- isVisible: isSeriesVisible(chartId, series.label)
4315
- }));
4316
- }, [
4317
- dataWithVisibleZeros,
4318
- chartId,
4319
- isSeriesVisible,
4320
- legendInteractive
4321
- ]);
4508
+ const seriesWithVisibility = (0, react.useMemo)(() => dataWithVisibleZeros.map((series, index) => ({
4509
+ series,
4510
+ index,
4511
+ isVisible: isSeriesRendered(series)
4512
+ })), [dataWithVisibleZeros, isSeriesRendered]);
4322
4513
  const allSeriesHidden = (0, react.useMemo)(() => {
4323
4514
  return seriesWithVisibility.every(({ isVisible }) => !isVisible);
4324
4515
  }, [seriesWithVisibility]);