@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/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
- chartRef.current?.focus();
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
- chartRef.current?.focus();
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
- chartRef.current?.focus();
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