@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 CHANGED
@@ -5,6 +5,14 @@ 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
+ ## [4.1.1] - 2026-09-15
9
+ ### Changed
10
+ - Update package dependencies. [#52297]
11
+
12
+ ### Fixed
13
+ - Fix unreadable axis labels in forced-colors mode. [#52268]
14
+ - Return keyboard focus to line and area charts after closing a tooltip with Escape. [#52284]
15
+
8
16
  ## [4.1.0] - 2026-09-14
9
17
  ### Added
10
18
  - Heatmap: add `locale` and `timeZone` options and a `useCalendarHeatmapData` hook, so a host can bucket and label the calendar in its own zone and language instead of the viewer's. Labels with no `locale` now follow the runtime locale rather than always rendering in English, and a calendar `dateString` must start `yyyy-MM-dd`. [#52114]
@@ -1063,6 +1071,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1063
1071
  - Fixed lints following ESLint rule changes for TS [#40584]
1064
1072
  - Fixing a bug in Chart storybook data. [#40640]
1065
1073
 
1074
+ [4.1.1]: https://github.com/Automattic/charts/compare/v4.1.0...v4.1.1
1066
1075
  [4.1.0]: https://github.com/Automattic/charts/compare/v4.0.0...v4.1.0
1067
1076
  [4.0.0]: https://github.com/Automattic/charts/compare/v3.2.0...v4.0.0
1068
1077
  [3.2.0]: https://github.com/Automattic/charts/compare/v3.1.1...v3.2.0
package/TOKENS.md CHANGED
@@ -12,7 +12,7 @@ Tooltips render inside the chart, so they carry the class only when no provider
12
12
 
13
13
  Inside a provider a tooltip therefore inherits the provider's inline `theme` var and any consumer rule scoped to the provider wrapper, the same as the chart it belongs to.
14
14
 
15
- Each catalog entry maps to a WPDS token. Source writes the mapping bare; the LightningCSS plugin in `tsdown.config.ts` injects the WPDS spec value as a fallback into `dist/`:
15
+ Catalog entries normally map to WPDS tokens. Source writes these mappings bare; the LightningCSS plugin in `tsdown.config.ts` injects the WPDS spec value as a fallback into `dist/`:
16
16
 
17
17
  ```scss
18
18
  :where(.a8c-charts-scope) {
@@ -28,7 +28,7 @@ Highest first:
28
28
 
29
29
  1. The role set on any element between the chart and the provider — the closest declaration wins. A role set on the chart's *own* element reaches only what CSS paints; see "The SVG bridge".
30
30
  2. The role set by a consumer rule targeting the provider wrapper. It beats the catalog default because `:where()` is zero-specificity.
31
- 3. The catalog default on the provider wrapper, resolving the mapped `--wpds-*` token.
31
+ 3. The catalog default on the provider wrapper, including the [forced-colors defaults](#one-pair-of-roles-per-axis) when active.
32
32
  4. The WPDS spec-value fallback, when no `--wpds-*` token is set either (SSR, jsdom, or WPDS not loaded). This is not a rare corner: WordPress itself defines no `--wpds-*` typography tokens, so in wp-admin the fallback is what renders. It is injected at build time by `@wordpress/theme`'s LightningCSS plugin.
33
33
 
34
34
  Configure catalog colors through CSS; the `theme` prop carries no colors. For local LineChart tooltip overrides, see [Below-Axis Tooltips](./?path=/docs/js-packages-charts-library-charts-line-chart--docs#below-axis-tooltips).
@@ -44,7 +44,7 @@ Some roles are deliberately narrower than the obvious name, so that moving one t
44
44
  | `--a8c-charts-color-label-axis` | `--a8c-charts-color-label` | legend labels, `.heatmap-chart__cell-value`, funnel labels, the line-chart tooltip |
45
45
  | `--a8c-charts-color-background` | — | — |
46
46
 
47
- `--a8c-charts-color-label-axis` *derives* from `--a8c-charts-color-label`, so setting the broad role moves every label at once and setting the narrow one moves only the SVG axis labels.
47
+ Outside forced-colors mode, `--a8c-charts-color-label-axis` derives from `--a8c-charts-color-label`, so setting the broad role moves every label at once and setting the narrow one moves only the SVG axis labels. See [One pair of roles per axis](#one-pair-of-roles-per-axis) for forced-colors defaults.
48
48
 
49
49
  `--a8c-charts-color-surface` — the annotation label, `.x-zoom__reset`, tooltips — is a **sibling** of `--a8c-charts-color-background`, not a child. The chart's own background and the surfaces floating over it are set apart from each other often enough that one has to be able to move without the other. The consequence: no single role repaints both.
50
50
 
@@ -72,7 +72,7 @@ The scope element is the wrapper a chart is rendered into, which sits **above**
72
72
 
73
73
  ## Tier 1 — semantic catalog (shared, themeable roles)
74
74
 
75
- > These tables are checked against `src/styles/chart-scope.scss` by `src/styles/test/chart-scope.test.ts`. Update the stylesheet and the table together.
75
+ > These tables describe the base defaults outside forced-colors mode and are checked against `src/styles/chart-scope.scss` by `src/styles/test/chart-scope.test.ts`. Update the stylesheet and the table together.
76
76
 
77
77
  | Role | Maps to | Fallback |
78
78
  |---|---|---|
@@ -114,6 +114,8 @@ Each axis names itself: `--a8c-charts-color-axis-x` / `-tick-x` paint the x axis
114
114
 
115
115
  Gridlines also stay on one role, `--a8c-charts-color-grid`: visx paints rows and columns from a single style object, so the grid is genuinely one thing rather than two.
116
116
 
117
+ Under `forced-colors: active`, the axis label role uses `CanvasText`, and the grid, x-axis line, and x-axis tick roles use `GrayText`. The y-axis line and ticks remain hidden by default.
118
+
117
119
  The terminal `none` in each chain is explicit rather than load-bearing: `stroke` already starts at `none`, so an undeclared role paints nothing either way.
118
120
 
119
121
  **It would be load-bearing on a `fill`.** A `var()` naming an undeclared property with no fallback is invalid at computed-value time, which drops the declaration rather than leaving it absent — and `fill` starts at *black*. That is why `--a8c-charts-color-label-background` carries a terminal `transparent`: without it, an undeclared role would put a black plate behind every pie label. Give any role read by a `fill` a terminal literal, even when the intent is "paint nothing".
package/dist/index.cjs CHANGED
@@ -2489,7 +2489,7 @@ const XyChartTooltipContent = ({ tooltipContext, container, renderTooltip, rende
2489
2489
  ..._visx_tooltip.defaultStyles,
2490
2490
  zIndex,
2491
2491
  backgroundColor: theme?.backgroundColor ?? "white",
2492
- boxShadow: `0 1px 2px ${theme?.htmlLabel?.color ? `${theme.htmlLabel.color}55` : "#22222255"}`,
2492
+ boxShadow: `0 1px 2px ${isValidHexColor(theme?.htmlLabel?.color) ? `${theme.htmlLabel.color}55` : "#22222255"}`,
2493
2493
  ...theme?.htmlLabel,
2494
2494
  ...style
2495
2495
  };
@@ -2672,7 +2672,8 @@ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating,
2672
2672
  else element.focus();
2673
2673
  }
2674
2674
  }, [preventTooltipScroll, selectedIndex]),
2675
- onChartFocus: (0, react.useCallback)(() => {
2675
+ onChartFocus: (0, react.useCallback)((event) => {
2676
+ if (event.currentTarget.contains(event.relatedTarget)) return;
2676
2677
  if (!isNavigating && selectedIndex !== void 0) setSelectedIndex(0);
2677
2678
  }, [
2678
2679
  isNavigating,
@@ -2684,15 +2685,19 @@ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating,
2684
2685
  }, [setIsNavigating]),
2685
2686
  onChartKeyDown: (0, react.useCallback)((event) => {
2686
2687
  if (totalPoints === 0) return;
2688
+ const focusChart = () => {
2689
+ if (preventTooltipScroll) chartRef.current?.focus({ preventScroll: true });
2690
+ else chartRef.current?.focus();
2691
+ };
2687
2692
  if (event.key === "Tab") {
2688
- chartRef.current?.focus();
2693
+ focusChart();
2689
2694
  setSelectedIndex(void 0);
2690
2695
  setIsNavigating(false);
2691
2696
  return;
2692
2697
  }
2693
2698
  const currentSelectedIndex = selectedIndex === void 0 ? -1 : selectedIndex;
2694
2699
  if (currentSelectedIndex + 1 >= totalPoints && ["ArrowRight"].includes(event.key)) {
2695
- chartRef.current?.focus();
2700
+ focusChart();
2696
2701
  setSelectedIndex(void 0);
2697
2702
  setIsNavigating(false);
2698
2703
  return;
@@ -2707,7 +2712,7 @@ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating,
2707
2712
  } else if (event.key === "Escape") {
2708
2713
  setSelectedIndex(void 0);
2709
2714
  setIsNavigating(false);
2710
- chartRef.current?.focus();
2715
+ focusChart();
2711
2716
  } else if ((event.key === "Enter" || event.key === " ") && selectedIndex !== void 0) onActivate?.(selectedIndex);
2712
2717
  }, [
2713
2718
  totalPoints,
@@ -2715,7 +2720,8 @@ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating,
2715
2720
  setSelectedIndex,
2716
2721
  setIsNavigating,
2717
2722
  chartRef,
2718
- onActivate
2723
+ onActivate,
2724
+ preventTooltipScroll
2719
2725
  ])
2720
2726
  };
2721
2727
  };
@@ -4318,6 +4324,7 @@ const LineChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
4318
4324
  children: ({ contentHeight }) => {
4319
4325
  const chartHeight = contentHeight > 0 ? contentHeight : height;
4320
4326
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4327
+ ref: chartRef,
4321
4328
  role: "grid",
4322
4329
  "aria-label": (0, _wordpress_i18n.__)("Line chart", "jetpack-charts"),
4323
4330
  tabIndex: 0,
@@ -4325,7 +4332,6 @@ const LineChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
4325
4332
  onFocus: onChartFocus,
4326
4333
  onBlur: onChartBlur,
4327
4334
  children: chartHeight > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4328
- ref: chartRef,
4329
4335
  className: xy_plot_module_default["xy-plot"],
4330
4336
  children: [zoomable && zoom.domain && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomResetButton, { onClick: zoom.reset }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_visx_xychart.XYChart, {
4331
4337
  theme,
@@ -4823,6 +4829,7 @@ const AreaChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
4823
4829
  children: ({ contentHeight }) => {
4824
4830
  const chartHeight = contentHeight > 0 ? contentHeight : height;
4825
4831
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4832
+ ref: chartRef,
4826
4833
  role: "grid",
4827
4834
  "aria-label": (0, _wordpress_i18n.__)("Area chart", "jetpack-charts"),
4828
4835
  tabIndex: 0,
@@ -4830,7 +4837,6 @@ const AreaChartInternal = (0, react.forwardRef)(({ data, chartId: providedChartI
4830
4837
  onFocus: onChartFocus,
4831
4838
  onBlur: onChartBlur,
4832
4839
  children: chartHeight > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4833
- ref: chartRef,
4834
4840
  className: xy_plot_module_default["xy-plot"],
4835
4841
  children: [zoomable && zoom.domain && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomResetButton, { onClick: zoom.reset }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_visx_xychart.XYChart, {
4836
4842
  theme,
@@ -6634,233 +6640,6 @@ const getNormalizedValue = (value, extent) => {
6634
6640
  return Math.min(1, Math.max(0, (value - min) / (max - min)));
6635
6641
  };
6636
6642
  //#endregion
6637
- //#region src/charts/heatmap-chart/private/civil-day.ts
6638
- const DAY_MS = 864e5;
6639
- const KEY_OPTIONS = {
6640
- calendar: "gregory",
6641
- numberingSystem: "latn",
6642
- year: "numeric",
6643
- month: "2-digit",
6644
- day: "2-digit"
6645
- };
6646
- const DATE_PREFIX = /^(\d{4})-(\d{2})-(\d{2})/;
6647
- const pad = (value, length) => String(value).padStart(length, "0");
6648
- /**
6649
- * The day a UTC proxy stands for.
6650
- *
6651
- * @param date - UTC proxy.
6652
- * @return Its day key.
6653
- */
6654
- const civilKey = (date) => `${pad(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1, 2)}-${pad(date.getUTCDate(), 2)}`;
6655
- /**
6656
- * A day key as a UTC-midnight proxy. UTC has no DST, so every step below is exactly
6657
- * `DAY_MS`.
6658
- *
6659
- * @param key - Day key.
6660
- * @return The proxy, or null when the key names no real day.
6661
- */
6662
- const civilDate = (key) => {
6663
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(key);
6664
- if (!match) return null;
6665
- const date = /* @__PURE__ */ new Date(0);
6666
- date.setUTCFullYear(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
6667
- date.setUTCHours(0, 0, 0, 0);
6668
- return civilKey(date) === key ? date : null;
6669
- };
6670
- /**
6671
- * The day a written date names, ignoring any time and zone it carries.
6672
- *
6673
- * @param text - `yyyy-MM-dd`, optionally followed by a time.
6674
- * @return Its day key, or null.
6675
- */
6676
- const writtenDayKey = (text) => {
6677
- const match = DATE_PREFIX.exec(text);
6678
- if (!match) return null;
6679
- const key = `${match[1]}-${match[2]}-${match[3]}`;
6680
- return civilDate(key) ? key : null;
6681
- };
6682
- /**
6683
- * Reads the day an instant falls on in a zone.
6684
- *
6685
- * @param timeZone - IANA zone name; absent leaves the runtime's own.
6686
- * @return Reader taking a `Date`, null for a day no key can hold.
6687
- */
6688
- const instantDayReader = (timeZone) => {
6689
- const formatter = new Intl.DateTimeFormat("en-US", {
6690
- ...KEY_OPTIONS,
6691
- timeZone
6692
- });
6693
- return (date) => {
6694
- const parts = formatter.formatToParts(date);
6695
- const read = (type) => parts.find((part) => part.type === type)?.value ?? "";
6696
- const key = `${pad(Number(read("year")), 4)}-${read("month")}-${read("day")}`;
6697
- return civilDate(key) ? key : null;
6698
- };
6699
- };
6700
- /**
6701
- * The day a series point belongs to.
6702
- *
6703
- * A `Date`, or a string carrying `Z` or an offset, is an instant read in the host's
6704
- * zone. Anything else is a wall clock the host wrote, so its date part is the day.
6705
- *
6706
- * @param point - Series point.
6707
- * @param readInstant - Reader from `instantDayReader`.
6708
- * @return Its day key, or null when the point carries no usable date.
6709
- */
6710
- const pointDayKey = (point, readInstant) => {
6711
- if (point.date instanceof Date && !isNaN(point.date.getTime())) {
6712
- const key = readInstant(point.date);
6713
- if (key) return key;
6714
- }
6715
- const text = point.dateString;
6716
- if (!text) return null;
6717
- if (!hasTimezone(text)) return writtenDayKey(text);
6718
- const parsed = (0, date_fns.parseISO)(text);
6719
- return isNaN(parsed.getTime()) ? null : readInstant(parsed);
6720
- };
6721
- /**
6722
- * Steps a proxy by whole days.
6723
- *
6724
- * @param date - UTC proxy.
6725
- * @param days - Days to add; may be negative.
6726
- * @return The shifted proxy.
6727
- */
6728
- const addCivilDays = (date, days) => new Date(date.getTime() + days * DAY_MS);
6729
- /**
6730
- * Rounds a proxy down to its week.
6731
- *
6732
- * @param date - UTC proxy.
6733
- * @param weekStartsOn - 0 for Sunday, 1 for Monday.
6734
- * @return The proxy for that week's first day.
6735
- */
6736
- const startOfCivilWeek = (date, weekStartsOn) => {
6737
- const daysSinceWeekStart = (date.getUTCDay() - weekStartsOn + 7) % 7;
6738
- return addCivilDays(date, -daysSinceWeekStart);
6739
- };
6740
- /**
6741
- * Counts the columns a span needs.
6742
- *
6743
- * @param from - Earlier UTC proxy.
6744
- * @param to - Later UTC proxy.
6745
- * @param weekStartsOn - 0 for Sunday, 1 for Monday.
6746
- * @return Columns needed to cover the span, inclusive.
6747
- */
6748
- const civilWeekSpan = (from, to, weekStartsOn) => Math.floor((startOfCivilWeek(to, weekStartsOn).getTime() - startOfCivilWeek(from, weekStartsOn).getTime()) / (7 * DAY_MS)) + 1;
6749
- //#endregion
6750
- //#region src/charts/heatmap-chart/private/build-calendar-data.ts
6751
- /** Rows that get a weekday label (Mon, Wed, Fri with a Monday week start). */
6752
- const LABELLED_ROWS = [
6753
- 0,
6754
- 2,
6755
- 4
6756
- ];
6757
- /**
6758
- * Resolve one grid bound: an unparseable or narrowing value falls back to the
6759
- * series' own bound, so the grid can only ever be widened.
6760
- *
6761
- * @param bound - Requested bound as `yyyy-MM-dd`, if any.
6762
- * @param fallback - The series' own bound on this side.
6763
- * @param direction - Which way the bound is allowed to move.
6764
- * @return The day key to draw to.
6765
- */
6766
- const widenTo = (bound, fallback, direction) => {
6767
- const key = bound ? writtenDayKey(bound) : null;
6768
- if (!key) {
6769
- 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.`);
6770
- return fallback;
6771
- }
6772
- if (direction === "earlier") return key < fallback ? key : fallback;
6773
- return key > fallback ? key : fallback;
6774
- };
6775
- /**
6776
- * Lay a day-bucketed series out as calendar columns.
6777
- *
6778
- * @param series - Points to bucket.
6779
- * @param options - Grid shape, plus the locale and zone to read days in.
6780
- * @return Columns and row labels for `HeatmapChart`.
6781
- */
6782
- const buildCalendarHeatmapData = (series, options = {}) => {
6783
- const weekStartsOn = options.weekStartsOn ?? 1;
6784
- const hideOutOfRangeDays = options.hideOutOfRangeDays ?? true;
6785
- const { locale, timeZone } = sanitizeFormatting({
6786
- locale: options.locale,
6787
- timeZone: options.timeZone
6788
- });
6789
- const readInstant = instantDayReader(timeZone);
6790
- const valueByDay = /* @__PURE__ */ new Map();
6791
- let minDayKey;
6792
- let maxDayKey;
6793
- for (const point of series) {
6794
- const key = pointDayKey(point, readInstant);
6795
- if (!key) {
6796
- const offending = point.dateString ?? (point.date && String(point.date));
6797
- 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.");
6798
- continue;
6799
- }
6800
- valueByDay.set(key, point.value);
6801
- if (!minDayKey || key < minDayKey) minDayKey = key;
6802
- if (!maxDayKey || key > maxDayKey) maxDayKey = key;
6803
- }
6804
- if (!minDayKey || !maxDayKey) return {
6805
- data: [],
6806
- rowLabels: []
6807
- };
6808
- const requestedMinDayKey = widenTo(options.gridSpan?.start, minDayKey, "earlier");
6809
- const gridMaxDayKey = widenTo(options.gridSpan?.end, maxDayKey, "later");
6810
- const requestedMinDate = civilDate(requestedMinDayKey);
6811
- const gridMaxDate = civilDate(gridMaxDayKey);
6812
- const labelFormatting = {
6813
- locale,
6814
- timeZone: "UTC"
6815
- };
6816
- const formatWeekday = createDateFormatter({ weekday: "short" }, labelFormatting);
6817
- const formatMonth = createDateFormatter({ month: "short" }, labelFormatting);
6818
- const formatDay = createDateFormatter({
6819
- weekday: "short",
6820
- month: "short",
6821
- day: "numeric",
6822
- year: "numeric"
6823
- }, labelFormatting);
6824
- const gridStart = startOfCivilWeek(requestedMinDate, weekStartsOn);
6825
- const gridMinDayKey = requestedMinDayKey < minDayKey ? civilKey(gridStart) : requestedMinDayKey;
6826
- const weekCount = civilWeekSpan(gridStart, gridMaxDate, weekStartsOn);
6827
- const rowLabels = Array.from({ length: 7 }, (_, row) => LABELLED_ROWS.includes(row) ? formatWeekday(addCivilDays(gridStart, row)) : "");
6828
- const MIN_FIRST_MONTH_WEEKS = 2;
6829
- const firstMonth = gridStart.getUTCMonth();
6830
- let firstMonthWeeks = 0;
6831
- while (firstMonthWeeks < weekCount && addCivilDays(gridStart, firstMonthWeeks * 7).getUTCMonth() === firstMonth) firstMonthWeeks++;
6832
- const showFirstMonthLabel = !(firstMonthWeeks < weekCount) || firstMonthWeeks >= MIN_FIRST_MONTH_WEEKS;
6833
- const data = [];
6834
- let previousMonth = -1;
6835
- for (let week = 0; week < weekCount; week++) {
6836
- const columnStart = addCivilDays(gridStart, week * 7);
6837
- const month = columnStart.getUTCMonth();
6838
- const label = month !== previousMonth && (week !== 0 || showFirstMonthLabel) ? formatMonth(columnStart) : "";
6839
- previousMonth = month;
6840
- const cells = [];
6841
- for (let row = 0; row < 7; row++) {
6842
- const day = addCivilDays(gridStart, week * 7 + row);
6843
- const key = civilKey(day);
6844
- const cell = {
6845
- label: formatDay(day),
6846
- value: valueByDay.has(key) ? valueByDay.get(key) : null
6847
- };
6848
- if (key < gridMinDayKey || key > gridMaxDayKey) {
6849
- if (hideOutOfRangeDays) cell.hidden = true;
6850
- } else if (key < minDayKey || key > maxDayKey) cell.placeholder = true;
6851
- cells.push(cell);
6852
- }
6853
- data.push({
6854
- label,
6855
- data: cells
6856
- });
6857
- }
6858
- return {
6859
- data,
6860
- rowLabels
6861
- };
6862
- };
6863
- //#endregion
6864
6643
  //#region src/charts/heatmap-chart/private/heatmap-context.ts
6865
6644
  /** Shared by the chart and legend without importing back from `heatmap-chart.tsx`. */
6866
6645
  const HeatmapContext = (0, react.createContext)(null);
@@ -7200,6 +6979,233 @@ const HeatmapChartResponsiveInner = (props) => /* @__PURE__ */ (0, react_jsx_run
7200
6979
  HeatmapChartResponsiveInner.displayName = "HeatmapChart";
7201
6980
  const HeatmapChartResponsive = attachSubComponents(withResponsive(HeatmapChartResponsiveInner), { Legend: HeatmapLegend });
7202
6981
  //#endregion
6982
+ //#region src/charts/heatmap-chart/private/civil-day.ts
6983
+ const DAY_MS = 864e5;
6984
+ const KEY_OPTIONS = {
6985
+ calendar: "gregory",
6986
+ numberingSystem: "latn",
6987
+ year: "numeric",
6988
+ month: "2-digit",
6989
+ day: "2-digit"
6990
+ };
6991
+ const DATE_PREFIX = /^(\d{4})-(\d{2})-(\d{2})/;
6992
+ const pad = (value, length) => String(value).padStart(length, "0");
6993
+ /**
6994
+ * The day a UTC proxy stands for.
6995
+ *
6996
+ * @param date - UTC proxy.
6997
+ * @return Its day key.
6998
+ */
6999
+ const civilKey = (date) => `${pad(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1, 2)}-${pad(date.getUTCDate(), 2)}`;
7000
+ /**
7001
+ * A day key as a UTC-midnight proxy. UTC has no DST, so every step below is exactly
7002
+ * `DAY_MS`.
7003
+ *
7004
+ * @param key - Day key.
7005
+ * @return The proxy, or null when the key names no real day.
7006
+ */
7007
+ const civilDate = (key) => {
7008
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(key);
7009
+ if (!match) return null;
7010
+ const date = /* @__PURE__ */ new Date(0);
7011
+ date.setUTCFullYear(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
7012
+ date.setUTCHours(0, 0, 0, 0);
7013
+ return civilKey(date) === key ? date : null;
7014
+ };
7015
+ /**
7016
+ * The day a written date names, ignoring any time and zone it carries.
7017
+ *
7018
+ * @param text - `yyyy-MM-dd`, optionally followed by a time.
7019
+ * @return Its day key, or null.
7020
+ */
7021
+ const writtenDayKey = (text) => {
7022
+ const match = DATE_PREFIX.exec(text);
7023
+ if (!match) return null;
7024
+ const key = `${match[1]}-${match[2]}-${match[3]}`;
7025
+ return civilDate(key) ? key : null;
7026
+ };
7027
+ /**
7028
+ * Reads the day an instant falls on in a zone.
7029
+ *
7030
+ * @param timeZone - IANA zone name; absent leaves the runtime's own.
7031
+ * @return Reader taking a `Date`, null for a day no key can hold.
7032
+ */
7033
+ const instantDayReader = (timeZone) => {
7034
+ const formatter = new Intl.DateTimeFormat("en-US", {
7035
+ ...KEY_OPTIONS,
7036
+ timeZone
7037
+ });
7038
+ return (date) => {
7039
+ const parts = formatter.formatToParts(date);
7040
+ const read = (type) => parts.find((part) => part.type === type)?.value ?? "";
7041
+ const key = `${pad(Number(read("year")), 4)}-${read("month")}-${read("day")}`;
7042
+ return civilDate(key) ? key : null;
7043
+ };
7044
+ };
7045
+ /**
7046
+ * The day a series point belongs to.
7047
+ *
7048
+ * A `Date`, or a string carrying `Z` or an offset, is an instant read in the host's
7049
+ * zone. Anything else is a wall clock the host wrote, so its date part is the day.
7050
+ *
7051
+ * @param point - Series point.
7052
+ * @param readInstant - Reader from `instantDayReader`.
7053
+ * @return Its day key, or null when the point carries no usable date.
7054
+ */
7055
+ const pointDayKey = (point, readInstant) => {
7056
+ if (point.date instanceof Date && !isNaN(point.date.getTime())) {
7057
+ const key = readInstant(point.date);
7058
+ if (key) return key;
7059
+ }
7060
+ const text = point.dateString;
7061
+ if (!text) return null;
7062
+ if (!hasTimezone(text)) return writtenDayKey(text);
7063
+ const parsed = (0, date_fns.parseISO)(text);
7064
+ return isNaN(parsed.getTime()) ? null : readInstant(parsed);
7065
+ };
7066
+ /**
7067
+ * Steps a proxy by whole days.
7068
+ *
7069
+ * @param date - UTC proxy.
7070
+ * @param days - Days to add; may be negative.
7071
+ * @return The shifted proxy.
7072
+ */
7073
+ const addCivilDays = (date, days) => new Date(date.getTime() + days * DAY_MS);
7074
+ /**
7075
+ * Rounds a proxy down to its week.
7076
+ *
7077
+ * @param date - UTC proxy.
7078
+ * @param weekStartsOn - 0 for Sunday, 1 for Monday.
7079
+ * @return The proxy for that week's first day.
7080
+ */
7081
+ const startOfCivilWeek = (date, weekStartsOn) => {
7082
+ const daysSinceWeekStart = (date.getUTCDay() - weekStartsOn + 7) % 7;
7083
+ return addCivilDays(date, -daysSinceWeekStart);
7084
+ };
7085
+ /**
7086
+ * Counts the columns a span needs.
7087
+ *
7088
+ * @param from - Earlier UTC proxy.
7089
+ * @param to - Later UTC proxy.
7090
+ * @param weekStartsOn - 0 for Sunday, 1 for Monday.
7091
+ * @return Columns needed to cover the span, inclusive.
7092
+ */
7093
+ const civilWeekSpan = (from, to, weekStartsOn) => Math.floor((startOfCivilWeek(to, weekStartsOn).getTime() - startOfCivilWeek(from, weekStartsOn).getTime()) / (7 * DAY_MS)) + 1;
7094
+ //#endregion
7095
+ //#region src/charts/heatmap-chart/build-calendar-data.ts
7096
+ /** Rows that get a weekday label (Mon, Wed, Fri with a Monday week start). */
7097
+ const LABELLED_ROWS = [
7098
+ 0,
7099
+ 2,
7100
+ 4
7101
+ ];
7102
+ /**
7103
+ * Resolve one grid bound: an unparseable or narrowing value falls back to the
7104
+ * series' own bound, so the grid can only ever be widened.
7105
+ *
7106
+ * @param bound - Requested bound as `yyyy-MM-dd`, if any.
7107
+ * @param fallback - The series' own bound on this side.
7108
+ * @param direction - Which way the bound is allowed to move.
7109
+ * @return The day key to draw to.
7110
+ */
7111
+ const widenTo = (bound, fallback, direction) => {
7112
+ const key = bound ? writtenDayKey(bound) : null;
7113
+ if (!key) {
7114
+ 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.`);
7115
+ return fallback;
7116
+ }
7117
+ if (direction === "earlier") return key < fallback ? key : fallback;
7118
+ return key > fallback ? key : fallback;
7119
+ };
7120
+ /**
7121
+ * Lay a day-bucketed series out as calendar columns.
7122
+ *
7123
+ * @param series - Points to bucket.
7124
+ * @param options - Grid shape, plus the locale and zone to read days in.
7125
+ * @return Columns and row labels for `HeatmapChart`.
7126
+ */
7127
+ const buildCalendarHeatmapData = (series, options = {}) => {
7128
+ const weekStartsOn = options.weekStartsOn ?? 1;
7129
+ const hideOutOfRangeDays = options.hideOutOfRangeDays ?? true;
7130
+ const { locale, timeZone } = sanitizeFormatting({
7131
+ locale: options.locale,
7132
+ timeZone: options.timeZone
7133
+ });
7134
+ const readInstant = instantDayReader(timeZone);
7135
+ const valueByDay = /* @__PURE__ */ new Map();
7136
+ let minDayKey;
7137
+ let maxDayKey;
7138
+ for (const point of series) {
7139
+ const key = pointDayKey(point, readInstant);
7140
+ if (!key) {
7141
+ const offending = point.dateString ?? (point.date && String(point.date));
7142
+ 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.");
7143
+ continue;
7144
+ }
7145
+ valueByDay.set(key, point.value);
7146
+ if (!minDayKey || key < minDayKey) minDayKey = key;
7147
+ if (!maxDayKey || key > maxDayKey) maxDayKey = key;
7148
+ }
7149
+ if (!minDayKey || !maxDayKey) return {
7150
+ data: [],
7151
+ rowLabels: []
7152
+ };
7153
+ const requestedMinDayKey = widenTo(options.gridSpan?.start, minDayKey, "earlier");
7154
+ const gridMaxDayKey = widenTo(options.gridSpan?.end, maxDayKey, "later");
7155
+ const requestedMinDate = civilDate(requestedMinDayKey);
7156
+ const gridMaxDate = civilDate(gridMaxDayKey);
7157
+ const labelFormatting = {
7158
+ locale,
7159
+ timeZone: "UTC"
7160
+ };
7161
+ const formatWeekday = createDateFormatter({ weekday: "short" }, labelFormatting);
7162
+ const formatMonth = createDateFormatter({ month: "short" }, labelFormatting);
7163
+ const formatDay = createDateFormatter({
7164
+ weekday: "short",
7165
+ month: "short",
7166
+ day: "numeric",
7167
+ year: "numeric"
7168
+ }, labelFormatting);
7169
+ const gridStart = startOfCivilWeek(requestedMinDate, weekStartsOn);
7170
+ const gridMinDayKey = requestedMinDayKey < minDayKey ? civilKey(gridStart) : requestedMinDayKey;
7171
+ const weekCount = civilWeekSpan(gridStart, gridMaxDate, weekStartsOn);
7172
+ const rowLabels = Array.from({ length: 7 }, (_, row) => LABELLED_ROWS.includes(row) ? formatWeekday(addCivilDays(gridStart, row)) : "");
7173
+ const MIN_FIRST_MONTH_WEEKS = 2;
7174
+ const firstMonth = gridStart.getUTCMonth();
7175
+ let firstMonthWeeks = 0;
7176
+ while (firstMonthWeeks < weekCount && addCivilDays(gridStart, firstMonthWeeks * 7).getUTCMonth() === firstMonth) firstMonthWeeks++;
7177
+ const showFirstMonthLabel = !(firstMonthWeeks < weekCount) || firstMonthWeeks >= MIN_FIRST_MONTH_WEEKS;
7178
+ const data = [];
7179
+ let previousMonth = -1;
7180
+ for (let week = 0; week < weekCount; week++) {
7181
+ const columnStart = addCivilDays(gridStart, week * 7);
7182
+ const month = columnStart.getUTCMonth();
7183
+ const label = month !== previousMonth && (week !== 0 || showFirstMonthLabel) ? formatMonth(columnStart) : "";
7184
+ previousMonth = month;
7185
+ const cells = [];
7186
+ for (let row = 0; row < 7; row++) {
7187
+ const day = addCivilDays(gridStart, week * 7 + row);
7188
+ const key = civilKey(day);
7189
+ const cell = {
7190
+ label: formatDay(day),
7191
+ value: valueByDay.has(key) ? valueByDay.get(key) : null
7192
+ };
7193
+ if (key < gridMinDayKey || key > gridMaxDayKey) {
7194
+ if (hideOutOfRangeDays) cell.hidden = true;
7195
+ } else if (key < minDayKey || key > maxDayKey) cell.placeholder = true;
7196
+ cells.push(cell);
7197
+ }
7198
+ data.push({
7199
+ label,
7200
+ data: cells
7201
+ });
7202
+ }
7203
+ return {
7204
+ data,
7205
+ rowLabels
7206
+ };
7207
+ };
7208
+ //#endregion
7203
7209
  //#region src/charts/heatmap-chart/use-calendar-heatmap-data.ts
7204
7210
  /**
7205
7211
  * `buildCalendarHeatmapData` with the locale and zone taken from