@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/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
|
|
370
|
-
* tick formatter derives tick formats from it
|
|
371
|
-
* inferring the resolution from point spacing.
|
|
372
|
-
*
|
|
373
|
-
*
|
|
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
|
|
370
|
-
* tick formatter derives tick formats from it
|
|
371
|
-
* inferring the resolution from point spacing.
|
|
372
|
-
*
|
|
373
|
-
*
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
*
|
|
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
|
|
580
|
+
* @param styles - The snapshot to read from, or null
|
|
551
581
|
* @return The computed value or null
|
|
552
582
|
*/
|
|
553
|
-
function
|
|
554
|
-
if (
|
|
583
|
+
function readCustomProperty(varName, styles) {
|
|
584
|
+
if (!styles) return null;
|
|
555
585
|
try {
|
|
556
|
-
|
|
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
|
|
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,
|
|
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 ||
|
|
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
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
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
|
|
2254
|
-
|
|
2255
|
-
|
|
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
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
week:
|
|
2268
|
-
|
|
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
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
const
|
|
2275
|
-
|
|
2276
|
-
const diffInHours = Math.abs(differenceInHours(maxX, minX));
|
|
2277
|
-
if (
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
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
|
|
4017
|
-
* @param horizontal
|
|
4018
|
-
* @param options
|
|
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
|
|
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
|
|
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:
|
|
4045
|
-
xAccessor:
|
|
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:
|
|
4242
|
+
tooltipLabelFormatter: tooltipDatumFormatter,
|
|
4055
4243
|
xAccessor: valueAccessor,
|
|
4056
|
-
yAccessor:
|
|
4244
|
+
yAccessor: bucketAccessor,
|
|
4057
4245
|
gridVisibility: "y",
|
|
4058
4246
|
xScale: linearScale,
|
|
4059
4247
|
yScale: bandScale
|
|
4060
4248
|
}
|
|
4061
4249
|
};
|
|
4062
|
-
}, [
|
|
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 ?
|
|
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
|
-
...
|
|
4273
|
+
...stableOptions.xScale || {},
|
|
4082
4274
|
...horizontal ? valueScaleDomainOverride : {}
|
|
4083
4275
|
};
|
|
4084
4276
|
const yScale = {
|
|
4085
4277
|
...baseYScale,
|
|
4086
|
-
...
|
|
4278
|
+
...stableOptions.yScale || {},
|
|
4087
4279
|
...!horizontal ? valueScaleDomainOverride : {}
|
|
4088
4280
|
};
|
|
4089
|
-
const
|
|
4090
|
-
const
|
|
4091
|
-
const {
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
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]);
|