@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/dist/index.d.cts CHANGED
@@ -366,12 +366,12 @@ type AxisOptions = {
366
366
  tickClassName?: string;
367
367
  tickFormat?: TickFormatter<ScaleInput<AxisScale>>;
368
368
  /**
369
- * Bucket resolution of the data on a time x-axis. When set, the automatic
370
- * tick formatter derives tick formats from it directly instead of
371
- * inferring the resolution from point spacing. The overall time span still
372
- * constrains the choice — e.g. hourly buckets spanning more than a week
373
- * get date ticks, since hour ticks would be unreadable at that span.
374
- * Ignored when `tickFormat` is set.
369
+ * Bucket resolution of the data, set on whichever axis carries the dates.
370
+ * When set, the automatic tick formatter derives tick formats from it
371
+ * directly instead of inferring the resolution from point spacing. For
372
+ * daily-or-finer buckets the overall time span still constrains the choice —
373
+ * e.g. hourly buckets spanning more than a week get date ticks, since hour
374
+ * ticks would be unreadable at that span. Ignored when `tickFormat` is set.
375
375
  */
376
376
  tickResolution?: TickResolution;
377
377
  /**
package/dist/index.d.ts CHANGED
@@ -366,12 +366,12 @@ type AxisOptions = {
366
366
  tickClassName?: string;
367
367
  tickFormat?: TickFormatter<ScaleInput<AxisScale>>;
368
368
  /**
369
- * Bucket resolution of the data on a time x-axis. When set, the automatic
370
- * tick formatter derives tick formats from it directly instead of
371
- * inferring the resolution from point spacing. The overall time span still
372
- * constrains the choice — e.g. hourly buckets spanning more than a week
373
- * get date ticks, since hour ticks would be unreadable at that span.
374
- * Ignored when `tickFormat` is set.
369
+ * Bucket resolution of the data, set on whichever axis carries the dates.
370
+ * When set, the automatic tick formatter derives tick formats from it
371
+ * directly instead of inferring the resolution from point spacing. For
372
+ * daily-or-finer buckets the overall time span still constrains the choice —
373
+ * e.g. hourly buckets spanning more than a week get date ticks, since hour
374
+ * ticks would be unreadable at that span. Ignored when `tickFormat` is set.
375
375
  */
376
376
  tickResolution?: TickResolution;
377
377
  /**
package/dist/index.js CHANGED
@@ -508,15 +508,31 @@ const CSS_VAR_NAME_PATTERN = /^--[\w-]+$/;
508
508
  * @param element - Optional DOM element to resolve the variable from (defaults to document.documentElement)
509
509
  * @return The resolved value, fallback value, or null if unresolvable
510
510
  */
511
- const resolveCssVariable = (value, element) => {
512
- if (!value) return null;
513
- if (value.startsWith("var(") && value.endsWith(")")) {
514
- const parsed = parseVarExpression(value);
515
- if (parsed) return resolveVariableName(parsed.varName, element) || parsed.fallback;
516
- }
517
- if (value.startsWith("--")) return resolveVariableName(value, element);
518
- return value;
519
- };
511
+ const resolveCssVariable = (value, element) => createCssVariableResolver(element)(value);
512
+ /**
513
+ * Builds a resolver that reads every value from one `getComputedStyle` snapshot.
514
+ *
515
+ * `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.
516
+ *
517
+ * @param element - The element to resolve against, or null/undefined for the document root.
518
+ * @return A resolver that resolves each value exactly as `resolveCssVariable` does.
519
+ */
520
+ function createCssVariableResolver(element) {
521
+ let styles = null;
522
+ const getStyles = () => {
523
+ if (!styles) styles = computedStyleFor(element);
524
+ return styles;
525
+ };
526
+ return (value) => {
527
+ if (!value) return null;
528
+ if (value.startsWith("var(") && value.endsWith(")")) {
529
+ const parsed = parseVarExpression(value);
530
+ if (parsed) return readCustomProperty(parsed.varName, getStyles()) || parsed.fallback;
531
+ }
532
+ if (value.startsWith("--")) return readCustomProperty(value, getStyles());
533
+ return value;
534
+ };
535
+ }
520
536
  /**
521
537
  * Parses a var() expression into its variable name and optional fallback.
522
538
  * Uses string manipulation instead of complex regex to avoid ReDoS.
@@ -544,17 +560,30 @@ function parseVarExpression(expr) {
544
560
  };
545
561
  }
546
562
  /**
547
- * Resolves a plain CSS variable name to its computed value.
563
+ * Takes one computed-style snapshot, or null where there is nothing to read (SSR, or a detached element).
564
+ *
565
+ * @param element - The element to read from, or null/undefined for the document root.
566
+ * @return The computed styles, or null.
567
+ */
568
+ function computedStyleFor(element) {
569
+ if (typeof window === "undefined" || typeof document === "undefined") return null;
570
+ try {
571
+ return getComputedStyle(element || document.documentElement);
572
+ } catch {
573
+ return null;
574
+ }
575
+ }
576
+ /**
577
+ * Reads one custom property from a computed-style snapshot.
548
578
  *
549
579
  * @param varName - A CSS variable name like '--my-color'
550
- * @param element - Optional DOM element to resolve from
580
+ * @param styles - The snapshot to read from, or null
551
581
  * @return The computed value or null
552
582
  */
553
- function resolveVariableName(varName, element) {
554
- if (typeof window === "undefined" || typeof document === "undefined") return null;
583
+ function readCustomProperty(varName, styles) {
584
+ if (!styles) return null;
555
585
  try {
556
- const targetElement = element || document.documentElement;
557
- return getComputedStyle(targetElement).getPropertyValue(varName).trim() || null;
586
+ return styles.getPropertyValue(varName).trim() || null;
558
587
  } catch {
559
588
  return null;
560
589
  }
@@ -1023,11 +1052,13 @@ const useDeepMemo = (value) => {
1023
1052
  };
1024
1053
  //#endregion
1025
1054
  //#region src/hooks/use-xychart-theme.ts
1026
- const resolveColor = (value) => value ? resolveCssVariable(value) ?? value : value;
1027
1055
  const useXYChartTheme = (data) => {
1028
1056
  const theme = useGlobalChartsTheme();
1057
+ const seriesColorKey = JSON.stringify((data ?? []).map((series) => series.options?.stroke).filter((color) => Boolean(color)));
1029
1058
  return useMemo(() => {
1030
- const seriesColors = (data ?? []).map((series) => series.options?.stroke).filter((color) => Boolean(color));
1059
+ const resolve = createCssVariableResolver();
1060
+ const resolveColor = (value) => value ? resolve(value) ?? value : value;
1061
+ const seriesColors = JSON.parse(seriesColorKey);
1031
1062
  return buildChartTheme({
1032
1063
  ...theme,
1033
1064
  colors: [...seriesColors, ...theme.colors ?? []],
@@ -1049,7 +1080,7 @@ const useXYChartTheme = (data) => {
1049
1080
  fill: resolveColor(theme.svgLabelSmall.fill)
1050
1081
  }
1051
1082
  });
1052
- }, [theme, data]);
1083
+ }, [theme, seriesColorKey]);
1053
1084
  };
1054
1085
  //#endregion
1055
1086
  //#region src/hooks/use-chart-data-transform.ts
@@ -1136,7 +1167,7 @@ const getXAxisLabelMetrics = (theme, orientation) => {
1136
1167
  const useChartMargin = (height, options, data, theme, horizontal = false) => {
1137
1168
  const yTicks = useMemo(() => {
1138
1169
  const allDataPoints = data.flatMap((series) => series.data);
1139
- if (horizontal) return allDataPoints.map((d) => d.label || options.axis?.y?.tickFormat(d.date.getTime(), 0, []));
1170
+ if (horizontal) return allDataPoints.map((d) => d.label || d.date?.getTime());
1140
1171
  if (options.axis?.y?.tickValues?.length) return options.axis.y.tickValues;
1141
1172
  const minY = Math.min(...allDataPoints.map((d) => d.value));
1142
1173
  const maxY = Math.max(...allDataPoints.map((d) => d.value));
@@ -2231,28 +2262,38 @@ const getCurveType = (type, smoothing) => {
2231
2262
  default: return curveLinear;
2232
2263
  }
2233
2264
  };
2234
- const formatYearTick = (timestamp) => {
2235
- return new Date(timestamp).toLocaleDateString(void 0, { year: "numeric" });
2236
- };
2237
- const formatDateTick$1 = (timestamp) => {
2238
- return new Date(timestamp).toLocaleDateString(void 0, {
2239
- month: "short",
2240
- day: "numeric"
2241
- });
2242
- };
2243
- const formatHourTick = (timestamp) => {
2244
- return new Date(timestamp).toLocaleTimeString(void 0, {
2245
- hour: "numeric",
2246
- hour12: true
2247
- });
2248
- };
2249
- const formatDateOrHourTick = (timestamp) => {
2250
- const date = new Date(timestamp);
2251
- return date.getHours() === 0 && date.getMinutes() === 0 ? formatDateTick$1(timestamp) : formatHourTick(timestamp);
2265
+ const dateTimeFormat = (options) => {
2266
+ let formatter;
2267
+ return (timestamp) => {
2268
+ if (!Number.isFinite(Number(timestamp))) return new Date(timestamp).toLocaleDateString();
2269
+ formatter = formatter ?? new Intl.DateTimeFormat(void 0, options);
2270
+ return formatter.format(timestamp);
2271
+ };
2252
2272
  };
2253
- const formatMonthOrYearTick = (timestamp) => {
2254
- const date = new Date(timestamp);
2255
- return date.getMonth() === 0 ? formatYearTick(timestamp) : date.toLocaleDateString(void 0, { month: "short" });
2273
+ const formatYearTick = dateTimeFormat({ year: "numeric" });
2274
+ const formatDateTick = dateTimeFormat({
2275
+ month: "short",
2276
+ day: "numeric"
2277
+ });
2278
+ const formatHourTick = dateTimeFormat({
2279
+ hour: "numeric",
2280
+ hour12: true
2281
+ });
2282
+ const formatMonthTick = dateTimeFormat({ month: "short" });
2283
+ const boundaryFormat = (isAnchor, atBoundary, between) => {
2284
+ const format = (timestamp) => isAnchor(new Date(timestamp)) ? atBoundary(timestamp) : between(timestamp);
2285
+ format.isAnchor = isAnchor;
2286
+ return format;
2287
+ };
2288
+ const formatDateOrHourTick = boundaryFormat((date) => date.getHours() === 0 && date.getMinutes() === 0, formatDateTick, formatHourTick);
2289
+ const formatMonthOrYearTick = boundaryFormat((date) => date.getMonth() === 0, formatYearTick, formatMonthTick);
2290
+ const getSpan = (sortedData) => {
2291
+ const bounds = sortedData.map((datom) => [datom.data.at(0)?.date, datom.data.at(-1)?.date]).filter(([first, last]) => first !== void 0 && last !== void 0);
2292
+ if (!bounds.length) return null;
2293
+ return {
2294
+ minX: Math.min(...bounds.map(([first]) => Number(first))),
2295
+ maxX: Math.max(...bounds.map(([, last]) => Number(last)))
2296
+ };
2256
2297
  };
2257
2298
  const getPointSpacingInHours = (sortedData) => {
2258
2299
  return sortedData.reduce((spacing, datom) => datom.data.reduce((seriesSpacing, point, index) => {
@@ -2261,26 +2302,90 @@ const getPointSpacingInHours = (sortedData) => {
2261
2302
  return Math.min(seriesSpacing, Math.abs(differenceInHours(point.date, previous.date)));
2262
2303
  }, spacing), Number.POSITIVE_INFINITY);
2263
2304
  };
2264
- const SPACING_BY_RESOLUTION = {
2265
- hour: 1,
2266
- day: 24,
2267
- week: 168,
2268
- month: 672
2305
+ const SUB_DAILY_SPACING_HOURS = 23;
2306
+ const MONTHLY_SPACING_HOURS = 672;
2307
+ const getBucketResolution = (sortedData, tickResolution) => {
2308
+ if (tickResolution) return tickResolution === "week" ? "day" : tickResolution;
2309
+ const spacingInHours = getPointSpacingInHours(sortedData);
2310
+ if (spacingInHours < SUB_DAILY_SPACING_HOURS) return "hour";
2311
+ if (!Number.isFinite(spacingInHours) || spacingInHours < MONTHLY_SPACING_HOURS) return "day";
2312
+ return spacingInHours < 12 * MONTHLY_SPACING_HOURS ? "month" : "year";
2269
2313
  };
2270
2314
  const getFormatter = (sortedData, tickResolution) => {
2271
- if (tickResolution === "year") return formatYearTick;
2272
- const minX = Math.min(...sortedData.map((datom) => datom.data.at(0)?.date));
2273
- const maxX = Math.max(...sortedData.map((datom) => datom.data.at(-1)?.date));
2274
- const spacingInHours = tickResolution ? SPACING_BY_RESOLUTION[tickResolution] : getPointSpacingInHours(sortedData);
2275
- const isSubDaily = spacingInHours < 23;
2276
- const diffInHours = Math.abs(differenceInHours(maxX, minX));
2277
- if (diffInHours <= 24 && isSubDaily) return formatHourTick;
2278
- if (diffInHours <= 168 && isSubDaily) return formatDateOrHourTick;
2279
- if (Math.abs(differenceInYears(maxX, minX)) <= 1) return Number.isFinite(spacingInHours) && spacingInHours >= 672 ? formatMonthOrYearTick : formatDateTick$1;
2280
- return formatYearTick;
2315
+ const resolution = getBucketResolution(sortedData, tickResolution);
2316
+ if (resolution === "year") return formatYearTick;
2317
+ if (resolution === "month") return formatMonthOrYearTick;
2318
+ const span = getSpan(sortedData);
2319
+ if (!span) return formatDateTick;
2320
+ const diffInHours = Math.abs(differenceInHours(span.maxX, span.minX));
2321
+ if (resolution === "hour") {
2322
+ if (diffInHours <= 24) return formatHourTick;
2323
+ if (diffInHours <= 168) return formatDateOrHourTick;
2324
+ }
2325
+ return Math.abs(differenceInYears(span.maxX, span.minX)) <= 1 ? formatDateTick : formatYearTick;
2326
+ };
2327
+ const getAnchorIndices = (domain, isAnchor) => domain.reduce((indices, date, index) => {
2328
+ if (isAnchor(date)) indices.push(index);
2329
+ return indices;
2330
+ }, []);
2331
+ const getCandidateSteps = (length, maxTicks, anchorSpacing) => {
2332
+ const steps = /* @__PURE__ */ new Set();
2333
+ for (let count = maxTicks; count > 1; count--) {
2334
+ steps.add(Math.max(1, Math.floor((length - 1) / (count - 1))));
2335
+ steps.add(Math.max(1, Math.ceil((length - 1) / (count - 1))));
2336
+ }
2337
+ if (anchorSpacing) {
2338
+ for (let divisor = 1; divisor <= anchorSpacing; divisor++) if (anchorSpacing % divisor === 0) steps.add(anchorSpacing / divisor);
2339
+ const minStep = Math.floor((length - 1) / Math.max(1, maxTicks)) + 1;
2340
+ steps.add(Math.ceil(minStep / anchorSpacing) * anchorSpacing);
2341
+ }
2342
+ return steps;
2343
+ };
2344
+ /**
2345
+ * Tick values for a band time axis.
2346
+ *
2347
+ * visx samples a band domain by index from offset zero, blind to which labels
2348
+ * carry a boundary and without collapsing repeats — so the tick that prints the
2349
+ * year or the date often isn't sampled at all, and a long series can show the
2350
+ * same label twice. Choose the values instead: sweep the evenly spaced
2351
+ * candidates, including those that step from anchor to anchor, and keep the one
2352
+ * that reaches the most anchors without thinning the axis or putting two
2353
+ * identical labels side by side.
2354
+ *
2355
+ * @param domain - Band domain, in axis order.
2356
+ * @param tickFormatter - Formatter the axis will render these values with.
2357
+ * @param maxTicks - Most ticks the axis should carry.
2358
+ * @return Values to hand the axis as `tickValues`.
2359
+ */
2360
+ const getBandTickValues = (domain, tickFormatter, maxTicks) => {
2361
+ if (!domain.length) return [];
2362
+ const { isAnchor } = tickFormatter;
2363
+ const anchorIndices = isAnchor ? getAnchorIndices(domain, isAnchor) : [];
2364
+ const domainLabels = domain.map((date) => tickFormatter(date.getTime()));
2365
+ const candidates = [];
2366
+ const consider = (indices) => {
2367
+ if (!indices.length || indices.length > maxTicks) return;
2368
+ if (!indices.some((index, position) => position > 0 && domainLabels[index] === domainLabels[indices[position - 1]])) candidates.push(indices);
2369
+ };
2370
+ const anchorSpacing = anchorIndices.length > 1 ? anchorIndices[1] - anchorIndices[0] : null;
2371
+ for (const step of getCandidateSteps(domain.length, maxTicks, anchorSpacing)) for (let offset = 0; offset < step; offset++) {
2372
+ const indices = [];
2373
+ for (let index = offset; index < domain.length; index += step) indices.push(index);
2374
+ consider(indices);
2375
+ }
2376
+ for (let stride = 1; stride <= anchorIndices.length; stride++) consider(anchorIndices.filter((_, position) => position % stride === 0));
2377
+ if (!candidates.length) return [domain[0]];
2378
+ const densest = candidates.reduce((most, indices) => Math.max(most, indices.length), 0);
2379
+ const anchorsIn = (indices) => isAnchor ? indices.filter((index) => isAnchor(domain[index])).length : 0;
2380
+ return candidates.filter((indices) => indices.length >= densest - 1).reduce((chosen, indices) => {
2381
+ const gain = anchorsIn(indices) - anchorsIn(chosen);
2382
+ return gain > 0 || gain === 0 && indices.length > chosen.length ? indices : chosen;
2383
+ }).map((index) => domain[index]);
2281
2384
  };
2282
2385
  const guessOptimalNumTicks = (data, chartWidth, tickFormatter) => {
2283
- const xScale = scaleTime({ domain: [Math.min(...data.map((datom) => datom.data.at(0)?.date)), Math.max(...data.map((datom) => datom.data.at(-1)?.date))] });
2386
+ const span = getSpan(data);
2387
+ if (!span) return 1;
2388
+ const xScale = scaleTime({ domain: [span.minX, span.maxX] });
2284
2389
  const upperBound = Math.min(data[0]?.data.length || 3, Math.ceil(chartWidth / X_TICK_WIDTH));
2285
2390
  let secondBestGuess = 1;
2286
2391
  for (let numTicks = upperBound; numTicks > 1; --numTicks) {
@@ -3058,8 +3163,8 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
3058
3163
  const legendShape = legend.shape ?? "line";
3059
3164
  const legendPosition = legend.position ?? "bottom";
3060
3165
  const providerTheme = useGlobalChartsTheme();
3061
- const resolvedBackgroundColor = resolveCssVariable(providerTheme.backgroundColor) ?? providerTheme.backgroundColor;
3062
3166
  const theme = useXYChartTheme(data);
3167
+ const resolvedBackgroundColor = theme.backgroundColor ?? providerTheme.backgroundColor;
3063
3168
  const chartId = useChartId(providedChartId);
3064
3169
  const chartRef = useRef(null);
3065
3170
  const [selectedIndex, setSelectedIndex] = useState(void 0);
@@ -3835,7 +3940,7 @@ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
3835
3940
  stacked,
3836
3941
  stackOffset,
3837
3942
  getElementStyles,
3838
- strokeColor: resolveCssVariable(providerTheme.backgroundColor) ?? providerTheme.backgroundColor
3943
+ strokeColor: theme.backgroundColor ?? providerTheme.backgroundColor
3839
3944
  })] }),
3840
3945
  /* @__PURE__ */ jsx(AreaChartScalesRef, {
3841
3946
  chartRef: internalChartRef,
@@ -3995,11 +4100,73 @@ const TruncatedYTickComponent = createTruncatedTickComponent("y");
3995
4100
  const BASE_BAND_PADDING = .2;
3996
4101
  /** Inner padding of the category band scale (the base gap between ticks). */
3997
4102
  const BASE_BAND_PADDING_INNER = .1;
3998
- const formatDateTick = (timestamp) => {
3999
- return new Date(timestamp).toLocaleDateString(void 0, {
4000
- month: "short",
4103
+ /** Ticks each axis carries unless the caller asks for a different count. */
4104
+ const DEFAULT_NUM_TICKS = 4;
4105
+ const ALL_RENDERED = () => true;
4106
+ const TOOLTIP_FORMAT_BY_RESOLUTION = {
4107
+ hour: {
4108
+ year: "numeric",
4109
+ month: "long",
4110
+ day: "numeric",
4111
+ hour: "numeric",
4112
+ hour12: true
4113
+ },
4114
+ day: {
4115
+ year: "numeric",
4116
+ month: "long",
4001
4117
  day: "numeric"
4002
- });
4118
+ },
4119
+ month: {
4120
+ year: "numeric",
4121
+ month: "long"
4122
+ },
4123
+ year: { year: "numeric" }
4124
+ };
4125
+ /**
4126
+ * Labels one bar's bucket, at the bucket's own granularity — finer than the
4127
+ * ticks whenever the overall time span coarsens the axis.
4128
+ *
4129
+ * @param data - Date-based series, already parsed and sorted by `useChartDataTransform`.
4130
+ * @param tickResolution - Caller-declared bucket resolution, when known.
4131
+ * @return Tooltip label formatter.
4132
+ */
4133
+ const getTooltipFormatter = (data, tickResolution) => {
4134
+ if (tickResolution === "week") return (timestamp) => sprintf(__("Week of %s", "jetpack-charts"), new Date(timestamp).toLocaleDateString(void 0, {
4135
+ year: "numeric",
4136
+ month: "long",
4137
+ day: "numeric"
4138
+ }));
4139
+ const format = TOOLTIP_FORMAT_BY_RESOLUTION[getBucketResolution(data, tickResolution)] ?? TOOLTIP_FORMAT_BY_RESOLUTION.day;
4140
+ return (timestamp) => new Date(timestamp).toLocaleString(void 0, format);
4141
+ };
4142
+ const identity = (label) => label;
4143
+ const byBucket = (format) => (bucket) => typeof bucket === "string" ? bucket : format(Number(bucket));
4144
+ const bucketAccessor = (d) => d?.label || d?.date;
4145
+ /**
4146
+ * The buckets the band axis can put a tick on.
4147
+ *
4148
+ * Built from the accessor visx is given, over the same series in the same order,
4149
+ * so that a tick value is a key the scale actually holds; `scaleBand` returns
4150
+ * `undefined` for one it does not, which parks the tick at the origin. Null
4151
+ * rather than a partial list when any bucket is labelled instead of dated.
4152
+ *
4153
+ * @param data - Every series handed to the chart.
4154
+ * @param isSeriesRendered - Whether visx mounts a series, i.e. the legend shows it.
4155
+ * @return Bucket dates in axis order, or null if the axis cannot be ticked by date.
4156
+ */
4157
+ const getBandDomain = (data, isSeriesRendered) => {
4158
+ const registry = {};
4159
+ for (const series of data) {
4160
+ if (series.options?.type === "comparison" || !isSeriesRendered(series)) continue;
4161
+ registry[series.label] = series;
4162
+ }
4163
+ const byTimestamp = /* @__PURE__ */ new Map();
4164
+ for (const key of Object.keys(registry)) for (const point of registry[key].data) {
4165
+ const bucket = bucketAccessor(point);
4166
+ if (!(bucket instanceof Date)) return null;
4167
+ if (!byTimestamp.has(bucket.getTime())) byTimestamp.set(bucket.getTime(), bucket);
4168
+ }
4169
+ return byTimestamp.size ? [...byTimestamp.values()] : null;
4003
4170
  };
4004
4171
  /**
4005
4172
  * Get the group padding of a scale.
@@ -4013,12 +4180,26 @@ const getGroupPadding = (scale) => {
4013
4180
  /**
4014
4181
  * Returns the merged options for the bar chart, including axis and scale configuration based on the orientation.
4015
4182
  *
4016
- * @param data - The data to be displayed in the chart.
4017
- * @param horizontal - Whether the chart is horizontal or vertical.
4018
- * @param options - The options for the chart.
4183
+ * @param data - The data to be displayed in the chart.
4184
+ * @param horizontal - Whether the chart is horizontal or vertical.
4185
+ * @param options - The options for the chart.
4186
+ * @param isSeriesRendered - Whether visx mounts a series, i.e. the legend shows it.
4019
4187
  * @return The merged options for the chart.
4020
4188
  */
4021
- function useBarChartOptions(data, horizontal, options = {}) {
4189
+ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = ALL_RENDERED) {
4190
+ const stableOptions = useDeepMemo(options);
4191
+ const axisConfig = useMemo(() => {
4192
+ const { labelOverflow: xLabelOverflow, tickResolution: xTickResolution, ...xAxisOptions } = stableOptions.axis?.x || {};
4193
+ const { labelOverflow: yLabelOverflow, tickResolution: yTickResolution, ...yAxisOptions } = stableOptions.axis?.y || {};
4194
+ return {
4195
+ xLabelOverflow,
4196
+ yLabelOverflow,
4197
+ xAxisOptions,
4198
+ yAxisOptions,
4199
+ tickResolution: horizontal ? yTickResolution : xTickResolution
4200
+ };
4201
+ }, [stableOptions, horizontal]);
4202
+ const { tickResolution } = axisConfig;
4022
4203
  const defaultOptions = useMemo(() => {
4023
4204
  const bandScale = {
4024
4205
  type: "band",
@@ -4030,19 +4211,26 @@ function useBarChartOptions(data, horizontal, options = {}) {
4030
4211
  nice: true,
4031
4212
  zero: false
4032
4213
  };
4033
- const labelFormatter = data?.[0]?.data?.[0]?.label ? (label) => label : formatDateTick;
4214
+ const hasLabels = Boolean(data?.[0]?.data?.[0]?.label);
4215
+ const timeTickFormatter = hasLabels ? null : getFormatter(data, tickResolution);
4216
+ const labelFormatter = timeTickFormatter ? byBucket(timeTickFormatter) : identity;
4217
+ const tooltipDatumFormatter = hasLabels ? labelFormatter : byBucket(getTooltipFormatter(data, tickResolution));
4034
4218
  const valueFormatter = formatNumberCompact;
4035
- const labelAccessor = (d) => d?.label || d?.date;
4219
+ const bandDomain = timeTickFormatter ? getBandDomain(data, isSeriesRendered) : null;
4036
4220
  const valueAccessor = (d) => {
4037
4221
  const enhancedPoint = d;
4038
4222
  return enhancedPoint?.visualValue !== void 0 ? enhancedPoint.visualValue : d?.value;
4039
4223
  };
4040
4224
  return {
4225
+ timeAxis: bandDomain && timeTickFormatter && {
4226
+ domain: bandDomain,
4227
+ tickFormatter: timeTickFormatter
4228
+ },
4041
4229
  vertical: {
4042
4230
  xTickFormat: labelFormatter,
4043
4231
  yTickFormat: valueFormatter,
4044
- tooltipLabelFormatter: labelFormatter,
4045
- xAccessor: labelAccessor,
4232
+ tooltipLabelFormatter: tooltipDatumFormatter,
4233
+ xAccessor: bucketAccessor,
4046
4234
  yAccessor: valueAccessor,
4047
4235
  gridVisibility: "x",
4048
4236
  xScale: bandScale,
@@ -4051,20 +4239,24 @@ function useBarChartOptions(data, horizontal, options = {}) {
4051
4239
  horizontal: {
4052
4240
  xTickFormat: valueFormatter,
4053
4241
  yTickFormat: labelFormatter,
4054
- tooltipLabelFormatter: labelFormatter,
4242
+ tooltipLabelFormatter: tooltipDatumFormatter,
4055
4243
  xAccessor: valueAccessor,
4056
- yAccessor: labelAccessor,
4244
+ yAccessor: bucketAccessor,
4057
4245
  gridVisibility: "y",
4058
4246
  xScale: linearScale,
4059
4247
  yScale: bandScale
4060
4248
  }
4061
4249
  };
4062
- }, [data]);
4250
+ }, [
4251
+ data,
4252
+ tickResolution,
4253
+ isSeriesRendered
4254
+ ]);
4063
4255
  return useMemo(() => {
4064
4256
  const { xTickFormat, yTickFormat, tooltipLabelFormatter: defaultTooltipLabelFormatter, xAccessor, yAccessor, gridVisibility, xScale: baseXScale, yScale: baseYScale } = defaultOptions[horizontal ? "horizontal" : "vertical"];
4065
4257
  let valueScaleDomainOverride = {};
4066
4258
  if (data.some((s) => s.options?.type === "comparison")) {
4067
- if (!(!horizontal ? options.yScale?.domain : options.xScale?.domain)) {
4259
+ if (!(!horizontal ? stableOptions.yScale?.domain : stableOptions.xScale?.domain)) {
4068
4260
  const allValues = [];
4069
4261
  data.forEach((series) => {
4070
4262
  series.data.forEach((d) => {
@@ -4078,17 +4270,20 @@ function useBarChartOptions(data, horizontal, options = {}) {
4078
4270
  }
4079
4271
  const xScale = {
4080
4272
  ...baseXScale,
4081
- ...options.xScale || {},
4273
+ ...stableOptions.xScale || {},
4082
4274
  ...horizontal ? valueScaleDomainOverride : {}
4083
4275
  };
4084
4276
  const yScale = {
4085
4277
  ...baseYScale,
4086
- ...options.yScale || {},
4278
+ ...stableOptions.yScale || {},
4087
4279
  ...!horizontal ? valueScaleDomainOverride : {}
4088
4280
  };
4089
- const providedToolTipLabelFormatter = horizontal ? options.axis?.y?.tickFormat : options.axis?.x?.tickFormat;
4090
- const { labelOverflow: xLabelOverflow, ...xAxisOptions } = options.axis?.x || {};
4091
- const { labelOverflow: yLabelOverflow, ...yAxisOptions } = options.axis?.y || {};
4281
+ const { xLabelOverflow, yLabelOverflow, xAxisOptions, yAxisOptions } = axisConfig;
4282
+ const providedToolTipLabelFormatter = horizontal ? yAxisOptions.tickFormat : xAxisOptions.tickFormat;
4283
+ const { timeAxis } = defaultOptions;
4284
+ const dateAxisOptions = horizontal ? yAxisOptions : xAxisOptions;
4285
+ const bandTickValues = timeAxis && !dateAxisOptions.tickFormat ? getBandTickValues(timeAxis.domain, timeAxis.tickFormatter, dateAxisOptions.numTicks ?? DEFAULT_NUM_TICKS) : null;
4286
+ const dateAxisTickValues = bandTickValues ? { tickValues: bandTickValues } : {};
4092
4287
  return {
4093
4288
  gridVisibility,
4094
4289
  xScale,
@@ -4100,15 +4295,17 @@ function useBarChartOptions(data, horizontal, options = {}) {
4100
4295
  axis: {
4101
4296
  x: {
4102
4297
  orientation: "bottom",
4103
- numTicks: 4,
4298
+ numTicks: DEFAULT_NUM_TICKS,
4104
4299
  tickFormat: xTickFormat,
4300
+ ...horizontal ? {} : dateAxisTickValues,
4105
4301
  ...xLabelOverflow === "ellipsis" ? { tickComponent: TruncatedXTickComponent } : {},
4106
4302
  ...xAxisOptions
4107
4303
  },
4108
4304
  y: {
4109
4305
  orientation: "left",
4110
- numTicks: 4,
4306
+ numTicks: DEFAULT_NUM_TICKS,
4111
4307
  tickFormat: yTickFormat,
4308
+ ...horizontal ? dateAxisTickValues : {},
4112
4309
  ...yLabelOverflow === "ellipsis" ? { tickComponent: TruncatedYTickComponent } : {},
4113
4310
  ...yAxisOptions
4114
4311
  }
@@ -4118,7 +4315,8 @@ function useBarChartOptions(data, horizontal, options = {}) {
4118
4315
  };
4119
4316
  }, [
4120
4317
  defaultOptions,
4121
- options,
4318
+ axisConfig,
4319
+ stableOptions,
4122
4320
  horizontal,
4123
4321
  data
4124
4322
  ]);
@@ -4280,7 +4478,13 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4280
4478
  valueAxisLength: horizontal ? width : height
4281
4479
  });
4282
4480
  const legendItems = useChartLegendItems(dataSorted, useMemo(() => ({ collapseGroups: legendCollapseGroups }), [legendCollapseGroups]));
4283
- const chartOptions = useBarChartOptions(dataWithVisibleZeros, horizontal, options);
4481
+ const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
4482
+ const isSeriesRendered = useCallback((series) => !chartId || !legendInteractive || isSeriesVisible(chartId, series.label), [
4483
+ chartId,
4484
+ legendInteractive,
4485
+ isSeriesVisible
4486
+ ]);
4487
+ const chartOptions = useBarChartOptions(dataWithVisibleZeros, horizontal, options, isSeriesRendered);
4284
4488
  const defaultMargin = useChartMargin(height, chartOptions, dataSorted, theme, horizontal);
4285
4489
  const chartRef = useRef(null);
4286
4490
  const { legendChildren, nonLegendChildren } = useChartChildren(children, "BarChart");
@@ -4299,24 +4503,11 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4299
4503
  chartRef,
4300
4504
  totalPoints: Math.max(0, ...primarySeriesForNav.map((s) => s.data?.length || 0)) * primarySeriesForNav.length
4301
4505
  });
4302
- const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
4303
- const seriesWithVisibility = useMemo(() => {
4304
- if (!chartId || !legendInteractive) return dataWithVisibleZeros.map((series, index) => ({
4305
- series,
4306
- index,
4307
- isVisible: true
4308
- }));
4309
- return dataWithVisibleZeros.map((series, index) => ({
4310
- series,
4311
- index,
4312
- isVisible: isSeriesVisible(chartId, series.label)
4313
- }));
4314
- }, [
4315
- dataWithVisibleZeros,
4316
- chartId,
4317
- isSeriesVisible,
4318
- legendInteractive
4319
- ]);
4506
+ const seriesWithVisibility = useMemo(() => dataWithVisibleZeros.map((series, index) => ({
4507
+ series,
4508
+ index,
4509
+ isVisible: isSeriesRendered(series)
4510
+ })), [dataWithVisibleZeros, isSeriesRendered]);
4320
4511
  const allSeriesHidden = useMemo(() => {
4321
4512
  return seriesWithVisibility.every(({ isVisible }) => !isVisible);
4322
4513
  }, [seriesWithVisibility]);