@automattic/charts 4.0.0 → 4.1.0

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/SECURITY.md +1 -0
  3. package/TOKENS.md +1 -1
  4. package/dist/index.cjs +548 -199
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.css +21 -0
  7. package/dist/index.d.cts +86 -72
  8. package/dist/index.d.ts +86 -72
  9. package/dist/index.js +549 -201
  10. package/dist/index.js.map +1 -1
  11. package/package.json +2 -1
  12. package/src/charts/heatmap-chart/heatmap-chart.module.scss +31 -0
  13. package/src/charts/heatmap-chart/heatmap-chart.tsx +44 -9
  14. package/src/charts/heatmap-chart/index.ts +2 -1
  15. package/src/charts/heatmap-chart/private/build-calendar-data.ts +126 -92
  16. package/src/charts/heatmap-chart/private/civil-day.ts +174 -0
  17. package/src/charts/heatmap-chart/private/index.ts +1 -1
  18. package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +5 -1
  19. package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +3 -0
  20. package/src/charts/heatmap-chart/test/civil-day.test.ts +70 -0
  21. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +91 -0
  22. package/src/charts/heatmap-chart/test/use-calendar-heatmap-data.test.tsx +118 -0
  23. package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +8 -0
  24. package/src/charts/heatmap-chart/test/zoned-calendar-data.test.ts +248 -0
  25. package/src/charts/heatmap-chart/types.ts +6 -0
  26. package/src/charts/heatmap-chart/use-calendar-heatmap-data.ts +42 -0
  27. package/src/charts/leaderboard-chart/types.ts +2 -2
  28. package/src/charts/line-chart/line-chart.tsx +39 -6
  29. package/src/charts/line-chart/test/crosshair-style-types.test.ts +31 -0
  30. package/src/charts/line-chart/test/line-chart.test.tsx +147 -38
  31. package/src/charts/line-chart/test/zoned-date-string.test.tsx +37 -0
  32. package/src/charts/line-chart/types.ts +13 -2
  33. package/src/charts/pie-chart/test/pie-chart.test.tsx +3 -12
  34. package/src/components/tooltip/accessible-tooltip.tsx +8 -2
  35. package/src/components/tooltip/private/bounded-tooltip.tsx +71 -16
  36. package/src/components/tooltip/test/accessible-tooltip.test.tsx +28 -1
  37. package/src/components/tooltip/test/bounded-tooltip.test.tsx +102 -6
  38. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +100 -0
  39. package/src/components/tooltip/xy-chart-tooltip.tsx +51 -13
  40. package/src/hooks/test/use-chart-data-transform-zone.test.tsx +36 -0
  41. package/src/hooks/test/use-chart-margin.test.tsx +173 -3
  42. package/src/hooks/use-chart-data-transform.ts +10 -3
  43. package/src/hooks/use-chart-margin.tsx +54 -9
  44. package/src/index.ts +4 -0
  45. package/src/test-utils/zoned-parsing-suite.ts +90 -0
  46. package/src/types.ts +2 -2
  47. package/src/utils/date-formatting.ts +1 -18
  48. package/src/utils/date-parsing.ts +103 -52
  49. package/src/utils/get-edge-tick-widths.ts +33 -0
  50. package/src/utils/index.ts +1 -0
  51. package/src/utils/test/date-parsing.auckland.test.ts +6 -0
  52. package/src/utils/test/date-parsing.santiago.test.ts +6 -0
  53. package/src/utils/test/date-parsing.test.ts +16 -1
  54. package/src/utils/test/get-edge-tick-widths.test.ts +55 -0
  55. package/src/utils/warn-once.ts +23 -0
  56. package/src/visx/types.ts +27 -3
package/dist/index.js CHANGED
@@ -6,7 +6,8 @@ import clsx from "clsx";
6
6
  import * as React from "react";
7
7
  import { Children, Fragment, createContext, createContext as createContext$1, createElement, forwardRef, forwardRef as forwardRef$1, isValidElement, memo, useCallback, useCallback as useCallback$1, useContext, useContext as useContext$1, useEffect, useEffect as useEffect$1, useId, useImperativeHandle, useLayoutEffect, useMemo, useMemo as useMemo$1, useRef, useRef as useRef$1, useState, useState as useState$1 } from "react";
8
8
  import { color, hsl } from "@visx/vendor/d3-color";
9
- import { addDays, differenceInCalendarWeeks, differenceInHours, differenceInYears, format, isValid, parse, parseISO, startOfWeek } from "date-fns";
9
+ import { differenceInHours, differenceInYears, isValid, parse, parseISO } from "date-fns";
10
+ import { tzOffset } from "@date-fns/tz";
10
11
  import { Text, getStringWidth } from "@visx/text";
11
12
  import deepmerge from "deepmerge";
12
13
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -84,46 +85,35 @@ function attachSubComponents(Chart, subComponents) {
84
85
  return Object.assign(Chart, subComponents);
85
86
  }
86
87
  //#endregion
87
- //#region src/utils/date-parsing.ts
88
+ //#region src/utils/warn-once.ts
89
+ const warned = /* @__PURE__ */ new Set();
88
90
  /**
89
- * @file Date parsing utilities using date-fns for local timezone handling
90
- *
91
- * This module provides utilities for parsing various date string formats and converting
92
- * them to local timezone dates using the battle-tested date-fns library. For formats
93
- * without timezone info, they're treated as local. For formats with timezone info,
94
- * they're converted to the equivalent local time.
91
+ * Warns about a host-supplied value once per process, and never in production.
95
92
  *
96
- * Note: And specifically it prevents format `YYYY-MM-DD` being parsed as UTC date.
97
- *
98
- * Key Features:
99
- * - All parsed dates are in local timezone
100
- * - Converts timezone-aware strings to local equivalent
101
- * - Robust input validation and error handling using date-fns
102
- * - TypeScript type safety
103
- * - Much smaller codebase than custom parsing
93
+ * @param key - Identifies the value, so a bad prop on every point warns once.
94
+ * @param message - What the host got wrong and what happens instead.
95
+ */
96
+ const warnOnce = (key, message) => {
97
+ if (warned.has(key) || process.env.NODE_ENV === "production") return;
98
+ warned.add(key);
99
+ console.warn(`[Charts] ${message}`);
100
+ };
101
+ //#endregion
102
+ //#region src/utils/date-parsing.ts
103
+ /**
104
+ * @file Date parsing: a naive string is dated in a supplied IANA zone, or the runtime's own
104
105
  *
105
- * Supported Formats:
106
- * - YYYY-MM-DD (treated as local)
107
- * - YYYY-MM-DD HH:mm:ss (treated as local)
108
- * - YYYY-MM-DD HH:mm (treated as local)
109
- * - YYYY-MM-DDTHH:mm:ss (treated as local)
110
- * - YYYY-MM-DDTHH:mm:ss.SSS (treated as local)
111
- * - YYYY-MM-DDTHH:mm (treated as local)
112
- * - YYYY-MM-DDTHH:mm:ssZ (converted to local)
113
- * - YYYY-MM-DDTHH:mm:ss±HH:mm (converted to local)
106
+ * A string carrying an offset is already an instant and parses the same everywhere. A string
107
+ * without one is only a wall-clock reading, so it means nothing until a zone is named. See
108
+ * `parseAsLocalDate` for the supported formats.
114
109
  *
115
- * @example
116
- * ```typescript
117
- * parseAsLocalDate("2025-01-01"); // Local timezone
118
- * parseAsLocalDate("2025-01-01 14:30:00"); // Local timezone
119
- * parseAsLocalDate("2025-01-01 14:30"); // Local timezone
120
- * parseAsLocalDate("2025-01-01T14:30:45.123"); // Local timezone
121
- * parseAsLocalDate("2025-01-01T14:30:00Z"); // UTC 14:30 → Local equivalent
122
- * parseAsLocalDate("2025-01-01T14:30:00+05:00"); // +05:00 14:30 → Local equivalent
123
- * ```
110
+ * Note: this specifically avoids date-fns's default of parsing `YYYY-MM-DD` as a UTC date.
124
111
  */
125
112
  /**
126
113
  * Checks if a date string contains timezone information
114
+ *
115
+ * The minutes are optional because ISO 8601 allows a bare `±hh`, which `parseISO` reads.
116
+ *
127
117
  * @param {string} dateString - The date string to check for timezone information
128
118
  * @return {boolean} True if the date string contains timezone information, false otherwise
129
119
  */
@@ -131,28 +121,57 @@ const hasTimezone = (dateString) => {
131
121
  const tIndex = dateString.indexOf("T");
132
122
  if (tIndex === -1) return false;
133
123
  if (dateString.endsWith("Z")) return true;
134
- return /[+-]\d{2}:?\d{2}$/.test(dateString.slice(tIndex + 1));
124
+ return /[+-]\d{2}(?::?\d{2})?$/.test(dateString.slice(tIndex + 1));
125
+ };
126
+ const NAIVE = /^(\d{1,4})-(\d{1,2})-(\d{1,2})(?:[T ](\d{1,2}):(\d{1,2})(?::(\d{1,2})(?:\.(\d{1,3}))?)?)?$/;
127
+ const asUtcMs = ([, ...fields]) => {
128
+ const [year, month, day, hour, minute, second, ms] = fields.map((field) => Number(field ?? 0));
129
+ const date = /* @__PURE__ */ new Date(0);
130
+ date.setUTCFullYear(year, month - 1, day);
131
+ date.setUTCHours(hour, minute, second, ms);
132
+ return date.getTime();
135
133
  };
136
134
  /**
137
- * Parses any supported date string format and returns a local timezone date
135
+ * The instant a wall-clock reading names in a given zone.
138
136
  *
139
- * Uses date-fns for robust parsing and validation. For strings without timezone
140
- * info, treats as local timezone. For strings with timezone info, converts to
141
- * local timezone equivalent.
137
+ * A reading a spring-forward gap deleted moves forward past the gap, and one a fall-back
138
+ * repeated resolves to a single instant. Neither is configurable: see CHARTS-268.
142
139
  *
143
- * Supports:
144
- * - YYYY-MM-DD (local)
145
- * - YYYY-MM-DD HH:mm:ss (local)
146
- * - YYYY-MM-DD HH:mm (local)
147
- * - YYYY-MM-DDTHH:mm:ss (local)
148
- * - YYYY-MM-DDTHH:mm:ss.SSS (local)
149
- * - YYYY-MM-DDTHH:mm (local)
150
- * - YYYY-MM-DDTHH:mm:ssZ (UTC → local)
151
- * - YYYY-MM-DDTHH:mm:ss±HH:mm (offset → local)
152
- * @param {string} dateString - The date string to parse into a local timezone date
153
- * @return {Date} A Date object representing the parsed date in local timezone, or an invalid Date if parsing fails
140
+ * @param wallClock - The reading, as the milliseconds it would be if it were UTC.
141
+ * @param timeZone - IANA zone the reading is dated in.
142
+ * @return The instant, or `null` where `timeZone` is not a zone Intl accepts.
154
143
  */
155
- const parseAsLocalDate = (dateString) => {
144
+ const wallClockToInstant = (wallClock, timeZone) => {
145
+ const first = tzOffset(timeZone, new Date(wallClock));
146
+ if (Number.isNaN(first)) {
147
+ warnOnce(`parse:timeZone:${timeZone}`, `timeZone ${JSON.stringify(timeZone)} is not a zone Intl accepts, so dates are read in the browser's zone. Pass an IANA name or a UTC offset such as "+05:30".`);
148
+ return null;
149
+ }
150
+ const guess = wallClock - first * 6e4;
151
+ const second = tzOffset(timeZone, new Date(guess));
152
+ if (first === second) return new Date(guess);
153
+ const corrected = wallClock - second * 6e4;
154
+ const third = tzOffset(timeZone, new Date(corrected));
155
+ if (second === third) return new Date(corrected);
156
+ return /* @__PURE__ */ new Date(wallClock - Math.min(second, third) * 6e4);
157
+ };
158
+ /**
159
+ * Parses any supported date string format into an instant, dated in `timeZone` or the runtime's own
160
+ *
161
+ * Uses date-fns for robust parsing and validation. A string carrying timezone info
162
+ * is already an instant and is returned as one, whatever `timeZone` says. A string
163
+ * without it is a wall-clock reading, dated in `timeZone` when one is supplied and
164
+ * in the runtime's own zone when none is.
165
+ *
166
+ * A wall clock a DST gap deleted is moved forward past the gap; one a fall-back repeated
167
+ * resolves to a single instant. Neither policy is selectable. A zone `Intl` rejects falls
168
+ * back to the runtime's own, and warns once outside production.
169
+ *
170
+ * @param {string} dateString - The date string to parse into a date
171
+ * @param {string} [timeZone] - IANA zone a naive string is read in; the runtime's own when absent, and ignored by a string that carries its own offset
172
+ * @return {Date} A Date object representing the parsed instant, or an invalid Date if parsing fails
173
+ */
174
+ const parseAsLocalDate = (dateString, timeZone) => {
156
175
  const trimmedString = dateString.trim();
157
176
  if (hasTimezone(trimmedString)) {
158
177
  const isoDate = parseISO(trimmedString);
@@ -168,7 +187,10 @@ const parseAsLocalDate = (dateString) => {
168
187
  "yyyy-MM-dd'T'HH:mm"
169
188
  ]) {
170
189
  const result = parse(trimmedString, format, /* @__PURE__ */ new Date());
171
- if (isValid(result)) return result;
190
+ if (!isValid(result)) continue;
191
+ if (!timeZone) return result;
192
+ const fields = NAIVE.exec(trimmedString);
193
+ return (fields && wallClockToInstant(asUtcMs(fields), timeZone)) ?? result;
172
194
  }
173
195
  return /* @__PURE__ */ new Date(NaN);
174
196
  };
@@ -273,6 +295,31 @@ const formatPercentage = (value) => {
273
295
  } });
274
296
  };
275
297
  //#endregion
298
+ //#region src/utils/get-edge-tick-widths.ts
299
+ /**
300
+ * Rendered widths of the first and last tick labels on an axis.
301
+ *
302
+ * An unmeasurable label reserves nothing, which is what a width of 0 already
303
+ * means to every caller, so it is reported as 0 rather than as its own case.
304
+ *
305
+ * @param ticks - Tick values, in axis order.
306
+ * @param formatTick - Function to format a tick.
307
+ * @param {object} labelStyle - Style object for the label.
308
+ * @return {object} - Widths in pixels.
309
+ */
310
+ const getEdgeTickWidths = (ticks, formatTick, labelStyle) => {
311
+ if (!ticks.length) return {
312
+ first: 0,
313
+ last: 0
314
+ };
315
+ const lastIndex = ticks.length - 1;
316
+ const label = (tick, index) => String(formatTick ? formatTick(tick, index, []) ?? "" : tick);
317
+ return {
318
+ first: getStringWidth(label(ticks[0], 0), labelStyle) ?? 0,
319
+ last: getStringWidth(label(ticks[lastIndex], lastIndex), labelStyle) ?? 0
320
+ };
321
+ };
322
+ //#endregion
276
323
  //#region src/utils/get-longest-tick-width.ts
277
324
  /**
278
325
  * Returns the width of the longest tick.
@@ -659,12 +706,6 @@ const getFormatter$1 = (options, { locale, timeZone }) => {
659
706
  }
660
707
  return formatter;
661
708
  };
662
- const warned = /* @__PURE__ */ new Set();
663
- const warnOnce = (key, message) => {
664
- if (warned.has(key) || process.env.NODE_ENV === "production") return;
665
- warned.add(key);
666
- console.warn(`[Charts] ${message}`);
667
- };
668
709
  const isUsable = (formatting) => {
669
710
  try {
670
711
  Intl.DateTimeFormat(formatting.locale, { timeZone: formatting.timeZone });
@@ -1290,13 +1331,28 @@ const useXYChartTheme = (data) => {
1290
1331
  ]);
1291
1332
  };
1292
1333
  //#endregion
1334
+ //#region src/providers/chart-context/hooks/use-chart-formatting.ts
1335
+ const RUNTIME_FORMATTING = {};
1336
+ /**
1337
+ * The locale and time zone dates are rendered in, as set on `GlobalChartsProvider`.
1338
+ *
1339
+ * Falls back to the runtime's own rather than throwing: charts render outside a
1340
+ * provider, and a host that sets neither gets exactly the browser-default
1341
+ * behavior this package had before the context existed.
1342
+ *
1343
+ * @return The host's formatting context.
1344
+ */
1345
+ const useChartFormatting = () => {
1346
+ return useContext(GlobalChartsContext)?.formatting ?? RUNTIME_FORMATTING;
1347
+ };
1348
+ //#endregion
1293
1349
  //#region src/hooks/use-chart-data-transform.ts
1294
1350
  /**
1295
1351
  * Hook that transforms and sorts chart data, handling date parsing and sorting
1296
1352
  *
1297
1353
  * This hook extracts the common data transformation logic used in both line-chart
1298
1354
  * and bar-chart components. It:
1299
- * 1. Parses date strings into Date objects using parseAsLocalDate
1355
+ * 1. Parses date strings into Date objects, dated in the provider's time zone
1300
1356
  * 2. Sorts data points by date when date properties are present
1301
1357
  * 3. Returns the original data unchanged when no date properties are found
1302
1358
  *
@@ -1304,6 +1360,7 @@ const useXYChartTheme = (data) => {
1304
1360
  * @return {SeriesData[]} The transformed and sorted data
1305
1361
  */
1306
1362
  const useChartDataTransform = (data) => {
1363
+ const { timeZone } = useChartFormatting();
1307
1364
  return useMemo(() => {
1308
1365
  const firstPoint = data?.[0]?.data?.[0];
1309
1366
  if (!(firstPoint && ("date" in firstPoint || "dateString" in firstPoint))) return data;
@@ -1312,7 +1369,7 @@ const useChartDataTransform = (data) => {
1312
1369
  data: series.data.map((point) => {
1313
1370
  let date;
1314
1371
  if ("date" in point && point.date) date = point.date;
1315
- else if ("dateString" in point && point.dateString) date = parseAsLocalDate(point.dateString);
1372
+ else if ("dateString" in point && point.dateString) date = parseAsLocalDate(point.dateString, timeZone);
1316
1373
  return {
1317
1374
  ...point,
1318
1375
  date
@@ -1322,7 +1379,7 @@ const useChartDataTransform = (data) => {
1322
1379
  return a.date.getTime() - b.date.getTime();
1323
1380
  })
1324
1381
  }));
1325
- }, [data]);
1382
+ }, [data, timeZone]);
1326
1383
  };
1327
1384
  //#endregion
1328
1385
  //#region src/hooks/use-chart-margin.tsx
@@ -1364,11 +1421,32 @@ const DEFAULT_TICK_LENGTH = 8;
1364
1421
  * measure them via getLongestTickWidth.
1365
1422
  */
1366
1423
  const DEFAULT_Y_TICK_WIDTH = 40;
1424
+ /**
1425
+ * Copy a label style with its lengths spelled out in px.
1426
+ *
1427
+ * `getStringWidth` applies the style through CSSOM. Blink resolves a bare number
1428
+ * on an SVG `<text>`, but that is its own leniency rather than the CSS rule, and
1429
+ * `buildChartTheme` hands us bare numbers.
1430
+ *
1431
+ * @param style - Raw label style from the theme.
1432
+ * @return The same style with px-qualified lengths.
1433
+ */
1434
+ const toMeasurableStyle = (style) => {
1435
+ if (!style) return style;
1436
+ const fontSize = resolveFontSize(style.fontSize);
1437
+ const { letterSpacing } = style;
1438
+ return {
1439
+ ...style,
1440
+ ...fontSize === void 0 ? {} : { fontSize: `${fontSize}px` },
1441
+ ...typeof letterSpacing === "number" ? { letterSpacing: `${letterSpacing}px` } : {}
1442
+ };
1443
+ };
1367
1444
  const getXAxisLabelMetrics = (theme, orientation) => {
1368
1445
  const xAxisStyles = orientation === "top" ? theme.axisStyles?.x?.top : theme.axisStyles?.x?.bottom;
1369
1446
  return {
1370
1447
  fontSize: resolveFontSize(xAxisStyles?.axisLabel?.fontSize) || resolveFontSize(theme.svgLabelSmall?.fontSize) || DEFAULT_FONT_SIZE,
1371
- tickLength: xAxisStyles?.tickLength ?? DEFAULT_TICK_LENGTH
1448
+ tickLength: xAxisStyles?.tickLength ?? DEFAULT_TICK_LENGTH,
1449
+ tickLabelStyle: toMeasurableStyle(xAxisStyles?.tickLabel)
1372
1450
  };
1373
1451
  };
1374
1452
  const useChartMargin = (height, options, data, theme, horizontal = false) => {
@@ -1380,7 +1458,7 @@ const useChartMargin = (height, options, data, theme, horizontal = false) => {
1380
1458
  const maxY = Math.max(...allDataPoints.map((d) => d.value));
1381
1459
  const yScale = createScale({
1382
1460
  ...options.yScale,
1383
- domain: [minY, maxY],
1461
+ domain: options.yScale?.domain ?? [minY, maxY],
1384
1462
  range: [height, 0]
1385
1463
  });
1386
1464
  return getTicks(yScale, options.axis?.y?.numTicks);
@@ -1399,18 +1477,25 @@ const useChartMargin = (height, options, data, theme, horizontal = false) => {
1399
1477
  };
1400
1478
  const yAxisOrientation = options.axis?.y?.orientation;
1401
1479
  const yAxisStyles = yAxisOrientation === "right" ? theme.axisStyles.y.right : theme.axisStyles.y.left;
1402
- const yTickWidth = getLongestTickWidth(yTicks, options.axis?.y?.tickFormat, yAxisStyles.axisLabel);
1480
+ const yTickWidth = getLongestTickWidth(yTicks, options.axis?.y?.tickFormat, toMeasurableStyle(yAxisStyles.axisLabel));
1403
1481
  const yTickLabelFontSize = resolveFontSize(yAxisStyles?.tickLabel?.fontSize) || DEFAULT_FONT_SIZE;
1404
1482
  const yMarginValue = (yTickWidth ?? DEFAULT_Y_TICK_WIDTH) + (yAxisStyles?.tickLength ?? 0) + Math.ceil(yTickLabelFontSize * .25);
1405
- if (yAxisOrientation === "right") defaultMargin.right = yMarginValue;
1406
- else defaultMargin.left = yMarginValue;
1483
+ if (options.axis?.y?.display !== false) {
1484
+ if (yAxisOrientation === "right") defaultMargin.right = yMarginValue;
1485
+ else defaultMargin.left = yMarginValue;
1486
+ }
1407
1487
  const xOrientation = options.axis?.x?.orientation === "top" ? "top" : "bottom";
1408
- const { fontSize, tickLength } = getXAxisLabelMetrics(theme, xOrientation);
1488
+ const { fontSize, tickLength, tickLabelStyle } = getXAxisLabelMetrics(theme, xOrientation);
1409
1489
  const computedXMargin = fontSize + tickLength;
1410
1490
  if (xOrientation === "top") {
1411
1491
  defaultMargin.top = Math.max(defaultMargin.top, computedXMargin);
1412
1492
  defaultMargin.bottom = DEFAULT_BOTTOM_FOR_TOP_AXIS;
1413
1493
  } else defaultMargin.bottom = Math.max(defaultMargin.bottom, computedXMargin);
1494
+ if (options.axis?.x?.display !== false) {
1495
+ const { first, last } = getEdgeTickWidths(options.axis?.x?.tickValues ?? [], options.axis?.x?.tickFormat, tickLabelStyle);
1496
+ defaultMargin.left = Math.max(defaultMargin.left, Math.ceil(first / 2));
1497
+ defaultMargin.right = Math.max(defaultMargin.right, Math.ceil(last / 2));
1498
+ }
1414
1499
  return defaultMargin;
1415
1500
  }, [
1416
1501
  options,
@@ -1686,21 +1771,6 @@ const useGlobalChartsTheme = () => {
1686
1771
  return useContext(GlobalChartsContext)?.theme ?? defaultTheme;
1687
1772
  };
1688
1773
  //#endregion
1689
- //#region src/providers/chart-context/hooks/use-chart-formatting.ts
1690
- const RUNTIME_FORMATTING = {};
1691
- /**
1692
- * The locale and time zone dates are rendered in, as set on `GlobalChartsProvider`.
1693
- *
1694
- * Falls back to the runtime's own rather than throwing: charts render outside a
1695
- * provider, and a host that sets neither gets exactly the browser-default
1696
- * behavior this package had before the context existed.
1697
- *
1698
- * @return The host's formatting context.
1699
- */
1700
- const useChartFormatting = () => {
1701
- return useContext(GlobalChartsContext)?.formatting ?? RUNTIME_FORMATTING;
1702
- };
1703
- //#endregion
1704
1774
  //#region src/components/legend/utils/value-or-identity.ts
1705
1775
  /**
1706
1776
  * Returns an object's value if defined, or the object.
@@ -2140,6 +2210,8 @@ const BaseTooltip = ({ data, top, left, component: Component = DefaultTooltipCon
2140
2210
  //#endregion
2141
2211
  //#region src/components/tooltip/private/bounded-tooltip.tsx
2142
2212
  const DEFAULT_OFFSET = 10;
2213
+ const POINTER_HEIGHT = 6;
2214
+ const clamp = (position, min, max, size) => Math.min(Math.max(position, min), Math.max(min, max - size));
2143
2215
  const isClipping = (element) => {
2144
2216
  const { overflow, overflowX, overflowY } = getComputedStyle(element);
2145
2217
  return [
@@ -2157,33 +2229,49 @@ const findClippingAncestor = (wrapper) => {
2157
2229
  return null;
2158
2230
  };
2159
2231
  /**
2160
- * Where the box goes, in wrapper coordinates. Below and to the right of the
2161
- * anchor; flipped to the other side when that side clips less, as visx's
2162
- * `TooltipWithBounds` decides it; then clamped so the box stays inside
2163
- * `bounds` whenever it fits at all.
2232
+ * Where the box goes, in wrapper coordinates. Automatic placement flips to
2233
+ * the side that clips less, then clamps inside `bounds`; below-axis placement
2234
+ * is centered on the anchor and clamped horizontally.
2164
2235
  *
2165
- * @param params - Anchor, offsets, the box size and the bounds to keep inside.
2236
+ * The flip measures against the visible part of the wrapper, its intersection
2237
+ * with `bounds`. Only the clamp is bound by the page: a box that fits inside the
2238
+ * chart on one side stays there, so it does not reach past the chart into
2239
+ * siblings that paint over it.
2240
+ *
2241
+ * @param params - Anchor, offsets, the box size and the rects to keep inside.
2166
2242
  * @param params.left - Anchor x, in wrapper coordinates.
2167
2243
  * @param params.top - Anchor y, in wrapper coordinates.
2168
2244
  * @param params.offsetLeft - Gap between the anchor and the box, horizontally.
2169
2245
  * @param params.offsetTop - Gap between the anchor and the box, vertically.
2170
2246
  * @param params.box - Rendered size of the box.
2247
+ * @param params.wrapper - The wrapper's own edges, in wrapper coordinates.
2248
+ * @param params.placement - Fixed below-axis placement or automatic flipping.
2171
2249
  * @param params.bounds - Edges the box must keep inside, in wrapper coordinates.
2172
- * @return The box's top-left corner, rounded to whole pixels.
2250
+ * @return The box's top-left corner; fixed placement preserves the axis's subpixel y.
2173
2251
  */
2174
- const getBoundedPosition = ({ left, top, offsetLeft, offsetTop, box, bounds }) => {
2252
+ const getBoundedPosition = ({ left, top, offsetLeft, offsetTop, box, wrapper, bounds, placement = "auto" }) => {
2253
+ if (placement === "below-axis") return {
2254
+ x: Math.round(clamp(left - box.width / 2, bounds.left, bounds.right, box.width)),
2255
+ y: top + POINTER_HEIGHT
2256
+ };
2257
+ const fit = {
2258
+ left: Math.max(wrapper.left, bounds.left),
2259
+ top: Math.max(wrapper.top, bounds.top),
2260
+ right: Math.min(wrapper.right, bounds.right),
2261
+ bottom: Math.min(wrapper.bottom, bounds.bottom)
2262
+ };
2175
2263
  const rightX = left + offsetLeft;
2176
2264
  const leftX = left - offsetLeft - box.width;
2177
- const rightOverflow = rightX + box.width - bounds.right;
2178
- const leftOverflow = bounds.left - leftX;
2265
+ const rightOverflow = rightX + box.width - fit.right;
2266
+ const leftOverflow = fit.left - leftX;
2179
2267
  let x = rightOverflow > 0 && rightOverflow > leftOverflow ? leftX : rightX;
2180
2268
  const downY = top + offsetTop;
2181
2269
  const upY = top - offsetTop - box.height;
2182
- const downOverflow = downY + box.height - bounds.bottom;
2183
- const upOverflow = bounds.top - upY;
2270
+ const downOverflow = downY + box.height - fit.bottom;
2271
+ const upOverflow = fit.top - upY;
2184
2272
  let y = downOverflow > 0 && downOverflow > upOverflow ? upY : downY;
2185
- x = Math.min(Math.max(x, bounds.left), Math.max(bounds.left, bounds.right - box.width));
2186
- y = Math.min(Math.max(y, bounds.top), Math.max(bounds.top, bounds.bottom - box.height));
2273
+ x = clamp(x, bounds.left, bounds.right, box.width);
2274
+ y = clamp(y, bounds.top, bounds.bottom, box.height);
2187
2275
  return {
2188
2276
  x: Math.round(x),
2189
2277
  y: Math.round(y)
@@ -2197,7 +2285,8 @@ const getBoundedPosition = ({ left, top, offsetLeft, offsetTop, box, bounds }) =
2197
2285
  * alone would let the box spill into an `overflow: hidden` card and be cut off.
2198
2286
  *
2199
2287
  * Re-measures on every render, so a box whose content changes width between
2200
- * two hovers is placed for its current size.
2288
+ * two hovers is placed for its current size. See `getBoundedPosition` for the
2289
+ * below-axis exception.
2201
2290
  *
2202
2291
  * @param props - visx `Tooltip` props.
2203
2292
  * @param props.left - Anchor x, in wrapper coordinates.
@@ -2207,9 +2296,10 @@ const getBoundedPosition = ({ left, top, offsetLeft, offsetTop, box, bounds }) =
2207
2296
  * @param props.style - Box styles; visx's defaults unless `unstyled`.
2208
2297
  * @param props.unstyled - Skip `style` and leave the box bare.
2209
2298
  * @param props.children - Box content.
2299
+ * @param props.placement - Fixed below-axis placement or automatic flipping.
2210
2300
  * @return The tooltip box.
2211
2301
  */
2212
- const BoundedTooltip = ({ left = 0, top = 0, offsetLeft = DEFAULT_OFFSET, offsetTop = DEFAULT_OFFSET, style = defaultStyles, unstyled = false, children, ...rest }) => {
2302
+ const BoundedTooltip = ({ left = 0, top = 0, offsetLeft = DEFAULT_OFFSET, offsetTop = DEFAULT_OFFSET, style = defaultStyles, unstyled = false, children, placement = "auto", ...rest }) => {
2213
2303
  const nodeRef = useRef(null);
2214
2304
  const clipRef = useRef(null);
2215
2305
  const [position, setPosition] = useState(null);
@@ -2234,10 +2324,17 @@ const BoundedTooltip = ({ left = 0, top = 0, offsetLeft = DEFAULT_OFFSET, offset
2234
2324
  top,
2235
2325
  offsetLeft,
2236
2326
  offsetTop,
2327
+ placement,
2237
2328
  box: {
2238
2329
  width: own.width,
2239
2330
  height: own.height
2240
2331
  },
2332
+ wrapper: {
2333
+ left: 0,
2334
+ top: 0,
2335
+ right: wrapperRect.width,
2336
+ bottom: wrapperRect.height
2337
+ },
2241
2338
  bounds: {
2242
2339
  left: clipRect.left - wrapperRect.left,
2243
2340
  top: clipRect.top - wrapperRect.top,
@@ -2251,11 +2348,12 @@ const BoundedTooltip = ({ left = 0, top = 0, offsetLeft = DEFAULT_OFFSET, offset
2251
2348
  top,
2252
2349
  offsetLeft,
2253
2350
  offsetTop,
2254
- children
2351
+ children,
2352
+ placement
2255
2353
  ]);
2256
2354
  const x = position?.x ?? left + offsetLeft;
2257
- const y = position?.y ?? top + offsetTop;
2258
- return /* @__PURE__ */ jsx(Tooltip, {
2355
+ const y = position?.y ?? top + (placement === "below-axis" ? POINTER_HEIGHT : offsetTop);
2356
+ return /* @__PURE__ */ jsxs(Tooltip, {
2259
2357
  ref: nodeRef,
2260
2358
  style: {
2261
2359
  position: "absolute",
@@ -2265,12 +2363,40 @@ const BoundedTooltip = ({ left = 0, top = 0, offsetLeft = DEFAULT_OFFSET, offset
2265
2363
  ...!unstyled && style
2266
2364
  },
2267
2365
  ...rest,
2268
- children
2366
+ children: [placement === "below-axis" && /* @__PURE__ */ jsx("span", {
2367
+ "aria-hidden": "true",
2368
+ style: {
2369
+ position: "absolute",
2370
+ left: left - x - POINTER_HEIGHT,
2371
+ top: -6,
2372
+ width: 12,
2373
+ height: POINTER_HEIGHT,
2374
+ background: "inherit",
2375
+ clipPath: "polygon(50% 0, 100% 100%, 0 100%)",
2376
+ pointerEvents: "none"
2377
+ }
2378
+ }), children]
2269
2379
  });
2270
2380
  };
2271
2381
  //#endregion
2272
2382
  //#region src/components/tooltip/xy-chart-tooltip.tsx
2273
2383
  const CROSSHAIR_STROKE_WIDTH = 1.5;
2384
+ const CROSSHAIR_PAINT_PROPERTIES = /* @__PURE__ */ new Set([
2385
+ "stroke",
2386
+ "strokeWidth",
2387
+ "strokeOpacity",
2388
+ "strokeDasharray",
2389
+ "strokeLinecap",
2390
+ "opacity"
2391
+ ]);
2392
+ const crosshairPaintProps = (props = {}) => {
2393
+ const pickPaint = (value) => Object.fromEntries(Object.entries(value).filter(([key]) => CROSSHAIR_PAINT_PROPERTIES.has(key)));
2394
+ return {
2395
+ ...pickPaint(props),
2396
+ ...props.className === void 0 ? {} : { className: props.className },
2397
+ ...props.style === void 0 ? {} : { style: pickPaint(props.style) }
2398
+ };
2399
+ };
2274
2400
  const DEFAULT_GLYPH_RADIUS = 4;
2275
2401
  const FALLBACK_COLOR = "#222";
2276
2402
  const DEFAULT_TOOLTIP_Z_INDEX = 3;
@@ -2301,7 +2427,7 @@ const defaultRenderGlyph$1 = ({ key, ...props }) => /* @__PURE__ */ jsx(DefaultG
2301
2427
  seriesKey: key,
2302
2428
  ...props
2303
2429
  }, key);
2304
- const XyChartTooltipContent = ({ tooltipContext, container, renderTooltip, renderGlyph = defaultRenderGlyph$1, glyphStyle, snapTooltipToDatumX = false, snapTooltipToDatumY = false, showVerticalCrosshair = false, showHorizontalCrosshair = false, showDatumGlyph = false, showSeriesGlyphs = false, verticalCrosshairStyle, horizontalCrosshairStyle, detectBounds = true, zIndex = DEFAULT_TOOLTIP_Z_INDEX, ...rest }) => {
2430
+ const XyChartTooltipContent = ({ tooltipContext, container, renderTooltip, renderGlyph = defaultRenderGlyph$1, glyphStyle, snapTooltipToDatumX = false, snapTooltipToDatumY = false, showVerticalCrosshair = false, showHorizontalCrosshair = false, showDatumGlyph = false, showSeriesGlyphs = false, verticalCrosshairStyle, horizontalCrosshairStyle, detectBounds = true, tooltipPlacement = "auto", zIndex = DEFAULT_TOOLTIP_Z_INDEX, style, ...rest }) => {
2305
2431
  const tooltipProps = Object.fromEntries(Object.entries(rest).filter(([key]) => !PORTAL_OPTIONS.has(key)));
2306
2432
  const { colorScale, theme, innerHeight = 0, innerWidth = 0, margin, xScale, yScale, dataRegistry } = useContext(DataContext) || {};
2307
2433
  const tooltipContent = renderTooltip ? renderTooltip({
@@ -2320,9 +2446,9 @@ const XyChartTooltipContent = ({ tooltipContext, container, renderTooltip, rende
2320
2446
  };
2321
2447
  const nearestDatum = tooltipContext.tooltipData?.nearestDatum;
2322
2448
  let { tooltipLeft, tooltipTop } = tooltipContext;
2323
- if (nearestDatum && (snapTooltipToDatumX || snapTooltipToDatumY)) {
2449
+ if (nearestDatum && (snapTooltipToDatumX || snapTooltipToDatumY || tooltipPlacement === "below-axis")) {
2324
2450
  const { left, top } = getDatumLeftTop(nearestDatum.key, nearestDatum.datum);
2325
- if (snapTooltipToDatumX && isValidNumber(left)) tooltipLeft = left;
2451
+ if ((snapTooltipToDatumX || tooltipPlacement === "below-axis") && isValidNumber(left)) tooltipLeft = left;
2326
2452
  if (snapTooltipToDatumY && isValidNumber(top)) tooltipTop = top;
2327
2453
  }
2328
2454
  const glyphs = [];
@@ -2356,13 +2482,14 @@ const XyChartTooltipContent = ({ tooltipContext, container, renderTooltip, rende
2356
2482
  const crosshairStroke = theme?.gridStyles?.stroke ?? theme?.htmlLabel?.color ?? FALLBACK_COLOR;
2357
2483
  const marginTop = margin?.top ?? 0;
2358
2484
  const marginLeft = margin?.left ?? 0;
2359
- const TooltipComponent = detectBounds ? BoundedTooltip : Tooltip;
2485
+ const TooltipComponent = detectBounds || tooltipPlacement === "below-axis" ? BoundedTooltip : Tooltip;
2360
2486
  const boxStyle = {
2361
2487
  ...defaultStyles,
2362
2488
  zIndex,
2363
- background: theme?.backgroundColor ?? "white",
2489
+ backgroundColor: theme?.backgroundColor ?? "white",
2364
2490
  boxShadow: `0 1px 2px ${theme?.htmlLabel?.color ? `${theme.htmlLabel.color}55` : "#22222255"}`,
2365
- ...theme?.htmlLabel
2491
+ ...theme?.htmlLabel,
2492
+ ...style
2366
2493
  };
2367
2494
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("g", {
2368
2495
  className: "visx-tooltip-overlay",
@@ -2370,32 +2497,33 @@ const XyChartTooltipContent = ({ tooltipContext, container, renderTooltip, rende
2370
2497
  children: [
2371
2498
  showVerticalCrosshair && isValidNumber(tooltipLeft) && /* @__PURE__ */ jsx("line", {
2372
2499
  className: "visx-crosshair visx-crosshair-vertical",
2500
+ stroke: crosshairStroke,
2501
+ strokeWidth: CROSSHAIR_STROKE_WIDTH,
2502
+ ...crosshairPaintProps(verticalCrosshairStyle),
2373
2503
  x1: tooltipLeft,
2374
2504
  x2: tooltipLeft,
2375
2505
  y1: marginTop,
2376
- y2: marginTop + innerHeight,
2377
- stroke: crosshairStroke,
2378
- strokeWidth: CROSSHAIR_STROKE_WIDTH,
2379
- ...verticalCrosshairStyle
2506
+ y2: marginTop + innerHeight
2380
2507
  }),
2381
2508
  showHorizontalCrosshair && isValidNumber(tooltipTop) && /* @__PURE__ */ jsx("line", {
2382
2509
  className: "visx-crosshair visx-crosshair-horizontal",
2510
+ stroke: crosshairStroke,
2511
+ strokeWidth: CROSSHAIR_STROKE_WIDTH,
2512
+ ...crosshairPaintProps(horizontalCrosshairStyle),
2383
2513
  x1: marginLeft,
2384
2514
  x2: marginLeft + innerWidth,
2385
2515
  y1: tooltipTop,
2386
- y2: tooltipTop,
2387
- stroke: crosshairStroke,
2388
- strokeWidth: CROSSHAIR_STROKE_WIDTH,
2389
- ...horizontalCrosshairStyle
2516
+ y2: tooltipTop
2390
2517
  }),
2391
2518
  glyphs
2392
2519
  ]
2393
2520
  }), container && createPortal(/* @__PURE__ */ jsx(TooltipComponent, {
2394
2521
  left: tooltipLeft,
2395
- top: tooltipTop,
2522
+ top: tooltipPlacement === "below-axis" ? marginTop + innerHeight + (margin?.bottom ?? 0) : tooltipTop,
2396
2523
  style: boxStyle,
2397
2524
  applyPositionStyle: true,
2398
2525
  ...tooltipProps,
2526
+ ...tooltipPlacement === "below-axis" && { placement: tooltipPlacement },
2399
2527
  children: tooltipContent
2400
2528
  }), container)] });
2401
2529
  };
@@ -2416,7 +2544,8 @@ const XyChartTooltipContent = ({ tooltipContext, container, renderTooltip, rende
2416
2544
  * isolation keeps the box's `zIndex` from competing with page chrome. The box
2417
2545
  * flips and clamps to stay inside the nearest ancestor that clips its overflow
2418
2546
  * (or the viewport), so it may extend past the chart wrapper but is never cut
2419
- * off unless that ancestor is smaller than the box itself.
2547
+ * off unless that ancestor is smaller than the box itself. Below-axis placement
2548
+ * pins it beneath the x-axis and applies horizontal bounds only.
2420
2549
  *
2421
2550
  * @param props - visx's `Tooltip` options. `scroll`, `debounce` and `resizeObserverPolyfill` are accepted and ignored.
2422
2551
  * @return An anchor in the SVG, plus the overlay and the tooltip box while the tooltip is open.
@@ -2533,11 +2662,14 @@ const AccessibleTooltip = ({ renderTooltip, selectedIndex, tooltipRef, keyboardF
2533
2662
  renderTooltip: focusableRenderTooltip
2534
2663
  });
2535
2664
  };
2536
- const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating, setIsNavigating, chartRef, totalPoints, onActivate }) => {
2665
+ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating, setIsNavigating, chartRef, totalPoints, onActivate, preventTooltipScroll = false }) => {
2537
2666
  return {
2538
2667
  tooltipRef: useCallback((element) => {
2539
- if (element && selectedIndex !== void 0) element.focus();
2540
- }, [selectedIndex]),
2668
+ if (element && selectedIndex !== void 0) {
2669
+ if (preventTooltipScroll) element.focus({ preventScroll: true });
2670
+ else element.focus();
2671
+ }
2672
+ }, [preventTooltipScroll, selectedIndex]),
2541
2673
  onChartFocus: useCallback(() => {
2542
2674
  if (!isNavigating && selectedIndex !== void 0) setSelectedIndex(0);
2543
2675
  }, [
@@ -3869,10 +4001,11 @@ const TooltipDate = ({ date, displayResolution }) => {
3869
4001
  * one row per visible series (label + formatted value), sorted descending by
3870
4002
  * value. Reused by AreaChart, which has the same multi-series shape.
3871
4003
  *
3872
- * @param params - visx `RenderTooltipParams< DataPointDate >`, plus the chart's optional `bucketInfo`.
4004
+ * @param params - visx tooltip data and the chart's optional `bucketInfo`.
4005
+ * @param contentStyle - Explicit tooltip content color overrides.
3873
4006
  * @return Tooltip JSX, or `null` when no datum is hovered.
3874
4007
  */
3875
- const renderDefaultTooltip = (params) => {
4008
+ const renderDefaultTooltip = (params, contentStyle) => {
3876
4009
  const { tooltipData, bucketInfo } = params;
3877
4010
  const nearestDatum = tooltipData?.nearestDatum?.datum;
3878
4011
  if (!nearestDatum) return null;
@@ -3882,6 +4015,7 @@ const renderDefaultTooltip = (params) => {
3882
4015
  })).sort((a, b) => b.value - a.value);
3883
4016
  return /* @__PURE__ */ jsxs("div", {
3884
4017
  className: line_chart_module_default["line-chart__tooltip"],
4018
+ style: contentStyle,
3885
4019
  children: [/* @__PURE__ */ jsx("div", {
3886
4020
  className: line_chart_module_default["line-chart__tooltip-date"],
3887
4021
  children: /* @__PURE__ */ jsx(TooltipDate, {
@@ -3931,7 +4065,7 @@ const LineChartScalesRef = ({ chartRef, width, height, margin }) => {
3931
4065
  ]);
3932
4066
  return null;
3933
4067
  };
3934
- const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, height, className, margin, withTooltips = true, withTooltipCrosshairs, showLegend = false, legend = {}, renderGlyph = defaultRenderGlyph, glyphStyle = {}, withLegendGlyph = false, withGradientFill = false, smoothing = true, curveType, renderTooltip = renderDefaultTooltip, withStartGlyphs = false, withEndGlyphs = false, animation, options = {}, onPointerDown = void 0, onPointerUp = void 0, onPointerMove = void 0, onPointerOut = void 0, onDatumActivate = void 0, zoomable = false, rescaleYOnVisibilityChange = true, defaultHiddenSeries, children, gridVisibility, gap = "md" }, ref) => {
4068
+ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, height, className, margin, withTooltips = true, withTooltipCrosshairs, showLegend = false, legend = {}, renderGlyph = defaultRenderGlyph, glyphStyle = {}, withLegendGlyph = false, withGradientFill = false, smoothing = true, curveType, renderTooltip = renderDefaultTooltip, tooltipPlacement, tooltipStyle, withStartGlyphs = false, withEndGlyphs = false, animation, options = {}, onPointerDown = void 0, onPointerUp = void 0, onPointerMove = void 0, onPointerOut = void 0, onDatumActivate = void 0, zoomable = false, rescaleYOnVisibilityChange = true, defaultHiddenSeries, children, gridVisibility, gap = "md" }, ref) => {
3935
4069
  const legendInteractive = legend.interactive ?? false;
3936
4070
  const legendCollapseGroups = legend.collapseGroups ?? false;
3937
4071
  const legendShape = legend.shape ?? "line";
@@ -4009,7 +4143,8 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4009
4143
  setIsNavigating,
4010
4144
  chartRef,
4011
4145
  totalPoints: dataSorted[0]?.data.length || 0,
4012
- onActivate: activateSelectedPoint
4146
+ onActivate: activateSelectedPoint,
4147
+ preventTooltipScroll: tooltipPlacement === "below-axis"
4013
4148
  });
4014
4149
  const chartOptions = useMemo(() => {
4015
4150
  return {
@@ -4118,10 +4253,29 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4118
4253
  xAccessor: (d) => d?.date,
4119
4254
  yAccessor: (d) => d?.value
4120
4255
  };
4121
- const tooltipRenderer = useMemo(() => (params) => renderTooltip({
4256
+ const resolvedTooltipStyle = useMemo(() => {
4257
+ if (renderTooltip !== renderDefaultTooltip || !tooltipStyle) return tooltipStyle;
4258
+ if (!tooltipStyle.color || tooltipStyle.background || tooltipStyle.backgroundColor) return tooltipStyle;
4259
+ return {
4260
+ backgroundColor: "var(--a8c-charts-color-tooltip-surface, rgb(0 0 0 / 85%))",
4261
+ ...tooltipStyle
4262
+ };
4263
+ }, [renderTooltip, tooltipStyle]);
4264
+ const tooltipRenderer = useMemo(() => (params) => renderTooltip === renderDefaultTooltip ? renderDefaultTooltip({
4122
4265
  ...params,
4123
4266
  bucketInfo
4124
- }), [renderTooltip, bucketInfo]);
4267
+ }, {
4268
+ color: tooltipStyle?.color,
4269
+ background: tooltipStyle?.background,
4270
+ backgroundColor: tooltipStyle?.backgroundColor
4271
+ }) : renderTooltip({
4272
+ ...params,
4273
+ bucketInfo
4274
+ }), [
4275
+ renderTooltip,
4276
+ bucketInfo,
4277
+ tooltipStyle
4278
+ ]);
4125
4279
  if (error) return /* @__PURE__ */ jsx("div", {
4126
4280
  className: clsx("line-chart", line_chart_module_default["line-chart"]),
4127
4281
  children: error
@@ -4260,6 +4414,8 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4260
4414
  withTooltips && /* @__PURE__ */ jsx(AccessibleTooltip, {
4261
4415
  detectBounds: true,
4262
4416
  snapTooltipToDatumX: true,
4417
+ tooltipPlacement,
4418
+ style: resolvedTooltipStyle,
4263
4419
  snapTooltipToDatumY: true,
4264
4420
  showSeriesGlyphs: true,
4265
4421
  renderTooltip: tooltipRenderer,
@@ -4267,6 +4423,8 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4267
4423
  glyphStyle,
4268
4424
  showVerticalCrosshair: withTooltipCrosshairs?.showVertical,
4269
4425
  showHorizontalCrosshair: withTooltipCrosshairs?.showHorizontal,
4426
+ verticalCrosshairStyle: withTooltipCrosshairs?.verticalStyle,
4427
+ horizontalCrosshairStyle: withTooltipCrosshairs?.horizontalStyle,
4270
4428
  selectedIndex,
4271
4429
  tooltipRef,
4272
4430
  keyboardFocusedClassName: line_chart_module_default["line-chart__tooltip--keyboard-focused"],
@@ -6419,9 +6577,13 @@ var heatmap_chart_module_default = {
6419
6577
  "heatmap-chart__cell--placeholder": "a8ccharts-O3YMOW-heatmap-chart__cell--placeholder",
6420
6578
  "heatmap-chart__cell--selected": "a8ccharts-O3YMOW-heatmap-chart__cell--selected",
6421
6579
  "heatmap-chart__cell--strong": "a8ccharts-O3YMOW-heatmap-chart__cell--strong",
6580
+ "heatmap-chart__cell--summary": "a8ccharts-O3YMOW-heatmap-chart__cell--summary",
6422
6581
  "heatmap-chart__cell-value": "a8ccharts-O3YMOW-heatmap-chart__cell-value",
6423
6582
  "heatmap-chart__col-label": "a8ccharts-O3YMOW-heatmap-chart__col-label",
6583
+ "heatmap-chart__col-label--summary": "a8ccharts-O3YMOW-heatmap-chart__col-label--summary",
6424
6584
  "heatmap-chart__empty": "a8ccharts-O3YMOW-heatmap-chart__empty",
6585
+ "heatmap-chart__gap-end": "a8ccharts-O3YMOW-heatmap-chart__gap-end",
6586
+ "heatmap-chart__gap-start": "a8ccharts-O3YMOW-heatmap-chart__gap-start",
6425
6587
  "heatmap-chart__grid": "a8ccharts-O3YMOW-heatmap-chart__grid",
6426
6588
  "heatmap-chart__grid--compact": "a8ccharts-O3YMOW-heatmap-chart__grid--compact",
6427
6589
  "heatmap-chart__grid--height-capped": "a8ccharts-O3YMOW-heatmap-chart__grid--height-capped",
@@ -6435,17 +6597,21 @@ var heatmap_chart_module_default = {
6435
6597
  //#region src/charts/heatmap-chart/private/use-heatmap-colors.ts
6436
6598
  const isPresent = (value) => value !== null && value !== void 0 && !isNaN(value);
6437
6599
  /**
6438
- * Get the min and max values from heatmap data, ignoring null/NaN.
6600
+ * Get the min and max values from heatmap data, ignoring null/NaN. Summary
6601
+ * columns stay out: a roll-up on the scale would flatten every real cell.
6439
6602
  * @param data - The heatmap columns
6440
6603
  * @return Tuple of [min, max] values
6441
6604
  */
6442
6605
  const getValueExtent = (data) => {
6443
6606
  let min = Infinity;
6444
6607
  let max = -Infinity;
6445
- for (const column of data) for (const cell of column.data) {
6446
- if (!isPresent(cell.value)) continue;
6447
- if (cell.value < min) min = cell.value;
6448
- if (cell.value > max) max = cell.value;
6608
+ for (const column of data) {
6609
+ if (column.summary) continue;
6610
+ for (const cell of column.data) {
6611
+ if (!isPresent(cell.value)) continue;
6612
+ if (cell.value < min) min = cell.value;
6613
+ if (cell.value > max) max = cell.value;
6614
+ }
6449
6615
  }
6450
6616
  if (min === Infinity) return [0, 0];
6451
6617
  return [min, max];
@@ -6466,6 +6632,119 @@ const getNormalizedValue = (value, extent) => {
6466
6632
  return Math.min(1, Math.max(0, (value - min) / (max - min)));
6467
6633
  };
6468
6634
  //#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
6469
6748
  //#region src/charts/heatmap-chart/private/build-calendar-data.ts
6470
6749
  /** Rows that get a weekday label (Mon, Wed, Fri with a Monday week start). */
6471
6750
  const LABELLED_ROWS = [
@@ -6480,69 +6759,88 @@ const LABELLED_ROWS = [
6480
6759
  * @param bound - Requested bound as `yyyy-MM-dd`, if any.
6481
6760
  * @param fallback - The series' own bound on this side.
6482
6761
  * @param direction - Which way the bound is allowed to move.
6483
- * @return The bound to draw to.
6762
+ * @return The day key to draw to.
6484
6763
  */
6485
6764
  const widenTo = (bound, fallback, direction) => {
6486
- if (!bound) return fallback;
6487
- const parsed = parseISO(bound);
6488
- if (isNaN(parsed.getTime())) return fallback;
6489
- if (direction === "earlier") return parsed < fallback ? parsed : fallback;
6490
- return parsed > fallback ? parsed : fallback;
6491
- };
6492
- const toDate = (point) => {
6493
- if (point.date instanceof Date && !isNaN(point.date.getTime())) return point.date;
6494
- if (point.dateString) {
6495
- const parsed = parseISO(point.dateString);
6496
- if (!isNaN(parsed.getTime())) return parsed;
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;
6497
6769
  }
6498
- return null;
6770
+ if (direction === "earlier") return key < fallback ? key : fallback;
6771
+ return key > fallback ? key : fallback;
6499
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
+ */
6500
6780
  const buildCalendarHeatmapData = (series, options = {}) => {
6501
6781
  const weekStartsOn = options.weekStartsOn ?? 1;
6502
6782
  const hideOutOfRangeDays = options.hideOutOfRangeDays ?? true;
6503
- const entries = series.map((point) => ({
6504
- date: toDate(point),
6505
- value: point.value
6506
- })).filter((entry) => entry.date !== null);
6507
- if (!entries.length) return {
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 {
6508
6803
  data: [],
6509
6804
  rowLabels: []
6510
6805
  };
6511
- const valueByDay = /* @__PURE__ */ new Map();
6512
- let minDate = entries[0].date;
6513
- let maxDate = entries[0].date;
6514
- for (const { date, value } of entries) {
6515
- valueByDay.set(format(date, "yyyy-MM-dd"), value);
6516
- if (date < minDate) minDate = date;
6517
- if (date > maxDate) maxDate = date;
6518
- }
6519
- const minDayKey = format(minDate, "yyyy-MM-dd");
6520
- const maxDayKey = format(maxDate, "yyyy-MM-dd");
6521
- const gridMinDate = widenTo(options.gridSpan?.start, minDate, "earlier");
6522
- const gridMaxDate = widenTo(options.gridSpan?.end, maxDate, "later");
6523
- const gridMaxDayKey = format(gridMaxDate, "yyyy-MM-dd");
6524
- const gridStart = startOfWeek(gridMinDate, { weekStartsOn });
6525
- const gridMinDayKey = format(gridMinDate < minDate ? gridStart : gridMinDate, "yyyy-MM-dd");
6526
- const weekCount = differenceInCalendarWeeks(gridMaxDate, gridStart, { weekStartsOn }) + 1;
6527
- const rowLabels = Array.from({ length: 7 }, (_, row) => LABELLED_ROWS.includes(row) ? format(addDays(gridStart, row), "EEE") : "");
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)) : "");
6528
6826
  const MIN_FIRST_MONTH_WEEKS = 2;
6529
- const firstMonth = gridStart.getMonth();
6827
+ const firstMonth = gridStart.getUTCMonth();
6530
6828
  let firstMonthWeeks = 0;
6531
- while (firstMonthWeeks < weekCount && addDays(gridStart, firstMonthWeeks * 7).getMonth() === firstMonth) firstMonthWeeks++;
6829
+ while (firstMonthWeeks < weekCount && addCivilDays(gridStart, firstMonthWeeks * 7).getUTCMonth() === firstMonth) firstMonthWeeks++;
6532
6830
  const showFirstMonthLabel = !(firstMonthWeeks < weekCount) || firstMonthWeeks >= MIN_FIRST_MONTH_WEEKS;
6533
6831
  const data = [];
6534
6832
  let previousMonth = -1;
6535
6833
  for (let week = 0; week < weekCount; week++) {
6536
- const columnStart = addDays(gridStart, week * 7);
6537
- const month = columnStart.getMonth();
6538
- const label = month !== previousMonth && (week !== 0 || showFirstMonthLabel) ? format(columnStart, "MMM") : "";
6834
+ const columnStart = addCivilDays(gridStart, week * 7);
6835
+ const month = columnStart.getUTCMonth();
6836
+ const label = month !== previousMonth && (week !== 0 || showFirstMonthLabel) ? formatMonth(columnStart) : "";
6539
6837
  previousMonth = month;
6540
6838
  const cells = [];
6541
6839
  for (let row = 0; row < 7; row++) {
6542
- const day = addDays(gridStart, week * 7 + row);
6543
- const key = format(day, "yyyy-MM-dd");
6840
+ const day = addCivilDays(gridStart, week * 7 + row);
6841
+ const key = civilKey(day);
6544
6842
  const cell = {
6545
- label: format(day, "EEE, MMM d, yyyy"),
6843
+ label: formatDay(day),
6546
6844
  value: valueByDay.has(key) ? valueByDay.get(key) : null
6547
6845
  };
6548
6846
  if (key < gridMinDayKey || key > gridMaxDayKey) {
@@ -6610,7 +6908,8 @@ const HeatmapLegend = ({ steps = 5, lessLabel, moreLabel }) => {
6610
6908
  //#endregion
6611
6909
  //#region src/charts/heatmap-chart/heatmap-chart.tsx
6612
6910
  const CELL_MIX_FLOOR = .15;
6613
- const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, height = 0, className, compact = false, showValues, maxCellWidth, maxCellHeight, minCellWidth, minCellHeight, rowLabels = [], primaryColor, gap = "md", withTooltips = false, renderTooltip, children }) => {
6911
+ const NO_ROW_LABELS = [];
6912
+ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, height = 0, className, compact = false, showValues, maxCellWidth, maxCellHeight, minCellWidth, minCellHeight, rowLabels = NO_ROW_LABELS, primaryColor, gap = "md", withTooltips = false, renderTooltip, children }) => {
6614
6913
  const chartId = useChartId(providedChartId);
6615
6914
  const { getElementStyles, theme } = useGlobalChartsContext();
6616
6915
  const scopeElement = useChartScopeElement();
@@ -6761,13 +7060,20 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6761
7060
  const rowTrack = compact ? "var(--a8c-charts-dimension-heatmap-cell-size)" : `minmax(${minCellHeight ?? 0}px, ${maxCellHeight ? `${maxCellHeight}px` : "1fr"})`;
6762
7061
  const gridStyle = {
6763
7062
  "--a8c-charts-color-heatmap-primary": primaryColorHex,
6764
- gridTemplateColumns: `auto repeat(${columns}, ${columnTrack})`,
7063
+ gridTemplateColumns: `auto ${data.some((column) => column.summary) ? data.map((column) => column.summary ? "minmax(auto, max-content)" : columnTrack).join(" ") : `repeat(${columns}, ${columnTrack})`}`,
6765
7064
  gridTemplateRows: `auto repeat(${rows}, ${rowTrack})`
6766
7065
  };
6767
7066
  if (compact) {
6768
7067
  gridStyle["--a8c-charts-dimension-heatmap-cell-gap"] = `${compactCellGap}px`;
6769
7068
  gridStyle["--a8c-charts-dimension-heatmap-cell-size"] = `${compactCellSize}px`;
6770
7069
  }
7070
+ const summaryGaps = (columnIndex) => {
7071
+ if (!data[columnIndex]?.summary) return {};
7072
+ return {
7073
+ [heatmap_chart_module_default["heatmap-chart__gap-start"]]: columnIndex > 0 && !data[columnIndex - 1]?.summary,
7074
+ [heatmap_chart_module_default["heatmap-chart__gap-end"]]: columnIndex < columns - 1 && !data[columnIndex + 1]?.summary
7075
+ };
7076
+ };
6771
7077
  const activeDescendant = selectedIndex !== void 0 ? `${chartId}-cell-${Math.floor(selectedIndex / rows)}-${selectedIndex % rows}` : void 0;
6772
7078
  const heightCapped = !compact && Boolean(maxCellHeight);
6773
7079
  return /* @__PURE__ */ jsx(HeatmapContext.Provider, {
@@ -6805,7 +7111,10 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6805
7111
  "aria-hidden": "true",
6806
7112
  className: heatmap_chart_module_default["heatmap-chart__row"],
6807
7113
  children: [/* @__PURE__ */ jsx("span", {}), data.map((column, columnIndex) => /* @__PURE__ */ jsx("span", {
6808
- className: heatmap_chart_module_default["heatmap-chart__col-label"],
7114
+ className: clsx(heatmap_chart_module_default["heatmap-chart__col-label"], {
7115
+ [heatmap_chart_module_default["heatmap-chart__col-label--summary"]]: column.summary,
7116
+ ...summaryGaps(columnIndex)
7117
+ }),
6809
7118
  children: column.label
6810
7119
  }, `col-${columnIndex}`))]
6811
7120
  }), Array.from({ length: rows }).map((_row, rowIndex) => {
@@ -6830,7 +7139,8 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6830
7139
  }, `cell-${columnIndex}-${rowIndex}`);
6831
7140
  const value = cell?.value ?? null;
6832
7141
  const present = isPresent(value);
6833
- const normalized = present ? getNormalizedValue(value, extent) : 0;
7142
+ const filled = present && !column.summary;
7143
+ const normalized = filled ? getNormalizedValue(value, extent) : 0;
6834
7144
  const flatIndex = columnIndex * rows + rowIndex;
6835
7145
  const info = buildTooltipData(columnIndex, rowIndex);
6836
7146
  const accessibleLabel = `${info.cellLabel || `${info.columnLabel ?? ""} ${info.rowLabel ?? ""}`.trim()}: ${info.value === null ? __("No data", "jetpack-charts") : formatNumber(info.value)}`;
@@ -6843,14 +7153,16 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6843
7153
  "data-column": columnIndex,
6844
7154
  "data-row": rowIndex,
6845
7155
  className: clsx(heatmap_chart_module_default["heatmap-chart__cell"], {
6846
- [heatmap_chart_module_default["heatmap-chart__cell--filled"]]: present,
6847
- [heatmap_chart_module_default["heatmap-chart__cell--strong"]]: present && cellHasLightText(normalized),
7156
+ [heatmap_chart_module_default["heatmap-chart__cell--filled"]]: filled,
7157
+ [heatmap_chart_module_default["heatmap-chart__cell--strong"]]: filled && cellHasLightText(normalized),
7158
+ [heatmap_chart_module_default["heatmap-chart__cell--summary"]]: column.summary,
7159
+ ...summaryGaps(columnIndex),
6848
7160
  [heatmap_chart_module_default["heatmap-chart__cell--selected"]]: selectedIndex === flatIndex
6849
7161
  }),
6850
- style: present ? { "--a8c-charts-heatmap-cell-intensity": normalized } : void 0,
7162
+ style: filled ? { "--a8c-charts-heatmap-cell-intensity": normalized } : void 0,
6851
7163
  onMouseMove: handleCellMouseMove,
6852
7164
  onMouseLeave: handleCellMouseLeave,
6853
- children: drawValues && present && /* @__PURE__ */ jsx("span", {
7165
+ children: (drawValues || column.summary) && present && /* @__PURE__ */ jsx("span", {
6854
7166
  className: heatmap_chart_module_default["heatmap-chart__cell-value"],
6855
7167
  children: formatNumberCompact(value)
6856
7168
  })
@@ -6886,6 +7198,42 @@ const HeatmapChartResponsiveInner = (props) => /* @__PURE__ */ jsx(HeatmapChartW
6886
7198
  HeatmapChartResponsiveInner.displayName = "HeatmapChart";
6887
7199
  const HeatmapChartResponsive = attachSubComponents(withResponsive(HeatmapChartResponsiveInner), { Legend: HeatmapLegend });
6888
7200
  //#endregion
7201
+ //#region src/charts/heatmap-chart/use-calendar-heatmap-data.ts
7202
+ /**
7203
+ * `buildCalendarHeatmapData` with the locale and zone taken from
7204
+ * `GlobalChartsProvider` where the caller names neither.
7205
+ *
7206
+ * @param series - Points to bucket. Held by reference, so a caller that rebuilds it each render defeats the memo.
7207
+ * @param options - As for `buildCalendarHeatmapData`; `locale` and `timeZone` win over the provider.
7208
+ * @return Columns and row labels for `HeatmapChart`.
7209
+ */
7210
+ const useCalendarHeatmapData = (series, options = {}) => {
7211
+ const formatting = useChartFormatting();
7212
+ const locale = options.locale ?? formatting.locale;
7213
+ const timeZone = options.timeZone ?? formatting.timeZone;
7214
+ const { weekStartsOn, hideOutOfRangeDays } = options;
7215
+ const gridStart = options.gridSpan?.start;
7216
+ const gridEnd = options.gridSpan?.end;
7217
+ return useMemo(() => buildCalendarHeatmapData(series, {
7218
+ locale,
7219
+ timeZone,
7220
+ weekStartsOn,
7221
+ hideOutOfRangeDays,
7222
+ gridSpan: {
7223
+ start: gridStart,
7224
+ end: gridEnd
7225
+ }
7226
+ }), [
7227
+ series,
7228
+ locale,
7229
+ timeZone,
7230
+ weekStartsOn,
7231
+ hideOutOfRangeDays,
7232
+ gridStart,
7233
+ gridEnd
7234
+ ]);
7235
+ };
7236
+ //#endregion
6889
7237
  //#region ../../../node_modules/.pnpm/is-plain-object@5.1.0/node_modules/is-plain-object/dist/is-plain-object.mjs
6890
7238
  /*!
6891
7239
  * is-plain-object <https://github.com/jonschlinkert/is-plain-object>
@@ -6953,7 +7301,7 @@ function paramCase(input, options) {
6953
7301
  return dotCase(input, __assign({ delimiter: "-" }, options));
6954
7302
  }
6955
7303
  //#endregion
6956
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/utils/hooks/use-update-effect.mjs
7304
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/utils/hooks/use-update-effect.mjs
6957
7305
  function useUpdateEffect(effect, deps) {
6958
7306
  const mountedRef = useRef$1(false);
6959
7307
  useEffect$1(() => {
@@ -8523,7 +8871,7 @@ _createEmotion.css;
8523
8871
  _createEmotion.sheet;
8524
8872
  _createEmotion.cache;
8525
8873
  //#endregion
8526
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/utils/hooks/use-cx.mjs
8874
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/utils/hooks/use-cx.mjs
8527
8875
  var isSerializedStyles = (o) => typeof o !== "undefined" && o !== null && ["name", "styles"].every((p) => typeof o[p] !== "undefined");
8528
8876
  var useCx = () => {
8529
8877
  const cache = __unsafe_useEmotionCache();
@@ -8630,7 +8978,7 @@ function memize(fn, options) {
8630
8978
  return memoized;
8631
8979
  }
8632
8980
  //#endregion
8633
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/utils/colors-values.mjs
8981
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/utils/colors-values.mjs
8634
8982
  var white = "#fff";
8635
8983
  var GRAY = {
8636
8984
  900: "#1e1e1e",
@@ -8709,7 +9057,7 @@ var COLORS = Object.freeze({
8709
9057
  ui: UI
8710
9058
  });
8711
9059
  //#endregion
8712
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/utils/config-values.mjs
9060
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/utils/config-values.mjs
8713
9061
  var CONTROL_HEIGHT = "36px";
8714
9062
  var CONTROL_PROPS = {
8715
9063
  controlPaddingX: 12,
@@ -8827,7 +9175,7 @@ function warning(message) {
8827
9175
  logged.add(message);
8828
9176
  }
8829
9177
  //#endregion
8830
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/context/context-system-provider.mjs
9178
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/context/context-system-provider.mjs
8831
9179
  var import_es6 = /* @__PURE__ */ __toESM(require_es6(), 1);
8832
9180
  var ComponentsContext = createContext$1(
8833
9181
  /** @type {Record<string, any>} */
@@ -8854,18 +9202,18 @@ var BaseContextSystemProvider = ({ children, value }) => {
8854
9202
  };
8855
9203
  memo(BaseContextSystemProvider);
8856
9204
  //#endregion
8857
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/context/constants.mjs
9205
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/context/constants.mjs
8858
9206
  var COMPONENT_NAMESPACE = "data-wp-component";
8859
9207
  var CONNECTED_NAMESPACE = "data-wp-c16t";
8860
9208
  var CONNECT_STATIC_NAMESPACE = "__contextSystemKey__";
8861
9209
  //#endregion
8862
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/context/get-styled-class-name-from-key.mjs
9210
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/context/get-styled-class-name-from-key.mjs
8863
9211
  function getStyledClassName(namespace) {
8864
9212
  return `components-${paramCase(namespace)}`;
8865
9213
  }
8866
9214
  var getStyledClassNameFromKey = memize(getStyledClassName);
8867
9215
  //#endregion
8868
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/context/context-connect.mjs
9216
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/context/context-connect.mjs
8869
9217
  function contextConnect(Component, namespace) {
8870
9218
  return _contextConnect(Component, namespace, { forwardsRef: true });
8871
9219
  }
@@ -8882,7 +9230,7 @@ function _contextConnect(Component, namespace, options) {
8882
9230
  });
8883
9231
  }
8884
9232
  //#endregion
8885
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/context/utils.mjs
9233
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/context/utils.mjs
8886
9234
  function getNamespace(componentName) {
8887
9235
  return { [COMPONENT_NAMESPACE]: componentName };
8888
9236
  }
@@ -8890,7 +9238,7 @@ function getConnectedNamespace() {
8890
9238
  return { [CONNECTED_NAMESPACE]: true };
8891
9239
  }
8892
9240
  //#endregion
8893
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/context/use-context-system.mjs
9241
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/context/use-context-system.mjs
8894
9242
  function useContextSystem(props, namespace) {
8895
9243
  const contextSystemProps = useComponentsContext();
8896
9244
  if (typeof namespace === "undefined") globalThis.SCRIPT_DEBUG === true && warning("useContextSystem: Please provide a namespace");
@@ -8910,7 +9258,7 @@ function useContextSystem(props, namespace) {
8910
9258
  return finalComponentProps;
8911
9259
  }
8912
9260
  //#endregion
8913
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/visually-hidden/styles.mjs
9261
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/visually-hidden/styles.mjs
8914
9262
  var visuallyHidden = {
8915
9263
  border: 0,
8916
9264
  clip: "rect(1px, 1px, 1px, 1px)",
@@ -8926,7 +9274,7 @@ var visuallyHidden = {
8926
9274
  wordBreak: "normal"
8927
9275
  };
8928
9276
  //#endregion
8929
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/utils/polymorphic-element.mjs
9277
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/utils/polymorphic-element.mjs
8930
9278
  var customAttributeRegExp = /^(data|aria|x)-/i;
8931
9279
  var eventHandlerRegExp = /^on[A-Z]/;
8932
9280
  var svgElementNames = new Set(`animate animateMotion animateTransform circle clipPath defs desc ellipse
@@ -9016,7 +9364,7 @@ function UnforwardedPolymorphicElement({ as, ...props }, ref) {
9016
9364
  }
9017
9365
  var PolymorphicElement = forwardRef$1(UnforwardedPolymorphicElement);
9018
9366
  //#endregion
9019
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/view/component.mjs
9367
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/view/component.mjs
9020
9368
  function UnforwardedView({ css, ...restProps }, ref) {
9021
9369
  return /* @__PURE__ */ jsx(PolymorphicElement, {
9022
9370
  ref,
@@ -9025,7 +9373,7 @@ function UnforwardedView({ css, ...restProps }, ref) {
9025
9373
  }
9026
9374
  var component_default$2 = Object.assign(forwardRef$1(UnforwardedView), { selector: ".components-view" });
9027
9375
  //#endregion
9028
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/visually-hidden/component.mjs
9376
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/visually-hidden/component.mjs
9029
9377
  function UnconnectedVisuallyHidden(props, forwardedRef) {
9030
9378
  const { style: styleProp, ...contextProps } = useContextSystem(props, "VisuallyHidden");
9031
9379
  return /* @__PURE__ */ jsx(component_default$2, {
@@ -9040,7 +9388,7 @@ function UnconnectedVisuallyHidden(props, forwardedRef) {
9040
9388
  }
9041
9389
  var component_default$1 = contextConnect(UnconnectedVisuallyHidden, "VisuallyHidden");
9042
9390
  //#endregion
9043
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/utils/use-responsive-value.mjs
9391
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/utils/use-responsive-value.mjs
9044
9392
  var breakpoints = [
9045
9393
  "40em",
9046
9394
  "52em",
@@ -9074,7 +9422,7 @@ function useResponsiveValue(values, options = {}) {
9074
9422
  return array[index >= array.length ? array.length - 1 : index];
9075
9423
  }
9076
9424
  //#endregion
9077
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/grid/utils.mjs
9425
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/grid/utils.mjs
9078
9426
  var ALIGNMENTS = {
9079
9427
  bottom: {
9080
9428
  alignItems: "flex-end",
@@ -9122,7 +9470,7 @@ function getAlignmentProps(alignment) {
9122
9470
  return alignment ? ALIGNMENTS[alignment] : {};
9123
9471
  }
9124
9472
  //#endregion
9125
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/grid/hook.mjs
9473
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/grid/hook.mjs
9126
9474
  function useGrid(props) {
9127
9475
  const { align, alignment, className, columnGap, columns = 2, gap = 3, isInline = false, justify, rowGap, rows, templateColumns, templateRows, ...otherProps } = useContextSystem(props, "Grid");
9128
9476
  const column = useResponsiveValue(Array.isArray(columns) ? columns : [columns]);
@@ -9164,7 +9512,7 @@ function useGrid(props) {
9164
9512
  };
9165
9513
  }
9166
9514
  //#endregion
9167
- //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_d122c1aea9dc8a45ffffc1d3c2f19116/node_modules/@wordpress/components/build-module/grid/component.mjs
9515
+ //#region ../../../node_modules/.pnpm/@wordpress+components@40.0.0_@date-fns+tz@1.4.1_@types+react-dom@18.3.7_@types+react@18_4546240ebdf94d561ebf2fde285113fe/node_modules/@wordpress/components/build-module/grid/component.mjs
9168
9516
  function UnconnectedGrid(props, forwardedRef) {
9169
9517
  const gridProps = useGrid(props);
9170
9518
  return /* @__PURE__ */ jsx(component_default$2, {
@@ -10420,6 +10768,6 @@ function TrendIndicator({ direction, value, className, style, showIcon = true })
10420
10768
  });
10421
10769
  }
10422
10770
  //#endregion
10423
- export { AccessibleTooltip, AreaChartResponsive as AreaChart, AreaChart as AreaChartUnresponsive, BarChartResponsive as BarChart, BarChart as BarChartUnresponsive, BarListChartResponsive as BarListChart, BarListChart as BarListChartUnresponsive, BaseTooltip, ConversionFunnelChartWithProvider as ConversionFunnelChart, GeoChartResponsive as GeoChart, GeoChartWithProvider as GeoChartUnresponsive, GlobalChartsContext, GlobalChartsProvider, GlobalChartsProvider as ThemeProvider, GoogleDataTableColumnRoleType, HeatmapChartResponsive as HeatmapChart, HeatmapChart as HeatmapChartUnresponsive, LeaderboardChartResponsive as LeaderboardChart, LeaderboardChart as LeaderboardChartUnresponsive, Legend, LineChartResponsive as LineChart, LineChart as LineChartUnresponsive, PieChartResponsive as PieChart, PieChart as PieChartUnresponsive, PieSemiCircleChartResponsive as PieSemiCircleChart, PieSemiCircleChart as PieSemiCircleChartUnresponsive, Sparkline, SparklineUnresponsive, TrendIndicator, buildCalendarHeatmapData, defaultTheme, formatMetricValue, formatPercentage, getBucketInfo, getColorDistance, hexToRgba, isValidHexColor, lightenHexColor, mergeThemes, mixHexColors, normalizeColorToHex, parseAsLocalDate, parseHslString, prefersLightText, relativeLuminance, resolveCssVariable, useChartFormatting, useChartLegendItems, useChartRegistration, useChartScopeElement, useGlobalChartsContext, useGlobalChartsTheme, useLeaderboardLegendItems, validateHexColor };
10771
+ export { AccessibleTooltip, AreaChartResponsive as AreaChart, AreaChart as AreaChartUnresponsive, BarChartResponsive as BarChart, BarChart as BarChartUnresponsive, BarListChartResponsive as BarListChart, BarListChart as BarListChartUnresponsive, BaseTooltip, ConversionFunnelChartWithProvider as ConversionFunnelChart, GeoChartResponsive as GeoChart, GeoChartWithProvider as GeoChartUnresponsive, GlobalChartsContext, GlobalChartsProvider, GlobalChartsProvider as ThemeProvider, GoogleDataTableColumnRoleType, HeatmapChartResponsive as HeatmapChart, HeatmapChart as HeatmapChartUnresponsive, LeaderboardChartResponsive as LeaderboardChart, LeaderboardChart as LeaderboardChartUnresponsive, Legend, LineChartResponsive as LineChart, LineChart as LineChartUnresponsive, PieChartResponsive as PieChart, PieChart as PieChartUnresponsive, PieSemiCircleChartResponsive as PieSemiCircleChart, PieSemiCircleChart as PieSemiCircleChartUnresponsive, Sparkline, SparklineUnresponsive, TrendIndicator, buildCalendarHeatmapData, defaultTheme, formatMetricValue, formatPercentage, getBucketInfo, getColorDistance, hexToRgba, isValidHexColor, lightenHexColor, mergeThemes, mixHexColors, normalizeColorToHex, parseAsLocalDate, parseHslString, prefersLightText, relativeLuminance, resolveCssVariable, useCalendarHeatmapData, useChartFormatting, useChartLegendItems, useChartRegistration, useChartScopeElement, useGlobalChartsContext, useGlobalChartsTheme, useLeaderboardLegendItems, validateHexColor };
10424
10772
 
10425
10773
  //# sourceMappingURL=index.js.map