@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 +12 -0
- package/dist/index.cjs +292 -101
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +292 -101
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/charts/area-chart/area-chart.tsx +3 -5
- package/src/charts/bar-chart/bar-chart.tsx +24 -15
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +197 -30
- package/src/charts/bar-chart/test/bar-chart.test.tsx +685 -1
- package/src/charts/line-chart/line-chart.tsx +3 -5
- package/src/charts/private/test/time-axis.test.ts +247 -2
- package/src/charts/private/time-axis.ts +252 -63
- package/src/components/legend/test/legend.test.tsx +2 -1
- package/src/components/trend-indicator/test/trend-indicator.test.tsx +2 -1
- package/src/hooks/test/use-chart-margin.test.tsx +60 -0
- package/src/hooks/test/use-xychart-theme-style-reads.test.tsx +83 -0
- package/src/hooks/use-chart-margin.tsx +3 -4
- package/src/hooks/use-xychart-theme.ts +17 -11
- package/src/types.ts +6 -6
- package/src/utils/index.ts +1 -1
- package/src/utils/resolve-css-var.ts +69 -28
- package/src/utils/test/create-css-variable-resolver.test.ts +83 -0
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
*
|
|
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
|
|
582
|
+
* @param styles - The snapshot to read from, or null
|
|
553
583
|
* @return The computed value or null
|
|
554
584
|
*/
|
|
555
|
-
function
|
|
556
|
-
if (
|
|
585
|
+
function readCustomProperty(varName, styles) {
|
|
586
|
+
if (!styles) return null;
|
|
557
587
|
try {
|
|
558
|
-
|
|
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
|
|
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,
|
|
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 ||
|
|
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
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
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
|
|
2256
|
-
|
|
2257
|
-
|
|
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
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
week:
|
|
2270
|
-
|
|
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
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
const
|
|
2277
|
-
|
|
2278
|
-
const diffInHours = Math.abs((0, date_fns.differenceInHours)(maxX, minX));
|
|
2279
|
-
if (
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
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
|
|
4019
|
-
* @param horizontal
|
|
4020
|
-
* @param options
|
|
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
|
|
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
|
|
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:
|
|
4047
|
-
xAccessor:
|
|
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:
|
|
4244
|
+
tooltipLabelFormatter: tooltipDatumFormatter,
|
|
4057
4245
|
xAccessor: valueAccessor,
|
|
4058
|
-
yAccessor:
|
|
4246
|
+
yAccessor: bucketAccessor,
|
|
4059
4247
|
gridVisibility: "y",
|
|
4060
4248
|
xScale: linearScale,
|
|
4061
4249
|
yScale: bandScale
|
|
4062
4250
|
}
|
|
4063
4251
|
};
|
|
4064
|
-
}, [
|
|
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 ?
|
|
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
|
-
...
|
|
4275
|
+
...stableOptions.xScale || {},
|
|
4084
4276
|
...horizontal ? valueScaleDomainOverride : {}
|
|
4085
4277
|
};
|
|
4086
4278
|
const yScale = {
|
|
4087
4279
|
...baseYScale,
|
|
4088
|
-
...
|
|
4280
|
+
...stableOptions.yScale || {},
|
|
4089
4281
|
...!horizontal ? valueScaleDomainOverride : {}
|
|
4090
4282
|
};
|
|
4091
|
-
const
|
|
4092
|
-
const
|
|
4093
|
-
const {
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
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]);
|