@automattic/charts 4.1.0 → 4.1.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 +9 -0
- package/TOKENS.md +6 -4
- package/dist/index.cjs +241 -235
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +9 -0
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.js +241 -235
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/charts/area-chart/area-chart.tsx +2 -1
- package/src/charts/area-chart/test/area-chart.test.tsx +31 -0
- package/src/charts/heatmap-chart/{private/build-calendar-data.ts → build-calendar-data.ts} +11 -35
- package/src/charts/heatmap-chart/index.ts +9 -3
- package/src/charts/heatmap-chart/private/index.ts +0 -2
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +1 -1
- package/src/charts/heatmap-chart/test/use-calendar-heatmap-data.test.tsx +1 -1
- package/src/charts/heatmap-chart/test/zoned-calendar-data.test.ts +1 -1
- package/src/charts/heatmap-chart/types.ts +29 -0
- package/src/charts/heatmap-chart/use-calendar-heatmap-data.ts +2 -2
- package/src/charts/line-chart/line-chart.tsx +2 -1
- package/src/charts/line-chart/test/line-chart.test.tsx +31 -0
- package/src/components/tooltip/accessible-tooltip.tsx +32 -10
- package/src/components/tooltip/test/accessible-tooltip.test.tsx +32 -0
- package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +22 -3
- package/src/components/tooltip/xy-chart-tooltip.tsx +2 -1
- package/src/styles/chart-scope.scss +11 -3
- package/src/styles/test/chart-scope.test.ts +35 -1
package/dist/index.css
CHANGED
|
@@ -37,6 +37,15 @@
|
|
|
37
37
|
--a8c-charts-dimension-leaderboard-row-gap: var(--wpds-dimension-gap-md, 12px);
|
|
38
38
|
--a8c-charts-dimension-leaderboard-column-gap: var(--wpds-dimension-gap-xs, 4px);
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
@media (forced-colors: active) {
|
|
42
|
+
:where(.a8c-charts-scope) {
|
|
43
|
+
--a8c-charts-color-label-axis: CanvasText;
|
|
44
|
+
--a8c-charts-color-grid: GrayText;
|
|
45
|
+
--a8c-charts-color-axis-x: GrayText;
|
|
46
|
+
--a8c-charts-color-tick-x: GrayText;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
40
49
|
.a8ccharts-04TogW-legend {
|
|
41
50
|
align-self: stretch;
|
|
42
51
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -1340,8 +1340,6 @@ interface HeatmapChartProps extends Omit<BaseChartProps<HeatmapColumn[]>, 'showL
|
|
|
1340
1340
|
renderTooltip?: (data: HeatmapTooltipData) => ReactNode;
|
|
1341
1341
|
children?: ReactNode;
|
|
1342
1342
|
}
|
|
1343
|
-
//#endregion
|
|
1344
|
-
//#region src/charts/heatmap-chart/private/build-calendar-data.d.ts
|
|
1345
1343
|
type CalendarHeatmapResult = {
|
|
1346
1344
|
data: HeatmapColumn[];
|
|
1347
1345
|
rowLabels: string[];
|
|
@@ -1372,14 +1370,6 @@ type CalendarHeatmapOptions = {
|
|
|
1372
1370
|
*/
|
|
1373
1371
|
timeZone?: string;
|
|
1374
1372
|
};
|
|
1375
|
-
/**
|
|
1376
|
-
* Lay a day-bucketed series out as calendar columns.
|
|
1377
|
-
*
|
|
1378
|
-
* @param series - Points to bucket.
|
|
1379
|
-
* @param options - Grid shape, plus the locale and zone to read days in.
|
|
1380
|
-
* @return Columns and row labels for `HeatmapChart`.
|
|
1381
|
-
*/
|
|
1382
|
-
declare const buildCalendarHeatmapData: (series: DataPointDate[], options?: CalendarHeatmapOptions) => CalendarHeatmapResult;
|
|
1383
1373
|
//#endregion
|
|
1384
1374
|
//#region src/charts/heatmap-chart/private/heatmap-legend.d.ts
|
|
1385
1375
|
interface HeatmapLegendProps {
|
|
@@ -1397,6 +1387,16 @@ interface HeatmapChartSubComponents {
|
|
|
1397
1387
|
declare const HeatmapChart: FC<HeatmapChartProps> & HeatmapChartSubComponents;
|
|
1398
1388
|
declare const HeatmapChartResponsive: FC<HeatmapChartProps & ResponsiveConfig> & HeatmapChartSubComponents;
|
|
1399
1389
|
//#endregion
|
|
1390
|
+
//#region src/charts/heatmap-chart/build-calendar-data.d.ts
|
|
1391
|
+
/**
|
|
1392
|
+
* Lay a day-bucketed series out as calendar columns.
|
|
1393
|
+
*
|
|
1394
|
+
* @param series - Points to bucket.
|
|
1395
|
+
* @param options - Grid shape, plus the locale and zone to read days in.
|
|
1396
|
+
* @return Columns and row labels for `HeatmapChart`.
|
|
1397
|
+
*/
|
|
1398
|
+
declare const buildCalendarHeatmapData: (series: DataPointDate[], options?: CalendarHeatmapOptions) => CalendarHeatmapResult;
|
|
1399
|
+
//#endregion
|
|
1400
1400
|
//#region src/charts/heatmap-chart/use-calendar-heatmap-data.d.ts
|
|
1401
1401
|
/**
|
|
1402
1402
|
* `buildCalendarHeatmapData` with the locale and zone taken from
|
package/dist/index.d.ts
CHANGED
|
@@ -1340,8 +1340,6 @@ interface HeatmapChartProps extends Omit<BaseChartProps<HeatmapColumn[]>, 'showL
|
|
|
1340
1340
|
renderTooltip?: (data: HeatmapTooltipData) => ReactNode;
|
|
1341
1341
|
children?: ReactNode;
|
|
1342
1342
|
}
|
|
1343
|
-
//#endregion
|
|
1344
|
-
//#region src/charts/heatmap-chart/private/build-calendar-data.d.ts
|
|
1345
1343
|
type CalendarHeatmapResult = {
|
|
1346
1344
|
data: HeatmapColumn[];
|
|
1347
1345
|
rowLabels: string[];
|
|
@@ -1372,14 +1370,6 @@ type CalendarHeatmapOptions = {
|
|
|
1372
1370
|
*/
|
|
1373
1371
|
timeZone?: string;
|
|
1374
1372
|
};
|
|
1375
|
-
/**
|
|
1376
|
-
* Lay a day-bucketed series out as calendar columns.
|
|
1377
|
-
*
|
|
1378
|
-
* @param series - Points to bucket.
|
|
1379
|
-
* @param options - Grid shape, plus the locale and zone to read days in.
|
|
1380
|
-
* @return Columns and row labels for `HeatmapChart`.
|
|
1381
|
-
*/
|
|
1382
|
-
declare const buildCalendarHeatmapData: (series: DataPointDate[], options?: CalendarHeatmapOptions) => CalendarHeatmapResult;
|
|
1383
1373
|
//#endregion
|
|
1384
1374
|
//#region src/charts/heatmap-chart/private/heatmap-legend.d.ts
|
|
1385
1375
|
interface HeatmapLegendProps {
|
|
@@ -1397,6 +1387,16 @@ interface HeatmapChartSubComponents {
|
|
|
1397
1387
|
declare const HeatmapChart: FC<HeatmapChartProps> & HeatmapChartSubComponents;
|
|
1398
1388
|
declare const HeatmapChartResponsive: FC<HeatmapChartProps & ResponsiveConfig> & HeatmapChartSubComponents;
|
|
1399
1389
|
//#endregion
|
|
1390
|
+
//#region src/charts/heatmap-chart/build-calendar-data.d.ts
|
|
1391
|
+
/**
|
|
1392
|
+
* Lay a day-bucketed series out as calendar columns.
|
|
1393
|
+
*
|
|
1394
|
+
* @param series - Points to bucket.
|
|
1395
|
+
* @param options - Grid shape, plus the locale and zone to read days in.
|
|
1396
|
+
* @return Columns and row labels for `HeatmapChart`.
|
|
1397
|
+
*/
|
|
1398
|
+
declare const buildCalendarHeatmapData: (series: DataPointDate[], options?: CalendarHeatmapOptions) => CalendarHeatmapResult;
|
|
1399
|
+
//#endregion
|
|
1400
1400
|
//#region src/charts/heatmap-chart/use-calendar-heatmap-data.d.ts
|
|
1401
1401
|
/**
|
|
1402
1402
|
* `buildCalendarHeatmapData` with the locale and zone taken from
|
package/dist/index.js
CHANGED
|
@@ -2487,7 +2487,7 @@ const XyChartTooltipContent = ({ tooltipContext, container, renderTooltip, rende
|
|
|
2487
2487
|
...defaultStyles,
|
|
2488
2488
|
zIndex,
|
|
2489
2489
|
backgroundColor: theme?.backgroundColor ?? "white",
|
|
2490
|
-
boxShadow: `0 1px 2px ${theme?.htmlLabel?.color ? `${theme.htmlLabel.color}55` : "#22222255"}`,
|
|
2490
|
+
boxShadow: `0 1px 2px ${isValidHexColor(theme?.htmlLabel?.color) ? `${theme.htmlLabel.color}55` : "#22222255"}`,
|
|
2491
2491
|
...theme?.htmlLabel,
|
|
2492
2492
|
...style
|
|
2493
2493
|
};
|
|
@@ -2670,7 +2670,8 @@ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating,
|
|
|
2670
2670
|
else element.focus();
|
|
2671
2671
|
}
|
|
2672
2672
|
}, [preventTooltipScroll, selectedIndex]),
|
|
2673
|
-
onChartFocus: useCallback(() => {
|
|
2673
|
+
onChartFocus: useCallback((event) => {
|
|
2674
|
+
if (event.currentTarget.contains(event.relatedTarget)) return;
|
|
2674
2675
|
if (!isNavigating && selectedIndex !== void 0) setSelectedIndex(0);
|
|
2675
2676
|
}, [
|
|
2676
2677
|
isNavigating,
|
|
@@ -2682,15 +2683,19 @@ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating,
|
|
|
2682
2683
|
}, [setIsNavigating]),
|
|
2683
2684
|
onChartKeyDown: useCallback((event) => {
|
|
2684
2685
|
if (totalPoints === 0) return;
|
|
2686
|
+
const focusChart = () => {
|
|
2687
|
+
if (preventTooltipScroll) chartRef.current?.focus({ preventScroll: true });
|
|
2688
|
+
else chartRef.current?.focus();
|
|
2689
|
+
};
|
|
2685
2690
|
if (event.key === "Tab") {
|
|
2686
|
-
|
|
2691
|
+
focusChart();
|
|
2687
2692
|
setSelectedIndex(void 0);
|
|
2688
2693
|
setIsNavigating(false);
|
|
2689
2694
|
return;
|
|
2690
2695
|
}
|
|
2691
2696
|
const currentSelectedIndex = selectedIndex === void 0 ? -1 : selectedIndex;
|
|
2692
2697
|
if (currentSelectedIndex + 1 >= totalPoints && ["ArrowRight"].includes(event.key)) {
|
|
2693
|
-
|
|
2698
|
+
focusChart();
|
|
2694
2699
|
setSelectedIndex(void 0);
|
|
2695
2700
|
setIsNavigating(false);
|
|
2696
2701
|
return;
|
|
@@ -2705,7 +2710,7 @@ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating,
|
|
|
2705
2710
|
} else if (event.key === "Escape") {
|
|
2706
2711
|
setSelectedIndex(void 0);
|
|
2707
2712
|
setIsNavigating(false);
|
|
2708
|
-
|
|
2713
|
+
focusChart();
|
|
2709
2714
|
} else if ((event.key === "Enter" || event.key === " ") && selectedIndex !== void 0) onActivate?.(selectedIndex);
|
|
2710
2715
|
}, [
|
|
2711
2716
|
totalPoints,
|
|
@@ -2713,7 +2718,8 @@ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating,
|
|
|
2713
2718
|
setSelectedIndex,
|
|
2714
2719
|
setIsNavigating,
|
|
2715
2720
|
chartRef,
|
|
2716
|
-
onActivate
|
|
2721
|
+
onActivate,
|
|
2722
|
+
preventTooltipScroll
|
|
2717
2723
|
])
|
|
2718
2724
|
};
|
|
2719
2725
|
};
|
|
@@ -4316,6 +4322,7 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
|
|
|
4316
4322
|
children: ({ contentHeight }) => {
|
|
4317
4323
|
const chartHeight = contentHeight > 0 ? contentHeight : height;
|
|
4318
4324
|
return /* @__PURE__ */ jsx("div", {
|
|
4325
|
+
ref: chartRef,
|
|
4319
4326
|
role: "grid",
|
|
4320
4327
|
"aria-label": __("Line chart", "jetpack-charts"),
|
|
4321
4328
|
tabIndex: 0,
|
|
@@ -4323,7 +4330,6 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
|
|
|
4323
4330
|
onFocus: onChartFocus,
|
|
4324
4331
|
onBlur: onChartBlur,
|
|
4325
4332
|
children: chartHeight > 0 && /* @__PURE__ */ jsxs("div", {
|
|
4326
|
-
ref: chartRef,
|
|
4327
4333
|
className: xy_plot_module_default["xy-plot"],
|
|
4328
4334
|
children: [zoomable && zoom.domain && /* @__PURE__ */ jsx(ZoomResetButton, { onClick: zoom.reset }), /* @__PURE__ */ jsxs(XYChart, {
|
|
4329
4335
|
theme,
|
|
@@ -4821,6 +4827,7 @@ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
|
|
|
4821
4827
|
children: ({ contentHeight }) => {
|
|
4822
4828
|
const chartHeight = contentHeight > 0 ? contentHeight : height;
|
|
4823
4829
|
return /* @__PURE__ */ jsx("div", {
|
|
4830
|
+
ref: chartRef,
|
|
4824
4831
|
role: "grid",
|
|
4825
4832
|
"aria-label": __("Area chart", "jetpack-charts"),
|
|
4826
4833
|
tabIndex: 0,
|
|
@@ -4828,7 +4835,6 @@ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
|
|
|
4828
4835
|
onFocus: onChartFocus,
|
|
4829
4836
|
onBlur: onChartBlur,
|
|
4830
4837
|
children: chartHeight > 0 && /* @__PURE__ */ jsxs("div", {
|
|
4831
|
-
ref: chartRef,
|
|
4832
4838
|
className: xy_plot_module_default["xy-plot"],
|
|
4833
4839
|
children: [zoomable && zoom.domain && /* @__PURE__ */ jsx(ZoomResetButton, { onClick: zoom.reset }), /* @__PURE__ */ jsxs(XYChart, {
|
|
4834
4840
|
theme,
|
|
@@ -6632,233 +6638,6 @@ const getNormalizedValue = (value, extent) => {
|
|
|
6632
6638
|
return Math.min(1, Math.max(0, (value - min) / (max - min)));
|
|
6633
6639
|
};
|
|
6634
6640
|
//#endregion
|
|
6635
|
-
//#region src/charts/heatmap-chart/private/civil-day.ts
|
|
6636
|
-
const DAY_MS = 864e5;
|
|
6637
|
-
const KEY_OPTIONS = {
|
|
6638
|
-
calendar: "gregory",
|
|
6639
|
-
numberingSystem: "latn",
|
|
6640
|
-
year: "numeric",
|
|
6641
|
-
month: "2-digit",
|
|
6642
|
-
day: "2-digit"
|
|
6643
|
-
};
|
|
6644
|
-
const DATE_PREFIX = /^(\d{4})-(\d{2})-(\d{2})/;
|
|
6645
|
-
const pad = (value, length) => String(value).padStart(length, "0");
|
|
6646
|
-
/**
|
|
6647
|
-
* The day a UTC proxy stands for.
|
|
6648
|
-
*
|
|
6649
|
-
* @param date - UTC proxy.
|
|
6650
|
-
* @return Its day key.
|
|
6651
|
-
*/
|
|
6652
|
-
const civilKey = (date) => `${pad(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1, 2)}-${pad(date.getUTCDate(), 2)}`;
|
|
6653
|
-
/**
|
|
6654
|
-
* A day key as a UTC-midnight proxy. UTC has no DST, so every step below is exactly
|
|
6655
|
-
* `DAY_MS`.
|
|
6656
|
-
*
|
|
6657
|
-
* @param key - Day key.
|
|
6658
|
-
* @return The proxy, or null when the key names no real day.
|
|
6659
|
-
*/
|
|
6660
|
-
const civilDate = (key) => {
|
|
6661
|
-
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(key);
|
|
6662
|
-
if (!match) return null;
|
|
6663
|
-
const date = /* @__PURE__ */ new Date(0);
|
|
6664
|
-
date.setUTCFullYear(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
6665
|
-
date.setUTCHours(0, 0, 0, 0);
|
|
6666
|
-
return civilKey(date) === key ? date : null;
|
|
6667
|
-
};
|
|
6668
|
-
/**
|
|
6669
|
-
* The day a written date names, ignoring any time and zone it carries.
|
|
6670
|
-
*
|
|
6671
|
-
* @param text - `yyyy-MM-dd`, optionally followed by a time.
|
|
6672
|
-
* @return Its day key, or null.
|
|
6673
|
-
*/
|
|
6674
|
-
const writtenDayKey = (text) => {
|
|
6675
|
-
const match = DATE_PREFIX.exec(text);
|
|
6676
|
-
if (!match) return null;
|
|
6677
|
-
const key = `${match[1]}-${match[2]}-${match[3]}`;
|
|
6678
|
-
return civilDate(key) ? key : null;
|
|
6679
|
-
};
|
|
6680
|
-
/**
|
|
6681
|
-
* Reads the day an instant falls on in a zone.
|
|
6682
|
-
*
|
|
6683
|
-
* @param timeZone - IANA zone name; absent leaves the runtime's own.
|
|
6684
|
-
* @return Reader taking a `Date`, null for a day no key can hold.
|
|
6685
|
-
*/
|
|
6686
|
-
const instantDayReader = (timeZone) => {
|
|
6687
|
-
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
6688
|
-
...KEY_OPTIONS,
|
|
6689
|
-
timeZone
|
|
6690
|
-
});
|
|
6691
|
-
return (date) => {
|
|
6692
|
-
const parts = formatter.formatToParts(date);
|
|
6693
|
-
const read = (type) => parts.find((part) => part.type === type)?.value ?? "";
|
|
6694
|
-
const key = `${pad(Number(read("year")), 4)}-${read("month")}-${read("day")}`;
|
|
6695
|
-
return civilDate(key) ? key : null;
|
|
6696
|
-
};
|
|
6697
|
-
};
|
|
6698
|
-
/**
|
|
6699
|
-
* The day a series point belongs to.
|
|
6700
|
-
*
|
|
6701
|
-
* A `Date`, or a string carrying `Z` or an offset, is an instant read in the host's
|
|
6702
|
-
* zone. Anything else is a wall clock the host wrote, so its date part is the day.
|
|
6703
|
-
*
|
|
6704
|
-
* @param point - Series point.
|
|
6705
|
-
* @param readInstant - Reader from `instantDayReader`.
|
|
6706
|
-
* @return Its day key, or null when the point carries no usable date.
|
|
6707
|
-
*/
|
|
6708
|
-
const pointDayKey = (point, readInstant) => {
|
|
6709
|
-
if (point.date instanceof Date && !isNaN(point.date.getTime())) {
|
|
6710
|
-
const key = readInstant(point.date);
|
|
6711
|
-
if (key) return key;
|
|
6712
|
-
}
|
|
6713
|
-
const text = point.dateString;
|
|
6714
|
-
if (!text) return null;
|
|
6715
|
-
if (!hasTimezone(text)) return writtenDayKey(text);
|
|
6716
|
-
const parsed = parseISO(text);
|
|
6717
|
-
return isNaN(parsed.getTime()) ? null : readInstant(parsed);
|
|
6718
|
-
};
|
|
6719
|
-
/**
|
|
6720
|
-
* Steps a proxy by whole days.
|
|
6721
|
-
*
|
|
6722
|
-
* @param date - UTC proxy.
|
|
6723
|
-
* @param days - Days to add; may be negative.
|
|
6724
|
-
* @return The shifted proxy.
|
|
6725
|
-
*/
|
|
6726
|
-
const addCivilDays = (date, days) => new Date(date.getTime() + days * DAY_MS);
|
|
6727
|
-
/**
|
|
6728
|
-
* Rounds a proxy down to its week.
|
|
6729
|
-
*
|
|
6730
|
-
* @param date - UTC proxy.
|
|
6731
|
-
* @param weekStartsOn - 0 for Sunday, 1 for Monday.
|
|
6732
|
-
* @return The proxy for that week's first day.
|
|
6733
|
-
*/
|
|
6734
|
-
const startOfCivilWeek = (date, weekStartsOn) => {
|
|
6735
|
-
const daysSinceWeekStart = (date.getUTCDay() - weekStartsOn + 7) % 7;
|
|
6736
|
-
return addCivilDays(date, -daysSinceWeekStart);
|
|
6737
|
-
};
|
|
6738
|
-
/**
|
|
6739
|
-
* Counts the columns a span needs.
|
|
6740
|
-
*
|
|
6741
|
-
* @param from - Earlier UTC proxy.
|
|
6742
|
-
* @param to - Later UTC proxy.
|
|
6743
|
-
* @param weekStartsOn - 0 for Sunday, 1 for Monday.
|
|
6744
|
-
* @return Columns needed to cover the span, inclusive.
|
|
6745
|
-
*/
|
|
6746
|
-
const civilWeekSpan = (from, to, weekStartsOn) => Math.floor((startOfCivilWeek(to, weekStartsOn).getTime() - startOfCivilWeek(from, weekStartsOn).getTime()) / (7 * DAY_MS)) + 1;
|
|
6747
|
-
//#endregion
|
|
6748
|
-
//#region src/charts/heatmap-chart/private/build-calendar-data.ts
|
|
6749
|
-
/** Rows that get a weekday label (Mon, Wed, Fri with a Monday week start). */
|
|
6750
|
-
const LABELLED_ROWS = [
|
|
6751
|
-
0,
|
|
6752
|
-
2,
|
|
6753
|
-
4
|
|
6754
|
-
];
|
|
6755
|
-
/**
|
|
6756
|
-
* Resolve one grid bound: an unparseable or narrowing value falls back to the
|
|
6757
|
-
* series' own bound, so the grid can only ever be widened.
|
|
6758
|
-
*
|
|
6759
|
-
* @param bound - Requested bound as `yyyy-MM-dd`, if any.
|
|
6760
|
-
* @param fallback - The series' own bound on this side.
|
|
6761
|
-
* @param direction - Which way the bound is allowed to move.
|
|
6762
|
-
* @return The day key to draw to.
|
|
6763
|
-
*/
|
|
6764
|
-
const widenTo = (bound, fallback, direction) => {
|
|
6765
|
-
const key = bound ? writtenDayKey(bound) : null;
|
|
6766
|
-
if (!key) {
|
|
6767
|
-
if (bound) warnOnce(`heatmap:gridSpan:${bound}`, `gridSpan.${direction === "earlier" ? "start" : "end"} ${JSON.stringify(bound)} is not a \`yyyy-MM-dd\` day, so the grid is drawn over the series' own span.`);
|
|
6768
|
-
return fallback;
|
|
6769
|
-
}
|
|
6770
|
-
if (direction === "earlier") return key < fallback ? key : fallback;
|
|
6771
|
-
return key > fallback ? key : fallback;
|
|
6772
|
-
};
|
|
6773
|
-
/**
|
|
6774
|
-
* Lay a day-bucketed series out as calendar columns.
|
|
6775
|
-
*
|
|
6776
|
-
* @param series - Points to bucket.
|
|
6777
|
-
* @param options - Grid shape, plus the locale and zone to read days in.
|
|
6778
|
-
* @return Columns and row labels for `HeatmapChart`.
|
|
6779
|
-
*/
|
|
6780
|
-
const buildCalendarHeatmapData = (series, options = {}) => {
|
|
6781
|
-
const weekStartsOn = options.weekStartsOn ?? 1;
|
|
6782
|
-
const hideOutOfRangeDays = options.hideOutOfRangeDays ?? true;
|
|
6783
|
-
const { locale, timeZone } = sanitizeFormatting({
|
|
6784
|
-
locale: options.locale,
|
|
6785
|
-
timeZone: options.timeZone
|
|
6786
|
-
});
|
|
6787
|
-
const readInstant = instantDayReader(timeZone);
|
|
6788
|
-
const valueByDay = /* @__PURE__ */ new Map();
|
|
6789
|
-
let minDayKey;
|
|
6790
|
-
let maxDayKey;
|
|
6791
|
-
for (const point of series) {
|
|
6792
|
-
const key = pointDayKey(point, readInstant);
|
|
6793
|
-
if (!key) {
|
|
6794
|
-
const offending = point.dateString ?? (point.date && String(point.date));
|
|
6795
|
-
warnOnce(`heatmap:unreadableDate:${offending}`, offending ? `${JSON.stringify(offending)} is not a day this can read, so its point is left out of the calendar. A \`dateString\` must start \`yyyy-MM-dd\`.` : "A point carries neither `date` nor `dateString`, so it is left out of the calendar.");
|
|
6796
|
-
continue;
|
|
6797
|
-
}
|
|
6798
|
-
valueByDay.set(key, point.value);
|
|
6799
|
-
if (!minDayKey || key < minDayKey) minDayKey = key;
|
|
6800
|
-
if (!maxDayKey || key > maxDayKey) maxDayKey = key;
|
|
6801
|
-
}
|
|
6802
|
-
if (!minDayKey || !maxDayKey) return {
|
|
6803
|
-
data: [],
|
|
6804
|
-
rowLabels: []
|
|
6805
|
-
};
|
|
6806
|
-
const requestedMinDayKey = widenTo(options.gridSpan?.start, minDayKey, "earlier");
|
|
6807
|
-
const gridMaxDayKey = widenTo(options.gridSpan?.end, maxDayKey, "later");
|
|
6808
|
-
const requestedMinDate = civilDate(requestedMinDayKey);
|
|
6809
|
-
const gridMaxDate = civilDate(gridMaxDayKey);
|
|
6810
|
-
const labelFormatting = {
|
|
6811
|
-
locale,
|
|
6812
|
-
timeZone: "UTC"
|
|
6813
|
-
};
|
|
6814
|
-
const formatWeekday = createDateFormatter({ weekday: "short" }, labelFormatting);
|
|
6815
|
-
const formatMonth = createDateFormatter({ month: "short" }, labelFormatting);
|
|
6816
|
-
const formatDay = createDateFormatter({
|
|
6817
|
-
weekday: "short",
|
|
6818
|
-
month: "short",
|
|
6819
|
-
day: "numeric",
|
|
6820
|
-
year: "numeric"
|
|
6821
|
-
}, labelFormatting);
|
|
6822
|
-
const gridStart = startOfCivilWeek(requestedMinDate, weekStartsOn);
|
|
6823
|
-
const gridMinDayKey = requestedMinDayKey < minDayKey ? civilKey(gridStart) : requestedMinDayKey;
|
|
6824
|
-
const weekCount = civilWeekSpan(gridStart, gridMaxDate, weekStartsOn);
|
|
6825
|
-
const rowLabels = Array.from({ length: 7 }, (_, row) => LABELLED_ROWS.includes(row) ? formatWeekday(addCivilDays(gridStart, row)) : "");
|
|
6826
|
-
const MIN_FIRST_MONTH_WEEKS = 2;
|
|
6827
|
-
const firstMonth = gridStart.getUTCMonth();
|
|
6828
|
-
let firstMonthWeeks = 0;
|
|
6829
|
-
while (firstMonthWeeks < weekCount && addCivilDays(gridStart, firstMonthWeeks * 7).getUTCMonth() === firstMonth) firstMonthWeeks++;
|
|
6830
|
-
const showFirstMonthLabel = !(firstMonthWeeks < weekCount) || firstMonthWeeks >= MIN_FIRST_MONTH_WEEKS;
|
|
6831
|
-
const data = [];
|
|
6832
|
-
let previousMonth = -1;
|
|
6833
|
-
for (let week = 0; week < weekCount; week++) {
|
|
6834
|
-
const columnStart = addCivilDays(gridStart, week * 7);
|
|
6835
|
-
const month = columnStart.getUTCMonth();
|
|
6836
|
-
const label = month !== previousMonth && (week !== 0 || showFirstMonthLabel) ? formatMonth(columnStart) : "";
|
|
6837
|
-
previousMonth = month;
|
|
6838
|
-
const cells = [];
|
|
6839
|
-
for (let row = 0; row < 7; row++) {
|
|
6840
|
-
const day = addCivilDays(gridStart, week * 7 + row);
|
|
6841
|
-
const key = civilKey(day);
|
|
6842
|
-
const cell = {
|
|
6843
|
-
label: formatDay(day),
|
|
6844
|
-
value: valueByDay.has(key) ? valueByDay.get(key) : null
|
|
6845
|
-
};
|
|
6846
|
-
if (key < gridMinDayKey || key > gridMaxDayKey) {
|
|
6847
|
-
if (hideOutOfRangeDays) cell.hidden = true;
|
|
6848
|
-
} else if (key < minDayKey || key > maxDayKey) cell.placeholder = true;
|
|
6849
|
-
cells.push(cell);
|
|
6850
|
-
}
|
|
6851
|
-
data.push({
|
|
6852
|
-
label,
|
|
6853
|
-
data: cells
|
|
6854
|
-
});
|
|
6855
|
-
}
|
|
6856
|
-
return {
|
|
6857
|
-
data,
|
|
6858
|
-
rowLabels
|
|
6859
|
-
};
|
|
6860
|
-
};
|
|
6861
|
-
//#endregion
|
|
6862
6641
|
//#region src/charts/heatmap-chart/private/heatmap-context.ts
|
|
6863
6642
|
/** Shared by the chart and legend without importing back from `heatmap-chart.tsx`. */
|
|
6864
6643
|
const HeatmapContext = createContext(null);
|
|
@@ -7198,6 +6977,233 @@ const HeatmapChartResponsiveInner = (props) => /* @__PURE__ */ jsx(HeatmapChartW
|
|
|
7198
6977
|
HeatmapChartResponsiveInner.displayName = "HeatmapChart";
|
|
7199
6978
|
const HeatmapChartResponsive = attachSubComponents(withResponsive(HeatmapChartResponsiveInner), { Legend: HeatmapLegend });
|
|
7200
6979
|
//#endregion
|
|
6980
|
+
//#region src/charts/heatmap-chart/private/civil-day.ts
|
|
6981
|
+
const DAY_MS = 864e5;
|
|
6982
|
+
const KEY_OPTIONS = {
|
|
6983
|
+
calendar: "gregory",
|
|
6984
|
+
numberingSystem: "latn",
|
|
6985
|
+
year: "numeric",
|
|
6986
|
+
month: "2-digit",
|
|
6987
|
+
day: "2-digit"
|
|
6988
|
+
};
|
|
6989
|
+
const DATE_PREFIX = /^(\d{4})-(\d{2})-(\d{2})/;
|
|
6990
|
+
const pad = (value, length) => String(value).padStart(length, "0");
|
|
6991
|
+
/**
|
|
6992
|
+
* The day a UTC proxy stands for.
|
|
6993
|
+
*
|
|
6994
|
+
* @param date - UTC proxy.
|
|
6995
|
+
* @return Its day key.
|
|
6996
|
+
*/
|
|
6997
|
+
const civilKey = (date) => `${pad(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1, 2)}-${pad(date.getUTCDate(), 2)}`;
|
|
6998
|
+
/**
|
|
6999
|
+
* A day key as a UTC-midnight proxy. UTC has no DST, so every step below is exactly
|
|
7000
|
+
* `DAY_MS`.
|
|
7001
|
+
*
|
|
7002
|
+
* @param key - Day key.
|
|
7003
|
+
* @return The proxy, or null when the key names no real day.
|
|
7004
|
+
*/
|
|
7005
|
+
const civilDate = (key) => {
|
|
7006
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(key);
|
|
7007
|
+
if (!match) return null;
|
|
7008
|
+
const date = /* @__PURE__ */ new Date(0);
|
|
7009
|
+
date.setUTCFullYear(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
7010
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
7011
|
+
return civilKey(date) === key ? date : null;
|
|
7012
|
+
};
|
|
7013
|
+
/**
|
|
7014
|
+
* The day a written date names, ignoring any time and zone it carries.
|
|
7015
|
+
*
|
|
7016
|
+
* @param text - `yyyy-MM-dd`, optionally followed by a time.
|
|
7017
|
+
* @return Its day key, or null.
|
|
7018
|
+
*/
|
|
7019
|
+
const writtenDayKey = (text) => {
|
|
7020
|
+
const match = DATE_PREFIX.exec(text);
|
|
7021
|
+
if (!match) return null;
|
|
7022
|
+
const key = `${match[1]}-${match[2]}-${match[3]}`;
|
|
7023
|
+
return civilDate(key) ? key : null;
|
|
7024
|
+
};
|
|
7025
|
+
/**
|
|
7026
|
+
* Reads the day an instant falls on in a zone.
|
|
7027
|
+
*
|
|
7028
|
+
* @param timeZone - IANA zone name; absent leaves the runtime's own.
|
|
7029
|
+
* @return Reader taking a `Date`, null for a day no key can hold.
|
|
7030
|
+
*/
|
|
7031
|
+
const instantDayReader = (timeZone) => {
|
|
7032
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
7033
|
+
...KEY_OPTIONS,
|
|
7034
|
+
timeZone
|
|
7035
|
+
});
|
|
7036
|
+
return (date) => {
|
|
7037
|
+
const parts = formatter.formatToParts(date);
|
|
7038
|
+
const read = (type) => parts.find((part) => part.type === type)?.value ?? "";
|
|
7039
|
+
const key = `${pad(Number(read("year")), 4)}-${read("month")}-${read("day")}`;
|
|
7040
|
+
return civilDate(key) ? key : null;
|
|
7041
|
+
};
|
|
7042
|
+
};
|
|
7043
|
+
/**
|
|
7044
|
+
* The day a series point belongs to.
|
|
7045
|
+
*
|
|
7046
|
+
* A `Date`, or a string carrying `Z` or an offset, is an instant read in the host's
|
|
7047
|
+
* zone. Anything else is a wall clock the host wrote, so its date part is the day.
|
|
7048
|
+
*
|
|
7049
|
+
* @param point - Series point.
|
|
7050
|
+
* @param readInstant - Reader from `instantDayReader`.
|
|
7051
|
+
* @return Its day key, or null when the point carries no usable date.
|
|
7052
|
+
*/
|
|
7053
|
+
const pointDayKey = (point, readInstant) => {
|
|
7054
|
+
if (point.date instanceof Date && !isNaN(point.date.getTime())) {
|
|
7055
|
+
const key = readInstant(point.date);
|
|
7056
|
+
if (key) return key;
|
|
7057
|
+
}
|
|
7058
|
+
const text = point.dateString;
|
|
7059
|
+
if (!text) return null;
|
|
7060
|
+
if (!hasTimezone(text)) return writtenDayKey(text);
|
|
7061
|
+
const parsed = parseISO(text);
|
|
7062
|
+
return isNaN(parsed.getTime()) ? null : readInstant(parsed);
|
|
7063
|
+
};
|
|
7064
|
+
/**
|
|
7065
|
+
* Steps a proxy by whole days.
|
|
7066
|
+
*
|
|
7067
|
+
* @param date - UTC proxy.
|
|
7068
|
+
* @param days - Days to add; may be negative.
|
|
7069
|
+
* @return The shifted proxy.
|
|
7070
|
+
*/
|
|
7071
|
+
const addCivilDays = (date, days) => new Date(date.getTime() + days * DAY_MS);
|
|
7072
|
+
/**
|
|
7073
|
+
* Rounds a proxy down to its week.
|
|
7074
|
+
*
|
|
7075
|
+
* @param date - UTC proxy.
|
|
7076
|
+
* @param weekStartsOn - 0 for Sunday, 1 for Monday.
|
|
7077
|
+
* @return The proxy for that week's first day.
|
|
7078
|
+
*/
|
|
7079
|
+
const startOfCivilWeek = (date, weekStartsOn) => {
|
|
7080
|
+
const daysSinceWeekStart = (date.getUTCDay() - weekStartsOn + 7) % 7;
|
|
7081
|
+
return addCivilDays(date, -daysSinceWeekStart);
|
|
7082
|
+
};
|
|
7083
|
+
/**
|
|
7084
|
+
* Counts the columns a span needs.
|
|
7085
|
+
*
|
|
7086
|
+
* @param from - Earlier UTC proxy.
|
|
7087
|
+
* @param to - Later UTC proxy.
|
|
7088
|
+
* @param weekStartsOn - 0 for Sunday, 1 for Monday.
|
|
7089
|
+
* @return Columns needed to cover the span, inclusive.
|
|
7090
|
+
*/
|
|
7091
|
+
const civilWeekSpan = (from, to, weekStartsOn) => Math.floor((startOfCivilWeek(to, weekStartsOn).getTime() - startOfCivilWeek(from, weekStartsOn).getTime()) / (7 * DAY_MS)) + 1;
|
|
7092
|
+
//#endregion
|
|
7093
|
+
//#region src/charts/heatmap-chart/build-calendar-data.ts
|
|
7094
|
+
/** Rows that get a weekday label (Mon, Wed, Fri with a Monday week start). */
|
|
7095
|
+
const LABELLED_ROWS = [
|
|
7096
|
+
0,
|
|
7097
|
+
2,
|
|
7098
|
+
4
|
|
7099
|
+
];
|
|
7100
|
+
/**
|
|
7101
|
+
* Resolve one grid bound: an unparseable or narrowing value falls back to the
|
|
7102
|
+
* series' own bound, so the grid can only ever be widened.
|
|
7103
|
+
*
|
|
7104
|
+
* @param bound - Requested bound as `yyyy-MM-dd`, if any.
|
|
7105
|
+
* @param fallback - The series' own bound on this side.
|
|
7106
|
+
* @param direction - Which way the bound is allowed to move.
|
|
7107
|
+
* @return The day key to draw to.
|
|
7108
|
+
*/
|
|
7109
|
+
const widenTo = (bound, fallback, direction) => {
|
|
7110
|
+
const key = bound ? writtenDayKey(bound) : null;
|
|
7111
|
+
if (!key) {
|
|
7112
|
+
if (bound) warnOnce(`heatmap:gridSpan:${bound}`, `gridSpan.${direction === "earlier" ? "start" : "end"} ${JSON.stringify(bound)} is not a \`yyyy-MM-dd\` day, so the grid is drawn over the series' own span.`);
|
|
7113
|
+
return fallback;
|
|
7114
|
+
}
|
|
7115
|
+
if (direction === "earlier") return key < fallback ? key : fallback;
|
|
7116
|
+
return key > fallback ? key : fallback;
|
|
7117
|
+
};
|
|
7118
|
+
/**
|
|
7119
|
+
* Lay a day-bucketed series out as calendar columns.
|
|
7120
|
+
*
|
|
7121
|
+
* @param series - Points to bucket.
|
|
7122
|
+
* @param options - Grid shape, plus the locale and zone to read days in.
|
|
7123
|
+
* @return Columns and row labels for `HeatmapChart`.
|
|
7124
|
+
*/
|
|
7125
|
+
const buildCalendarHeatmapData = (series, options = {}) => {
|
|
7126
|
+
const weekStartsOn = options.weekStartsOn ?? 1;
|
|
7127
|
+
const hideOutOfRangeDays = options.hideOutOfRangeDays ?? true;
|
|
7128
|
+
const { locale, timeZone } = sanitizeFormatting({
|
|
7129
|
+
locale: options.locale,
|
|
7130
|
+
timeZone: options.timeZone
|
|
7131
|
+
});
|
|
7132
|
+
const readInstant = instantDayReader(timeZone);
|
|
7133
|
+
const valueByDay = /* @__PURE__ */ new Map();
|
|
7134
|
+
let minDayKey;
|
|
7135
|
+
let maxDayKey;
|
|
7136
|
+
for (const point of series) {
|
|
7137
|
+
const key = pointDayKey(point, readInstant);
|
|
7138
|
+
if (!key) {
|
|
7139
|
+
const offending = point.dateString ?? (point.date && String(point.date));
|
|
7140
|
+
warnOnce(`heatmap:unreadableDate:${offending}`, offending ? `${JSON.stringify(offending)} is not a day this can read, so its point is left out of the calendar. A \`dateString\` must start \`yyyy-MM-dd\`.` : "A point carries neither `date` nor `dateString`, so it is left out of the calendar.");
|
|
7141
|
+
continue;
|
|
7142
|
+
}
|
|
7143
|
+
valueByDay.set(key, point.value);
|
|
7144
|
+
if (!minDayKey || key < minDayKey) minDayKey = key;
|
|
7145
|
+
if (!maxDayKey || key > maxDayKey) maxDayKey = key;
|
|
7146
|
+
}
|
|
7147
|
+
if (!minDayKey || !maxDayKey) return {
|
|
7148
|
+
data: [],
|
|
7149
|
+
rowLabels: []
|
|
7150
|
+
};
|
|
7151
|
+
const requestedMinDayKey = widenTo(options.gridSpan?.start, minDayKey, "earlier");
|
|
7152
|
+
const gridMaxDayKey = widenTo(options.gridSpan?.end, maxDayKey, "later");
|
|
7153
|
+
const requestedMinDate = civilDate(requestedMinDayKey);
|
|
7154
|
+
const gridMaxDate = civilDate(gridMaxDayKey);
|
|
7155
|
+
const labelFormatting = {
|
|
7156
|
+
locale,
|
|
7157
|
+
timeZone: "UTC"
|
|
7158
|
+
};
|
|
7159
|
+
const formatWeekday = createDateFormatter({ weekday: "short" }, labelFormatting);
|
|
7160
|
+
const formatMonth = createDateFormatter({ month: "short" }, labelFormatting);
|
|
7161
|
+
const formatDay = createDateFormatter({
|
|
7162
|
+
weekday: "short",
|
|
7163
|
+
month: "short",
|
|
7164
|
+
day: "numeric",
|
|
7165
|
+
year: "numeric"
|
|
7166
|
+
}, labelFormatting);
|
|
7167
|
+
const gridStart = startOfCivilWeek(requestedMinDate, weekStartsOn);
|
|
7168
|
+
const gridMinDayKey = requestedMinDayKey < minDayKey ? civilKey(gridStart) : requestedMinDayKey;
|
|
7169
|
+
const weekCount = civilWeekSpan(gridStart, gridMaxDate, weekStartsOn);
|
|
7170
|
+
const rowLabels = Array.from({ length: 7 }, (_, row) => LABELLED_ROWS.includes(row) ? formatWeekday(addCivilDays(gridStart, row)) : "");
|
|
7171
|
+
const MIN_FIRST_MONTH_WEEKS = 2;
|
|
7172
|
+
const firstMonth = gridStart.getUTCMonth();
|
|
7173
|
+
let firstMonthWeeks = 0;
|
|
7174
|
+
while (firstMonthWeeks < weekCount && addCivilDays(gridStart, firstMonthWeeks * 7).getUTCMonth() === firstMonth) firstMonthWeeks++;
|
|
7175
|
+
const showFirstMonthLabel = !(firstMonthWeeks < weekCount) || firstMonthWeeks >= MIN_FIRST_MONTH_WEEKS;
|
|
7176
|
+
const data = [];
|
|
7177
|
+
let previousMonth = -1;
|
|
7178
|
+
for (let week = 0; week < weekCount; week++) {
|
|
7179
|
+
const columnStart = addCivilDays(gridStart, week * 7);
|
|
7180
|
+
const month = columnStart.getUTCMonth();
|
|
7181
|
+
const label = month !== previousMonth && (week !== 0 || showFirstMonthLabel) ? formatMonth(columnStart) : "";
|
|
7182
|
+
previousMonth = month;
|
|
7183
|
+
const cells = [];
|
|
7184
|
+
for (let row = 0; row < 7; row++) {
|
|
7185
|
+
const day = addCivilDays(gridStart, week * 7 + row);
|
|
7186
|
+
const key = civilKey(day);
|
|
7187
|
+
const cell = {
|
|
7188
|
+
label: formatDay(day),
|
|
7189
|
+
value: valueByDay.has(key) ? valueByDay.get(key) : null
|
|
7190
|
+
};
|
|
7191
|
+
if (key < gridMinDayKey || key > gridMaxDayKey) {
|
|
7192
|
+
if (hideOutOfRangeDays) cell.hidden = true;
|
|
7193
|
+
} else if (key < minDayKey || key > maxDayKey) cell.placeholder = true;
|
|
7194
|
+
cells.push(cell);
|
|
7195
|
+
}
|
|
7196
|
+
data.push({
|
|
7197
|
+
label,
|
|
7198
|
+
data: cells
|
|
7199
|
+
});
|
|
7200
|
+
}
|
|
7201
|
+
return {
|
|
7202
|
+
data,
|
|
7203
|
+
rowLabels
|
|
7204
|
+
};
|
|
7205
|
+
};
|
|
7206
|
+
//#endregion
|
|
7201
7207
|
//#region src/charts/heatmap-chart/use-calendar-heatmap-data.ts
|
|
7202
7208
|
/**
|
|
7203
7209
|
* `buildCalendarHeatmapData` with the locale and zone taken from
|