@automattic/charts 3.2.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 (135) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/SECURITY.md +1 -0
  3. package/TOKENS.md +53 -58
  4. package/dist/index.cjs +1483 -664
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.css +93 -29
  7. package/dist/index.d.cts +233 -247
  8. package/dist/index.d.ts +233 -247
  9. package/dist/index.js +1484 -667
  10. package/dist/index.js.map +1 -1
  11. package/package.json +7 -6
  12. package/src/charts/area-chart/area-chart.tsx +39 -25
  13. package/src/charts/area-chart/test/area-chart.test.tsx +4 -33
  14. package/src/charts/area-chart/test/tick-placement.test.tsx +55 -0
  15. package/src/charts/area-chart/test/tooltip-resolution-branches.test.tsx +86 -0
  16. package/src/charts/area-chart/test/tooltip-resolution.test.tsx +69 -0
  17. package/src/charts/area-chart/types.ts +4 -7
  18. package/src/charts/bar-chart/bar-chart.tsx +2 -1
  19. package/src/charts/bar-chart/private/test/use-bar-chart-options-formatting.test.tsx +69 -0
  20. package/src/charts/bar-chart/private/use-bar-chart-options.ts +27 -15
  21. package/src/charts/bar-list-chart/bar-list-chart.tsx +5 -0
  22. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +33 -23
  23. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +138 -182
  24. package/src/charts/conversion-funnel-chart/test/conversion-funnel-chart.test.tsx +46 -0
  25. package/src/charts/geo-chart/geo-chart.module.scss +1 -0
  26. package/src/charts/geo-chart/geo-chart.tsx +5 -10
  27. package/src/charts/geo-chart/test/geo-chart.test.tsx +2 -2
  28. package/src/charts/heatmap-chart/heatmap-chart.module.scss +52 -16
  29. package/src/charts/heatmap-chart/heatmap-chart.tsx +89 -37
  30. package/src/charts/heatmap-chart/index.ts +2 -1
  31. package/src/charts/heatmap-chart/private/build-calendar-data.ts +126 -89
  32. package/src/charts/heatmap-chart/private/civil-day.ts +174 -0
  33. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +11 -4
  34. package/src/charts/heatmap-chart/private/index.ts +1 -1
  35. package/src/charts/heatmap-chart/private/use-heatmap-colors.ts +5 -1
  36. package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +3 -0
  37. package/src/charts/heatmap-chart/test/civil-day.test.ts +70 -0
  38. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +142 -12
  39. package/src/charts/heatmap-chart/test/use-calendar-heatmap-data.test.tsx +118 -0
  40. package/src/charts/heatmap-chart/test/use-heatmap-colors.test.ts +8 -0
  41. package/src/charts/heatmap-chart/test/zoned-calendar-data.test.ts +248 -0
  42. package/src/charts/heatmap-chart/types.ts +8 -2
  43. package/src/charts/heatmap-chart/use-calendar-heatmap-data.ts +42 -0
  44. package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +2 -2
  45. package/src/charts/leaderboard-chart/hooks/use-leaderboard-legend-items.ts +3 -5
  46. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +31 -19
  47. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +27 -17
  48. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +14 -0
  49. package/src/charts/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx +2 -9
  50. package/src/charts/leaderboard-chart/types.ts +2 -2
  51. package/src/charts/line-chart/line-chart.module.scss +10 -18
  52. package/src/charts/line-chart/line-chart.tsx +128 -26
  53. package/src/charts/line-chart/private/line-chart-annotation.tsx +18 -19
  54. package/src/charts/line-chart/test/crosshair-style-types.test.ts +31 -0
  55. package/src/charts/line-chart/test/default-tooltip-formatting.test.tsx +74 -0
  56. package/src/charts/line-chart/test/line-chart.test.tsx +174 -41
  57. package/src/charts/line-chart/test/tick-placement.test.tsx +84 -0
  58. package/src/charts/line-chart/test/tooltip-resolution.test.tsx +140 -0
  59. package/src/charts/line-chart/test/zoned-date-string.test.tsx +37 -0
  60. package/src/charts/line-chart/types.ts +17 -3
  61. package/src/charts/pie-chart/pie-chart.module.scss +13 -1
  62. package/src/charts/pie-chart/pie-chart.tsx +30 -40
  63. package/src/charts/pie-chart/test/pie-chart.test.tsx +59 -12
  64. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +8 -3
  65. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +23 -37
  66. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +50 -0
  67. package/src/charts/private/chart-layout/chart-layout.module.scss +3 -2
  68. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
  69. package/src/charts/private/test/axis-formatting.test.tsx +85 -0
  70. package/src/charts/private/test/time-axis-formatting.test.ts +109 -0
  71. package/src/charts/private/test/time-axis-options.test.ts +267 -0
  72. package/src/charts/private/test/time-axis-tick-invariants.test.ts +153 -0
  73. package/src/charts/private/test/time-axis-tick-values.test.ts +280 -0
  74. package/src/charts/private/test/time-axis.test.ts +9 -69
  75. package/src/charts/private/time-axis-options.ts +106 -0
  76. package/src/charts/private/time-axis.ts +318 -136
  77. package/src/charts/private/x-zoom/x-zoom.module.scss +3 -3
  78. package/src/charts/private/xy-plot/xy-plot.module.scss +8 -0
  79. package/src/charts/sparkline/test/sparkline.test.tsx +29 -56
  80. package/src/charts/sparkline/types.ts +1 -1
  81. package/src/components/legend/private/base-legend.module.scss +9 -5
  82. package/src/components/tooltip/accessible-tooltip.tsx +21 -17
  83. package/src/components/tooltip/base-tooltip.module.scss +3 -3
  84. package/src/components/tooltip/private/bounded-tooltip.tsx +227 -0
  85. package/src/components/tooltip/test/accessible-tooltip.test.tsx +30 -4
  86. package/src/components/tooltip/test/bounded-tooltip.test.tsx +225 -0
  87. package/src/components/tooltip/test/xy-chart-tooltip.test.tsx +379 -0
  88. package/src/components/tooltip/xy-chart-tooltip.tsx +326 -0
  89. package/src/components/trend-indicator/trend-indicator.module.scss +2 -2
  90. package/src/hooks/test/use-chart-data-transform-zone.test.tsx +36 -0
  91. package/src/hooks/test/use-chart-margin.test.tsx +173 -3
  92. package/src/hooks/test/use-xychart-theme.test.tsx +16 -3
  93. package/src/hooks/use-chart-data-transform.ts +10 -3
  94. package/src/hooks/use-chart-margin.tsx +54 -9
  95. package/src/hooks/use-xychart-theme.ts +25 -9
  96. package/src/index.ts +8 -0
  97. package/src/providers/chart-context/global-charts-provider.tsx +37 -38
  98. package/src/providers/chart-context/hooks/use-chart-formatting.ts +22 -0
  99. package/src/providers/chart-context/index.ts +1 -0
  100. package/src/providers/chart-context/private/catalog-pointers.ts +42 -0
  101. package/src/providers/chart-context/private/series-palette.ts +3 -13
  102. package/src/providers/chart-context/test/{sparse-theme.test.ts → catalog-pointers.test.ts} +22 -10
  103. package/src/providers/chart-context/test/chart-context.test.tsx +177 -792
  104. package/src/providers/chart-context/test/use-global-charts-theme.test.tsx +7 -25
  105. package/src/providers/chart-context/themes.ts +7 -52
  106. package/src/providers/chart-context/types.ts +3 -0
  107. package/src/styles/chart-scope.scss +39 -62
  108. package/src/styles/test/chart-scope.test.ts +19 -47
  109. package/src/test-utils/series-fixtures.ts +31 -0
  110. package/src/test-utils/zoned-parsing-suite.ts +90 -0
  111. package/src/types.ts +79 -166
  112. package/src/utils/bucket-info.ts +87 -0
  113. package/src/utils/color-utils.ts +0 -27
  114. package/src/utils/date-formatting.ts +157 -0
  115. package/src/utils/date-parsing.ts +103 -52
  116. package/src/utils/get-edge-tick-widths.ts +33 -0
  117. package/src/utils/index.ts +4 -0
  118. package/src/utils/test/bucket-info.test.ts +185 -0
  119. package/src/utils/test/color-utils.test.ts +1 -48
  120. package/src/utils/test/date-formatting.test.ts +89 -0
  121. package/src/utils/test/date-parsing.auckland.test.ts +6 -0
  122. package/src/utils/test/date-parsing.santiago.test.ts +6 -0
  123. package/src/utils/test/date-parsing.test.ts +16 -1
  124. package/src/utils/test/get-edge-tick-widths.test.ts +55 -0
  125. package/src/utils/test/get-styles.test.ts +10 -7
  126. package/src/utils/test/merge-themes.test.ts +18 -46
  127. package/src/utils/warn-once.ts +23 -0
  128. package/src/visx/types.ts +56 -5
  129. package/src/hooks/test/use-xychart-theme-provider-override.test.tsx +0 -28
  130. package/src/providers/chart-context/private/theme-override-vars.ts +0 -131
  131. package/src/providers/chart-context/private/with-catalog-pointers.ts +0 -93
  132. package/src/providers/chart-context/test/theme-override-vars-guard.test.tsx +0 -29
  133. package/src/providers/chart-context/test/theme-override-vars.test.ts +0 -183
  134. package/src/providers/chart-context/test/with-catalog-pointers.test.ts +0 -104
  135. package/src/styles/test/wpds-fallbacks.test.ts +0 -131
package/dist/index.js CHANGED
@@ -1,20 +1,22 @@
1
1
  import { getStringWidth as getStringWidth$1 } from "./visx/text/index.js";
2
2
  import { formatNumber, formatNumberCompact } from "@automattic/number-formatters";
3
- import { AnimatedAreaSeries, AnimatedAreaStack, AreaSeries, Axis, BarGroup, BarSeries, DataContext, Grid, Tooltip, TooltipContext, XYChart, buildChartTheme } from "@visx/xychart";
3
+ import { AnimatedAreaSeries, AnimatedAreaStack, AreaSeries, Axis, BarGroup, BarSeries, DataContext, Grid, TooltipContext, XYChart, buildChartTheme } from "@visx/xychart";
4
4
  import { __, _x, sprintf } from "@wordpress/i18n";
5
5
  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";
13
- import { useTooltip, useTooltipInPortal } from "@visx/tooltip";
14
+ import { Tooltip, defaultStyles, useTooltip } from "@visx/tooltip";
14
15
  import { createScale, getTicks, scaleBand, scaleOrdinal, scaleTime } from "@visx/scale";
15
16
  import { Group } from "@visx/group";
16
17
  import { LegendItem, LegendLabel, LegendOrdinal, LegendShape } from "@visx/legend";
17
18
  import { IconButton, Stack, Text as Text$1 } from "@wordpress/ui";
19
+ import { createPortal } from "react-dom";
18
20
  import { LinearGradient } from "@visx/gradient";
19
21
  import { curveCatmullRom, curveLinear, curveMonotoneX } from "@visx/curve";
20
22
  import { useParentSize } from "@visx/responsive";
@@ -83,46 +85,35 @@ function attachSubComponents(Chart, subComponents) {
83
85
  return Object.assign(Chart, subComponents);
84
86
  }
85
87
  //#endregion
86
- //#region src/utils/date-parsing.ts
88
+ //#region src/utils/warn-once.ts
89
+ const warned = /* @__PURE__ */ new Set();
87
90
  /**
88
- * @file Date parsing utilities using date-fns for local timezone handling
89
- *
90
- * This module provides utilities for parsing various date string formats and converting
91
- * them to local timezone dates using the battle-tested date-fns library. For formats
92
- * without timezone info, they're treated as local. For formats with timezone info,
93
- * they're converted to the equivalent local time.
91
+ * Warns about a host-supplied value once per process, and never in production.
94
92
  *
95
- * Note: And specifically it prevents format `YYYY-MM-DD` being parsed as UTC date.
96
- *
97
- * Key Features:
98
- * - All parsed dates are in local timezone
99
- * - Converts timezone-aware strings to local equivalent
100
- * - Robust input validation and error handling using date-fns
101
- * - TypeScript type safety
102
- * - 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
103
105
  *
104
- * Supported Formats:
105
- * - YYYY-MM-DD (treated as local)
106
- * - YYYY-MM-DD HH:mm:ss (treated as local)
107
- * - YYYY-MM-DD HH:mm (treated as local)
108
- * - YYYY-MM-DDTHH:mm:ss (treated as local)
109
- * - YYYY-MM-DDTHH:mm:ss.SSS (treated as local)
110
- * - YYYY-MM-DDTHH:mm (treated as local)
111
- * - YYYY-MM-DDTHH:mm:ssZ (converted to local)
112
- * - 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.
113
109
  *
114
- * @example
115
- * ```typescript
116
- * parseAsLocalDate("2025-01-01"); // Local timezone
117
- * parseAsLocalDate("2025-01-01 14:30:00"); // Local timezone
118
- * parseAsLocalDate("2025-01-01 14:30"); // Local timezone
119
- * parseAsLocalDate("2025-01-01T14:30:45.123"); // Local timezone
120
- * parseAsLocalDate("2025-01-01T14:30:00Z"); // UTC 14:30 → Local equivalent
121
- * parseAsLocalDate("2025-01-01T14:30:00+05:00"); // +05:00 14:30 → Local equivalent
122
- * ```
110
+ * Note: this specifically avoids date-fns's default of parsing `YYYY-MM-DD` as a UTC date.
123
111
  */
124
112
  /**
125
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
+ *
126
117
  * @param {string} dateString - The date string to check for timezone information
127
118
  * @return {boolean} True if the date string contains timezone information, false otherwise
128
119
  */
@@ -130,28 +121,57 @@ const hasTimezone = (dateString) => {
130
121
  const tIndex = dateString.indexOf("T");
131
122
  if (tIndex === -1) return false;
132
123
  if (dateString.endsWith("Z")) return true;
133
- 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();
134
133
  };
135
134
  /**
136
- * Parses any supported date string format and returns a local timezone date
135
+ * The instant a wall-clock reading names in a given zone.
137
136
  *
138
- * Uses date-fns for robust parsing and validation. For strings without timezone
139
- * info, treats as local timezone. For strings with timezone info, converts to
140
- * 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.
141
139
  *
142
- * Supports:
143
- * - YYYY-MM-DD (local)
144
- * - YYYY-MM-DD HH:mm:ss (local)
145
- * - YYYY-MM-DD HH:mm (local)
146
- * - YYYY-MM-DDTHH:mm:ss (local)
147
- * - YYYY-MM-DDTHH:mm:ss.SSS (local)
148
- * - YYYY-MM-DDTHH:mm (local)
149
- * - YYYY-MM-DDTHH:mm:ssZ (UTC → local)
150
- * - YYYY-MM-DDTHH:mm:ss±HH:mm (offset → local)
151
- * @param {string} dateString - The date string to parse into a local timezone date
152
- * @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.
153
143
  */
154
- 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) => {
155
175
  const trimmedString = dateString.trim();
156
176
  if (hasTimezone(trimmedString)) {
157
177
  const isoDate = parseISO(trimmedString);
@@ -167,11 +187,47 @@ const parseAsLocalDate = (dateString) => {
167
187
  "yyyy-MM-dd'T'HH:mm"
168
188
  ]) {
169
189
  const result = parse(trimmedString, format, /* @__PURE__ */ new Date());
170
- 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;
171
194
  }
172
195
  return /* @__PURE__ */ new Date(NaN);
173
196
  };
174
197
  //#endregion
198
+ //#region src/utils/bucket-info.ts
199
+ const getInstant = (point) => {
200
+ if (!point) return;
201
+ if ("date" in point && point.date) return point.date.getTime();
202
+ if ("dateString" in point && point.dateString) return parseAsLocalDate(point.dateString).getTime();
203
+ };
204
+ const getPointSpacingInHours = (data) => {
205
+ return data.reduce((spacing, datom) => {
206
+ const instants = [...new Set(datom.data.map((point) => getInstant(point)))].filter((time) => time !== void 0 && Number.isFinite(time)).sort((a, b) => a - b);
207
+ return instants.reduce((seriesSpacing, time, index) => index === 0 ? seriesSpacing : Math.min(seriesSpacing, Math.abs(differenceInHours(time, instants[index - 1]))), spacing);
208
+ }, Number.POSITIVE_INFINITY);
209
+ };
210
+ const SUB_DAILY_SPACING_HOURS = 23;
211
+ const MONTHLY_SPACING_HOURS = 672;
212
+ const getBucketResolution = (data, tickResolution) => {
213
+ if (tickResolution) return tickResolution === "week" ? "day" : tickResolution;
214
+ const spacingInHours = getPointSpacingInHours(data);
215
+ if (spacingInHours < SUB_DAILY_SPACING_HOURS) return "hour";
216
+ if (!Number.isFinite(spacingInHours) || spacingInHours < MONTHLY_SPACING_HOURS) return "day";
217
+ return spacingInHours < 12 * MONTHLY_SPACING_HOURS ? "month" : "year";
218
+ };
219
+ /**
220
+ * How this data was classified, for consumers that render their own labels.
221
+ *
222
+ * @param data - The series the chart draws, in any order, dated by `date` or `dateString`.
223
+ * @param tickResolution - Caller-declared bucket resolution, when known.
224
+ * @return The declared or inferred bucket, and the resolution formats key on.
225
+ */
226
+ const getBucketInfo = (data, tickResolution) => ({
227
+ bucket: tickResolution ?? getBucketResolution(data),
228
+ displayResolution: getBucketResolution(data, tickResolution)
229
+ });
230
+ //#endregion
175
231
  //#region src/utils/format-metric-value.ts
176
232
  /**
177
233
  * Format a numeric metric value based on type, precision and scale.
@@ -239,6 +295,31 @@ const formatPercentage = (value) => {
239
295
  } });
240
296
  };
241
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
242
323
  //#region src/utils/get-longest-tick-width.ts
243
324
  /**
244
325
  * Returns the width of the longest tick.
@@ -384,20 +465,6 @@ const parseHslString = (hslString) => {
384
465
  ];
385
466
  };
386
467
  /**
387
- * Parse an RGB string like 'rgb(255, 0, 0)' into a hex color.
388
- *
389
- * @deprecated Use normalizeColorToHex() instead, which handles all color formats including rgb() and rgba().
390
- * @param rgbString - RGB color string (not RGBA)
391
- * @return hex color string or null if invalid
392
- */
393
- const parseRgbString = (rgbString) => {
394
- const lower = rgbString.toLowerCase().trim();
395
- if (!lower.startsWith("rgb(") || lower.startsWith("rgba(")) return null;
396
- const parsed = color(lower);
397
- if (!parsed) return null;
398
- return parsed.formatHex();
399
- };
400
- /**
401
468
  * Normalize any CSS color value to a hex color string.
402
469
  * Handles hex, HSL, HSLA, RGB, RGBA, named CSS colors, and CSS variables.
403
470
  *
@@ -625,6 +692,106 @@ const resolveFontSize = (val) => {
625
692
  }
626
693
  };
627
694
  //#endregion
695
+ //#region src/utils/date-formatting.ts
696
+ const formatters = /* @__PURE__ */ new Map();
697
+ const getFormatter$1 = (options, { locale, timeZone }) => {
698
+ const key = `${locale ?? ""}|${timeZone ?? ""}|${JSON.stringify(options)}`;
699
+ let formatter = formatters.get(key);
700
+ if (!formatter) {
701
+ formatter = new Intl.DateTimeFormat(locale, {
702
+ ...options,
703
+ timeZone
704
+ });
705
+ formatters.set(key, formatter);
706
+ }
707
+ return formatter;
708
+ };
709
+ const isUsable = (formatting) => {
710
+ try {
711
+ Intl.DateTimeFormat(formatting.locale, { timeZone: formatting.timeZone });
712
+ return true;
713
+ } catch {
714
+ return false;
715
+ }
716
+ };
717
+ const toBcp47 = (locale) => locale?.replace(/_/g, "-");
718
+ /**
719
+ * Repairs a host locale for `Intl`, dropping either half it still cannot use.
720
+ *
721
+ * Every formatter below is built during render, so an unusable string would throw
722
+ * where React unmounts the whole host tree rather than one chart. WordPress hands
723
+ * out the likeliest one either way: `timezone_string` is `''` on a site set to a
724
+ * raw UTC offset, and has no repair.
725
+ *
726
+ * @param formatting - Locale and time zone as the host supplied them.
727
+ * @return The usable halves, each `undefined` where the host's value was not.
728
+ */
729
+ const sanitizeFormatting = (formatting) => {
730
+ const repaired = {
731
+ ...formatting,
732
+ locale: toBcp47(formatting.locale)
733
+ };
734
+ if (isUsable(repaired)) return repaired;
735
+ const locale = isUsable({ locale: repaired.locale }) ? repaired.locale : void 0;
736
+ const timeZone = isUsable({ timeZone: repaired.timeZone }) ? repaired.timeZone : void 0;
737
+ if (locale === void 0 && formatting.locale !== void 0) warnOnce(`locale:${formatting.locale}`, `locale ${JSON.stringify(formatting.locale)} is not a BCP-47 tag Intl accepts, so dates render in the browser's locale.`);
738
+ if (timeZone === void 0 && formatting.timeZone !== void 0) warnOnce(`timeZone:${formatting.timeZone}`, `timeZone ${JSON.stringify(formatting.timeZone)} is not a zone Intl accepts, so dates render in the browser's zone. Pass an IANA name or a UTC offset such as "+05:30".`);
739
+ return {
740
+ locale,
741
+ timeZone
742
+ };
743
+ };
744
+ /**
745
+ * A date formatter bound to the host's locale and time zone.
746
+ *
747
+ * @param options - Date-part options, as for `Intl.DateTimeFormat`.
748
+ * @param formatting - Host locale and time zone; either half absent leaves the runtime's own.
749
+ * @return Formatter taking a timestamp or a `Date`.
750
+ */
751
+ const createDateFormatter = (options, formatting) => {
752
+ return (value) => {
753
+ if (!Number.isFinite(Number(value))) return new Date(Number(value)).toLocaleDateString();
754
+ return getFormatter$1(options, formatting).format(value);
755
+ };
756
+ };
757
+ const CLOCK_OPTIONS = {
758
+ month: "numeric",
759
+ hour: "numeric",
760
+ hourCycle: "h23"
761
+ };
762
+ const NO_FIELDS = {
763
+ month: NaN,
764
+ hour: NaN
765
+ };
766
+ /**
767
+ * A clock reading calendar fields in the host's time zone.
768
+ *
769
+ * `Date`'s own getters read the browser's zone, so a chart viewed from another
770
+ * country anchors its ticks to the wrong midnights and the wrong January
771
+ * without this.
772
+ *
773
+ * @param timeZone - IANA zone name; absent leaves the runtime's own.
774
+ * @return Reader taking a `Date`.
775
+ */
776
+ const createZonedClock = (timeZone) => {
777
+ if (!timeZone) return (date) => ({
778
+ month: date.getMonth() + 1,
779
+ hour: date.getHours()
780
+ });
781
+ return (date) => {
782
+ if (!Number.isFinite(Number(date))) return NO_FIELDS;
783
+ const parts = getFormatter$1(CLOCK_OPTIONS, {
784
+ locale: "en-US",
785
+ timeZone
786
+ }).formatToParts(date);
787
+ const read = (type) => Number(parts.find((part) => part.type === type)?.value);
788
+ return {
789
+ month: read("month"),
790
+ hour: read("hour")
791
+ };
792
+ };
793
+ };
794
+ //#endregion
628
795
  //#region src/providers/chart-scope/chart-scope-context.ts
629
796
  /**
630
797
  * The nearest chart element, published so JS token resolution reads the same CSS cascade that paints the chart's own colours.
@@ -772,150 +939,29 @@ const SERIES_SLOT_1_FALLBACK = "#3858e9";
772
939
  /** The catalog pointer for every slot, in slot order. */
773
940
  const SERIES_PALETTE_POINTERS = Array.from({ length: 5 }, (_, index) => index === 0 ? `var(${seriesRole(1)}, ${SERIES_SLOT_1_FALLBACK})` : `var(${seriesRole(index + 1)})`);
774
941
  //#endregion
775
- //#region src/providers/chart-context/private/theme-override-vars.ts
776
- const ROLE_FOR_FIELD = [
777
- ["--a8c-charts-color-background", (theme) => theme.backgroundColor],
778
- ["--a8c-charts-color-grid", (theme) => theme.gridStyles?.stroke],
779
- ["--a8c-charts-color-axis", (theme) => theme.xAxisLineStyles?.stroke],
780
- ["--a8c-charts-color-tick", (theme) => theme.xTickLineStyles?.stroke],
781
- ["--a8c-charts-color-label-axis", (theme) => theme.svgLabelSmall?.fill],
782
- ["--a8c-charts-color-label-background", (theme) => theme.labelBackgroundColor],
783
- ["--a8c-charts-color-label-on-fill", (theme) => theme.labelTextColor],
784
- ...Array.from({ length: 5 }, (_, index) => [seriesRole(index + 1), (theme) => theme.colors?.[index]])
785
- ];
786
- let warnedAboutExtraColors = false;
787
- /**
788
- * Warns once when a consumer passes more colors than there are slots to publish them into.
789
- *
790
- * @param count - How many entries `theme.colors` holds.
791
- */
792
- function warnOnceAboutExtraColors(count) {
793
- if (warnedAboutExtraColors || process.env.NODE_ENV === "production") return;
794
- warnedAboutExtraColors = true;
795
- console.warn(`[Charts] theme.colors holds ${count} colors and the palette has 5 slots, so entries past the 5th are ignored. Set a per-series color with \`options.stroke\` instead.`);
796
- }
797
- /**
798
- * The variable a `theme` prop override is published as, one layer outside the role itself.
799
- *
800
- * `chart-scope.scss` declares each of these roles as `var(<role>-theme, <catalog default>)`, so an override that is invalid at computed-value time — a fallback-less `var()` naming a token the host never set — only invalidates this variable, and the role still resolves its mapped `--wpds-*` token. Published as the role directly, such a value took the role and every bare `var(<role>)` read site down with it.
801
- *
802
- * @param role - The catalog role being overridden.
803
- * @return The custom property carrying the consumer's value.
804
- */
805
- const themeLayerVar = (role) => `${role}-theme`;
806
- ROLE_FOR_FIELD.map(([role]) => role);
807
- /**
808
- * Whether a value reads the role it is being published for, which would make the catalog entry depend on itself.
809
- *
810
- * The dependency runs `<role>: var(<role>-theme, …)`, so a value naming `<role>` closes a cycle through the catalog entry. CSS marks every custom property in a cycle invalid at computed-value time — the role's own fallback is *not* used, so the token resolves to nothing and every chart loses that color. Verified in Chrome: `--role-theme: var(--role, blue); --role: var(--role-theme, green)` leaves `--role` empty, not `green`.
811
- *
812
- * Three things this has to get right, none of which a plain substring test gets:
813
- *
814
- * `var()` permits whitespace after the opening paren, so testing for `var(<role>` misses `var( --a8c-charts-color-grid, … )`.
815
- *
816
- * Several roles are prefixes of others (`--a8c-charts-color-label` of `--a8c-charts-color-label-secondary`), and every role is a prefix of its own theme layer. A prefix match would read a legitimate cross-role pointer as a self-reference and silently drop the override, so the role must be followed by something that cannot continue an identifier — `-` included. A value naming `<role>-theme` is therefore not treated as a self-reference: that cycle is confined to the theme layer, which leaves the role free to fall back to the catalog default.
817
- *
818
- * `resolveCssVariable` also accepts a bare custom-property name with no `var()` wrapper (`--a8c-charts-color-grid`), so that is legal `theme` input too. It forms no cycle — custom properties accept arbitrary token streams — so it survives as a literal string and drops silently at the use site instead of falling back to the catalog default.
819
- *
820
- * @param value - The consumer's value for the field.
821
- * @param role - The catalog role this value would override.
822
- * @return True when the value reads `role`, so it must not be published.
823
- */
824
- const readsOwnRole = (value, role) => value.trim() === role || new RegExp(`var\\(\\s*${role.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`).test(value);
825
- /**
826
- * Maps a sparse consumer theme onto the theme-layer variables its overridden catalog roles read, so CSS-painted and JS-resolved colors read one source.
827
- *
828
- * A value that reads the role it would override is left unpublished — that is the default theme's own pointer surviving `mergeThemes`, and publishing it would invalidate the role. The role is still reported in `roles`: `withCatalogPointers` restores its theme field to the catalog pointer either way, so CSS and visx agree on the catalog default rather than visx painting a literal CSS never sees.
829
- *
830
- * `theme.colors` publishes through the same mechanism, one slot per entry, so a CSS declaration of `--a8c-charts-color-series-N` still beats it and a short array leaves the later slots unset rather than blank.
831
- *
832
- * @param theme - The consumer's `theme` prop, before merging with the default theme.
833
- * @return The wrapper's custom properties, and the roles the consumer overrode.
834
- */
835
- const themeOverrideVars = (theme) => {
836
- if (!theme) return {
837
- vars: {},
838
- roles: []
839
- };
840
- if (Array.isArray(theme.colors) && theme.colors.length > 5) warnOnceAboutExtraColors(theme.colors.length);
841
- const vars = {};
842
- const roles = [];
843
- for (const [role, read] of ROLE_FOR_FIELD) {
844
- const value = read(theme);
845
- if (typeof value !== "string" || value === "") continue;
846
- roles.push(role);
847
- if (!readsOwnRole(value, role)) vars[themeLayerVar(role)] = value;
848
- }
849
- return {
850
- vars,
851
- roles
852
- };
853
- };
854
- //#endregion
855
942
  //#region src/providers/chart-context/themes.ts
856
943
  /**
857
- * Default theme configuration
944
+ * Default theme configuration: the shape and spacing a consumer can override.
945
+ *
946
+ * For a color, set the matching `--a8c-charts-color-*` role in CSS instead; the `var()`
947
+ * chains that deliver those live in `private/catalog-pointers.ts`.
858
948
  */
859
949
  const defaultTheme = {
860
- backgroundColor: "var(--a8c-charts-color-background, #fff)",
861
- labelBackgroundColor: "var(--a8c-charts-color-label-background, transparent)",
862
- labelTextColor: "var(--a8c-charts-color-label-on-fill, #FFFFFF)",
863
- colors: [...SERIES_PALETTE_POINTERS],
864
- gridStyles: {
865
- stroke: "var(--a8c-charts-color-grid, #dbdbdb)",
866
- strokeWidth: 1
867
- },
950
+ gridStyles: { strokeWidth: 1 },
868
951
  tickLength: 4,
869
- gridColor: "",
870
- gridColorDark: "",
871
- xTickLineStyles: {
872
- stroke: "var(--a8c-charts-color-tick, #dbdbdb)",
873
- strokeWidth: 1
874
- },
875
- xAxisLineStyles: {
876
- stroke: "var(--a8c-charts-color-axis, #dbdbdb)",
877
- strokeWidth: 1
878
- },
952
+ xTickLineStyles: { strokeWidth: 1 },
953
+ xAxisLineStyles: { strokeWidth: 1 },
879
954
  legend: {
880
- labelStyles: { color: "var(--a8c-charts-color-label, #1e1e1e)" },
955
+ labelStyles: {},
881
956
  containerStyles: {},
882
957
  shapeStyles: []
883
958
  },
884
959
  seriesLineStyles: [],
885
960
  glyphs: [],
886
- svgLabelSmall: {
887
- fill: "var(--a8c-charts-color-label-axis, #1e1e1e)",
888
- fontFamily: "inherit"
889
- },
961
+ svgLabelSmall: { fontFamily: "inherit" },
890
962
  svgLabelBig: { fontFamily: "inherit" },
891
- annotationStyles: {
892
- label: {
893
- anchorLineStroke: "var(--a8c-charts-color-annotation, #1e1e1e)",
894
- backgroundFill: "var(--a8c-charts-color-surface, #fff)"
895
- },
896
- connector: { stroke: "var(--a8c-charts-color-annotation, #1e1e1e)" },
897
- circleSubject: {
898
- stroke: "transparent",
899
- fill: "var(--a8c-charts-color-annotation, #1e1e1e)",
900
- radius: 5
901
- }
902
- },
903
- geoChart: { featureFillColor: "var(--a8c-charts-color-surface-secondary, #f4f4f4)" },
904
- leaderboardChart: {
905
- rowGap: 12,
906
- columnGap: 4,
907
- labelSpacing: "xs",
908
- deltaColors: [
909
- "var(--a8c-charts-color-trend-down, #cc1818)",
910
- "var(--a8c-charts-color-trend-neutral, #707070)",
911
- "var(--a8c-charts-color-trend-up, #008030)"
912
- ]
913
- },
914
- conversionFunnelChart: {
915
- backgroundColor: "var(--a8c-charts-color-surface-secondary, #f4f4f4)",
916
- positiveChangeColor: "var(--a8c-charts-color-trend-up, #008030)",
917
- negativeChangeColor: "var(--a8c-charts-color-trend-down, #cc1818)"
918
- },
963
+ annotationStyles: {},
964
+ leaderboardChart: { labelSpacing: "xs" },
919
965
  lineChart: { lineStyles: { comparison: {
920
966
  strokeDasharray: "4 4",
921
967
  strokeLinecap: "square"
@@ -939,70 +985,9 @@ const defaultTheme = {
939
985
  }
940
986
  };
941
987
  //#endregion
942
- //#region src/providers/chart-context/private/with-catalog-pointers.ts
943
- /**
944
- * The pointer one palette slot is restored to.
945
- *
946
- * Unlike the other roles, this keeps the consumer's own color as the pointer's terminal literal instead of the catalog's. The literal is only ever reached where `getComputedStyle` resolves nothing — SSR and jsdom — and there the palette is the one field where falling back to the catalog default would be visible: every series would paint the same seeded blue. In a browser the slot resolves first, so a CSS declaration of it still outranks the consumer's `theme.colors`.
947
- *
948
- * @param index - The slot's zero-based index in `colors`.
949
- * @param value - The merged theme's entry for it.
950
- * @return The catalog pointer for that slot.
951
- */
952
- const seriesPointer = (index, value) => {
953
- const role = seriesRole(index + 1);
954
- if (typeof value === "string" && value.startsWith(`var(${role}`)) return value;
955
- if (typeof value !== "string" || value === "") return SERIES_PALETTE_POINTERS[index];
956
- return `var(${role}, ${value})`;
957
- };
958
- const CATALOG_RESTORE_FOR_ROLE = {
959
- ...Object.fromEntries(Array.from({ length: 5 }, (_, index) => [seriesRole(index + 1), (theme) => ({ colors: SERIES_PALETTE_POINTERS.map((_pointer, slot) => seriesPointer(slot, theme.colors?.[slot])) })])),
960
- "--a8c-charts-color-background": () => ({ backgroundColor: defaultTheme.backgroundColor }),
961
- "--a8c-charts-color-grid": (theme) => ({ gridStyles: {
962
- ...theme.gridStyles,
963
- stroke: defaultTheme.gridStyles.stroke
964
- } }),
965
- "--a8c-charts-color-axis": (theme) => ({ xAxisLineStyles: {
966
- ...theme.xAxisLineStyles,
967
- stroke: defaultTheme.xAxisLineStyles.stroke
968
- } }),
969
- "--a8c-charts-color-tick": (theme) => ({ xTickLineStyles: {
970
- ...theme.xTickLineStyles,
971
- stroke: defaultTheme.xTickLineStyles.stroke
972
- } }),
973
- "--a8c-charts-color-label-axis": (theme) => ({ svgLabelSmall: {
974
- ...theme.svgLabelSmall,
975
- fill: defaultTheme.svgLabelSmall.fill
976
- } }),
977
- "--a8c-charts-color-label-background": () => ({ labelBackgroundColor: defaultTheme.labelBackgroundColor }),
978
- "--a8c-charts-color-label-on-fill": () => ({ labelTextColor: defaultTheme.labelTextColor })
979
- };
980
- /**
981
- * Restores the mapped theme fields of `overriddenRoles` to their catalog pointer, so the theme-layer variable `themeOverrideVars` writes on the provider wrapper is the only carrier for an overridden role — CSS and the JS bridge (`useXYChartTheme`) then resolve it through the same cascade instead of visx reading a baked literal that can disagree with a closer CSS override.
982
- *
983
- * This covers roles `themeOverrideVars` deliberately left unpublished as well as the ones it published. A value that reads its own role is not publishable, but leaving visx the consumer's literal is what the whole mechanism exists to prevent: `theme={ { gridStyles: { stroke: 'var(--brand, var(--a8c-charts-color-grid, red))' } } }` would have CSS paint the catalog default while visx painted `--brand`.
984
- *
985
- * Every field outside the mapped roles is left exactly as `merged` provided it: those aren't published as theme-layer vars, so rewriting them would erase a consumer's override with no replacement carrier.
986
- *
987
- * @param merged - The consumer theme merged onto `defaultTheme` (`mergeThemes` output).
988
- * @param overriddenRoles - The catalog roles the consumer overrode, from `themeOverrideVars`.
989
- * @return A new theme object; `merged` is not mutated.
990
- */
991
- const withCatalogPointers = (merged, overriddenRoles) => {
992
- let result = merged;
993
- for (const role of overriddenRoles) {
994
- const restore = CATALOG_RESTORE_FOR_ROLE[role];
995
- if (restore) result = {
996
- ...result,
997
- ...restore(result)
998
- };
999
- }
1000
- return result;
1001
- };
1002
- //#endregion
1003
988
  //#region src/providers/chart-context/global-charts-provider.tsx
1004
989
  const GlobalChartsContext = createContext(null);
1005
- const GlobalChartsProvider = ({ children, theme }) => {
990
+ const GlobalChartsProvider = ({ children, theme, locale, timeZone }) => {
1006
991
  const [charts, setCharts] = useState(() => /* @__PURE__ */ new Map());
1007
992
  const [hiddenSeries, setHiddenSeries] = useState(() => /* @__PURE__ */ new Map());
1008
993
  const seededCharts = useRef(/* @__PURE__ */ new Set());
@@ -1012,11 +997,7 @@ const GlobalChartsProvider = ({ children, theme }) => {
1012
997
  wrapperRef.current = node;
1013
998
  setScopeNode(node);
1014
999
  }, []);
1015
- const { vars: overrideVars, roles: overriddenRoles } = useMemo(() => themeOverrideVars(theme), [theme]);
1016
- const providerTheme = useMemo(() => {
1017
- if (!theme) return defaultTheme;
1018
- return withCatalogPointers(mergeThemes(defaultTheme, theme), overriddenRoles);
1019
- }, [theme, overriddenRoles]);
1000
+ const providerTheme = useMemo(() => theme ? mergeThemes(defaultTheme, theme) : defaultTheme, [theme]);
1020
1001
  const [colorCache, setColorCache] = useState(() => ({
1021
1002
  colors: [],
1022
1003
  hues: [],
@@ -1032,7 +1013,7 @@ const GlobalChartsProvider = ({ children, theme }) => {
1032
1013
  const existingHslColors = [];
1033
1014
  let minHue = 360;
1034
1015
  let maxHue = 0;
1035
- for (const color of providerTheme.colors ?? SERIES_PALETTE_POINTERS) {
1016
+ for (const color of SERIES_PALETTE_POINTERS) {
1036
1017
  const normalizedColor = normalizeColorToHex(color, wrapperRef.current, resolveCssVariable);
1037
1018
  if (normalizedColor.startsWith("#")) {
1038
1019
  resolvedColors.push(normalizedColor);
@@ -1057,7 +1038,7 @@ const GlobalChartsProvider = ({ children, theme }) => {
1057
1038
  minHue,
1058
1039
  maxHue
1059
1040
  });
1060
- }, [providerTheme]);
1041
+ }, []);
1061
1042
  useEffect(() => {
1062
1043
  if (colorCache.colors.length > 0) setIsColorPaletteResolved(true);
1063
1044
  }, [colorCache]);
@@ -1149,12 +1130,17 @@ const GlobalChartsProvider = ({ children, theme }) => {
1149
1130
  const set = hiddenSeries.get(chartId);
1150
1131
  return set ? new Set(set) : /* @__PURE__ */ new Set();
1151
1132
  }, [hiddenSeries]);
1133
+ const formatting = useMemo(() => sanitizeFormatting({
1134
+ locale,
1135
+ timeZone
1136
+ }), [locale, timeZone]);
1152
1137
  const value = useMemo(() => ({
1153
1138
  charts,
1154
1139
  registerChart,
1155
1140
  unregisterChart,
1156
1141
  getChartData,
1157
1142
  theme: providerTheme,
1143
+ formatting,
1158
1144
  getElementStyles,
1159
1145
  toggleSeriesVisibility,
1160
1146
  setSeriesVisibility,
@@ -1170,6 +1156,7 @@ const GlobalChartsProvider = ({ children, theme }) => {
1170
1156
  unregisterChart,
1171
1157
  getChartData,
1172
1158
  providerTheme,
1159
+ formatting,
1173
1160
  getElementStyles,
1174
1161
  toggleSeriesVisibility,
1175
1162
  setSeriesVisibility,
@@ -1185,10 +1172,7 @@ const GlobalChartsProvider = ({ children, theme }) => {
1185
1172
  children: /* @__PURE__ */ jsx("div", {
1186
1173
  ref: setWrapperNode,
1187
1174
  className: CHART_SCOPE_CLASS,
1188
- style: {
1189
- display: "contents",
1190
- ...overrideVars
1191
- },
1175
+ style: { display: "contents" },
1192
1176
  children: /* @__PURE__ */ jsx(ChartScopeContext.Provider, {
1193
1177
  value: scopeNode,
1194
1178
  children
@@ -1252,6 +1236,39 @@ const useDeepMemo = (value) => {
1252
1236
  return ref.current;
1253
1237
  };
1254
1238
  //#endregion
1239
+ //#region src/providers/chart-context/private/catalog-pointers.ts
1240
+ const CATALOG_POINTERS = {
1241
+ background: "var(--a8c-charts-color-background, #fff)",
1242
+ labelAxis: "var(--a8c-charts-color-label-axis, #1e1e1e)",
1243
+ grid: "var(--a8c-charts-color-grid, #dbdbdb)",
1244
+ axisX: "var(--a8c-charts-color-axis-x, #dbdbdb)",
1245
+ tickX: "var(--a8c-charts-color-tick-x, #dbdbdb)",
1246
+ axisY: "var(--a8c-charts-color-axis-y, none)",
1247
+ tickY: "var(--a8c-charts-color-tick-y, none)",
1248
+ annotation: "var(--a8c-charts-color-annotation, #1e1e1e)",
1249
+ surface: "var(--a8c-charts-color-surface, #fff)",
1250
+ surfaceSecondary: "var(--a8c-charts-color-surface-secondary, #f4f4f4)",
1251
+ trendUp: "var(--a8c-charts-color-trend-up, #008030)",
1252
+ trendDown: "var(--a8c-charts-color-trend-down, #cc1818)",
1253
+ series: SERIES_PALETTE_POINTERS
1254
+ };
1255
+ /**
1256
+ * The annotation parts visx paints, in the shape `@visx/annotation` takes. `radius`
1257
+ * rides along as the base the theme and the per-datum styles merge onto.
1258
+ */
1259
+ const ANNOTATION_POINTERS = {
1260
+ label: {
1261
+ anchorLineStroke: CATALOG_POINTERS.annotation,
1262
+ backgroundFill: CATALOG_POINTERS.surface
1263
+ },
1264
+ connector: { stroke: CATALOG_POINTERS.annotation },
1265
+ circleSubject: {
1266
+ stroke: "transparent",
1267
+ fill: CATALOG_POINTERS.annotation,
1268
+ radius: 5
1269
+ }
1270
+ };
1271
+ //#endregion
1255
1272
  //#region src/providers/chart-scope/use-chart-scope-element.ts
1256
1273
  /**
1257
1274
  * Returns the nearest chart scope element, or `null` when rendered outside any scope (and during SSR, before the ref is attached).
@@ -1278,13 +1295,34 @@ const useXYChartTheme = (data) => {
1278
1295
  return useMemo(() => {
1279
1296
  const resolve = createCssVariableResolver(scopeElement);
1280
1297
  const resolveColor = (value) => value ? resolve(value) ?? value : value;
1281
- const paletteColors = [...JSON.parse(seriesColorKey), ...theme.colors ?? []].map((color) => resolveColor(color)).filter((color) => Boolean(color) && !color.includes("var("));
1282
- const htmlLabelColor = resolveColor(theme.svgLabelSmall?.fill);
1298
+ const paletteColors = [...JSON.parse(seriesColorKey), ...CATALOG_POINTERS.series].map((color) => resolveColor(color)).filter((color) => Boolean(color) && !color.includes("var("));
1299
+ const resolvedLabelColor = resolveColor(CATALOG_POINTERS.labelAxis);
1300
+ const htmlLabelColor = resolvedLabelColor ? normalizeColorToHex(resolvedLabelColor) || resolvedLabelColor : resolvedLabelColor;
1283
1301
  return buildChartTheme({
1284
1302
  ...theme,
1303
+ gridColor: "",
1304
+ gridColorDark: "",
1285
1305
  colors: paletteColors,
1286
- backgroundColor: resolveColor(theme.backgroundColor),
1287
- htmlLabel: htmlLabelColor ? { color: htmlLabelColor } : void 0
1306
+ backgroundColor: resolveColor(CATALOG_POINTERS.background),
1307
+ htmlLabel: htmlLabelColor ? { color: htmlLabelColor } : void 0,
1308
+ gridStyles: {
1309
+ ...theme.gridStyles,
1310
+ stroke: CATALOG_POINTERS.grid
1311
+ },
1312
+ xAxisLineStyles: {
1313
+ ...theme.xAxisLineStyles,
1314
+ stroke: CATALOG_POINTERS.axisX
1315
+ },
1316
+ xTickLineStyles: {
1317
+ ...theme.xTickLineStyles,
1318
+ stroke: CATALOG_POINTERS.tickX
1319
+ },
1320
+ yAxisLineStyles: { stroke: CATALOG_POINTERS.axisY },
1321
+ yTickLineStyles: { stroke: CATALOG_POINTERS.tickY },
1322
+ svgLabelSmall: {
1323
+ ...theme.svgLabelSmall,
1324
+ fill: CATALOG_POINTERS.labelAxis
1325
+ }
1288
1326
  });
1289
1327
  }, [
1290
1328
  theme,
@@ -1293,13 +1331,28 @@ const useXYChartTheme = (data) => {
1293
1331
  ]);
1294
1332
  };
1295
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
1296
1349
  //#region src/hooks/use-chart-data-transform.ts
1297
1350
  /**
1298
1351
  * Hook that transforms and sorts chart data, handling date parsing and sorting
1299
1352
  *
1300
1353
  * This hook extracts the common data transformation logic used in both line-chart
1301
1354
  * and bar-chart components. It:
1302
- * 1. Parses date strings into Date objects using parseAsLocalDate
1355
+ * 1. Parses date strings into Date objects, dated in the provider's time zone
1303
1356
  * 2. Sorts data points by date when date properties are present
1304
1357
  * 3. Returns the original data unchanged when no date properties are found
1305
1358
  *
@@ -1307,6 +1360,7 @@ const useXYChartTheme = (data) => {
1307
1360
  * @return {SeriesData[]} The transformed and sorted data
1308
1361
  */
1309
1362
  const useChartDataTransform = (data) => {
1363
+ const { timeZone } = useChartFormatting();
1310
1364
  return useMemo(() => {
1311
1365
  const firstPoint = data?.[0]?.data?.[0];
1312
1366
  if (!(firstPoint && ("date" in firstPoint || "dateString" in firstPoint))) return data;
@@ -1315,7 +1369,7 @@ const useChartDataTransform = (data) => {
1315
1369
  data: series.data.map((point) => {
1316
1370
  let date;
1317
1371
  if ("date" in point && point.date) date = point.date;
1318
- else if ("dateString" in point && point.dateString) date = parseAsLocalDate(point.dateString);
1372
+ else if ("dateString" in point && point.dateString) date = parseAsLocalDate(point.dateString, timeZone);
1319
1373
  return {
1320
1374
  ...point,
1321
1375
  date
@@ -1325,7 +1379,7 @@ const useChartDataTransform = (data) => {
1325
1379
  return a.date.getTime() - b.date.getTime();
1326
1380
  })
1327
1381
  }));
1328
- }, [data]);
1382
+ }, [data, timeZone]);
1329
1383
  };
1330
1384
  //#endregion
1331
1385
  //#region src/hooks/use-chart-margin.tsx
@@ -1367,11 +1421,32 @@ const DEFAULT_TICK_LENGTH = 8;
1367
1421
  * measure them via getLongestTickWidth.
1368
1422
  */
1369
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
+ };
1370
1444
  const getXAxisLabelMetrics = (theme, orientation) => {
1371
1445
  const xAxisStyles = orientation === "top" ? theme.axisStyles?.x?.top : theme.axisStyles?.x?.bottom;
1372
1446
  return {
1373
1447
  fontSize: resolveFontSize(xAxisStyles?.axisLabel?.fontSize) || resolveFontSize(theme.svgLabelSmall?.fontSize) || DEFAULT_FONT_SIZE,
1374
- tickLength: xAxisStyles?.tickLength ?? DEFAULT_TICK_LENGTH
1448
+ tickLength: xAxisStyles?.tickLength ?? DEFAULT_TICK_LENGTH,
1449
+ tickLabelStyle: toMeasurableStyle(xAxisStyles?.tickLabel)
1375
1450
  };
1376
1451
  };
1377
1452
  const useChartMargin = (height, options, data, theme, horizontal = false) => {
@@ -1383,7 +1458,7 @@ const useChartMargin = (height, options, data, theme, horizontal = false) => {
1383
1458
  const maxY = Math.max(...allDataPoints.map((d) => d.value));
1384
1459
  const yScale = createScale({
1385
1460
  ...options.yScale,
1386
- domain: [minY, maxY],
1461
+ domain: options.yScale?.domain ?? [minY, maxY],
1387
1462
  range: [height, 0]
1388
1463
  });
1389
1464
  return getTicks(yScale, options.axis?.y?.numTicks);
@@ -1402,18 +1477,25 @@ const useChartMargin = (height, options, data, theme, horizontal = false) => {
1402
1477
  };
1403
1478
  const yAxisOrientation = options.axis?.y?.orientation;
1404
1479
  const yAxisStyles = yAxisOrientation === "right" ? theme.axisStyles.y.right : theme.axisStyles.y.left;
1405
- const yTickWidth = getLongestTickWidth(yTicks, options.axis?.y?.tickFormat, yAxisStyles.axisLabel);
1480
+ const yTickWidth = getLongestTickWidth(yTicks, options.axis?.y?.tickFormat, toMeasurableStyle(yAxisStyles.axisLabel));
1406
1481
  const yTickLabelFontSize = resolveFontSize(yAxisStyles?.tickLabel?.fontSize) || DEFAULT_FONT_SIZE;
1407
1482
  const yMarginValue = (yTickWidth ?? DEFAULT_Y_TICK_WIDTH) + (yAxisStyles?.tickLength ?? 0) + Math.ceil(yTickLabelFontSize * .25);
1408
- if (yAxisOrientation === "right") defaultMargin.right = yMarginValue;
1409
- else defaultMargin.left = yMarginValue;
1483
+ if (options.axis?.y?.display !== false) {
1484
+ if (yAxisOrientation === "right") defaultMargin.right = yMarginValue;
1485
+ else defaultMargin.left = yMarginValue;
1486
+ }
1410
1487
  const xOrientation = options.axis?.x?.orientation === "top" ? "top" : "bottom";
1411
- const { fontSize, tickLength } = getXAxisLabelMetrics(theme, xOrientation);
1488
+ const { fontSize, tickLength, tickLabelStyle } = getXAxisLabelMetrics(theme, xOrientation);
1412
1489
  const computedXMargin = fontSize + tickLength;
1413
1490
  if (xOrientation === "top") {
1414
1491
  defaultMargin.top = Math.max(defaultMargin.top, computedXMargin);
1415
1492
  defaultMargin.bottom = DEFAULT_BOTTOM_FOR_TOP_AXIS;
1416
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
+ }
1417
1499
  return defaultMargin;
1418
1500
  }, [
1419
1501
  options,
@@ -2126,15 +2208,370 @@ const BaseTooltip = ({ data, top, left, component: Component = DefaultTooltipCon
2126
2208
  });
2127
2209
  };
2128
2210
  //#endregion
2211
+ //#region src/components/tooltip/private/bounded-tooltip.tsx
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));
2215
+ const isClipping = (element) => {
2216
+ const { overflow, overflowX, overflowY } = getComputedStyle(element);
2217
+ return [
2218
+ overflow,
2219
+ overflowX,
2220
+ overflowY
2221
+ ].some((value) => value && value !== "visible");
2222
+ };
2223
+ const findClippingAncestor = (wrapper) => {
2224
+ let element = wrapper.parentElement;
2225
+ while (element && element !== document.body) {
2226
+ if (isClipping(element)) return element;
2227
+ element = element.parentElement;
2228
+ }
2229
+ return null;
2230
+ };
2231
+ /**
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.
2235
+ *
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.
2242
+ * @param params.left - Anchor x, in wrapper coordinates.
2243
+ * @param params.top - Anchor y, in wrapper coordinates.
2244
+ * @param params.offsetLeft - Gap between the anchor and the box, horizontally.
2245
+ * @param params.offsetTop - Gap between the anchor and the box, vertically.
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.
2249
+ * @param params.bounds - Edges the box must keep inside, in wrapper coordinates.
2250
+ * @return The box's top-left corner; fixed placement preserves the axis's subpixel y.
2251
+ */
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
+ };
2263
+ const rightX = left + offsetLeft;
2264
+ const leftX = left - offsetLeft - box.width;
2265
+ const rightOverflow = rightX + box.width - fit.right;
2266
+ const leftOverflow = fit.left - leftX;
2267
+ let x = rightOverflow > 0 && rightOverflow > leftOverflow ? leftX : rightX;
2268
+ const downY = top + offsetTop;
2269
+ const upY = top - offsetTop - box.height;
2270
+ const downOverflow = downY + box.height - fit.bottom;
2271
+ const upOverflow = fit.top - upY;
2272
+ let y = downOverflow > 0 && downOverflow > upOverflow ? upY : downY;
2273
+ x = clamp(x, bounds.left, bounds.right, box.width);
2274
+ y = clamp(y, bounds.top, bounds.bottom, box.height);
2275
+ return {
2276
+ x: Math.round(x),
2277
+ y: Math.round(y)
2278
+ };
2279
+ };
2280
+ /**
2281
+ * visx's `Tooltip`, positioned like its `TooltipWithBounds` but kept inside the
2282
+ * nearest clipping ancestor — or the viewport when there is none — rather than
2283
+ * inside its own parent. Rendered in-tree, a tooltip's parent is the chart
2284
+ * wrapper, which is often narrower than the box; measuring against the parent
2285
+ * alone would let the box spill into an `overflow: hidden` card and be cut off.
2286
+ *
2287
+ * Re-measures on every render, so a box whose content changes width between
2288
+ * two hovers is placed for its current size. See `getBoundedPosition` for the
2289
+ * below-axis exception.
2290
+ *
2291
+ * @param props - visx `Tooltip` props.
2292
+ * @param props.left - Anchor x, in wrapper coordinates.
2293
+ * @param props.top - Anchor y, in wrapper coordinates.
2294
+ * @param props.offsetLeft - Gap between the anchor and the box, horizontally.
2295
+ * @param props.offsetTop - Gap between the anchor and the box, vertically.
2296
+ * @param props.style - Box styles; visx's defaults unless `unstyled`.
2297
+ * @param props.unstyled - Skip `style` and leave the box bare.
2298
+ * @param props.children - Box content.
2299
+ * @param props.placement - Fixed below-axis placement or automatic flipping.
2300
+ * @return The tooltip box.
2301
+ */
2302
+ const BoundedTooltip = ({ left = 0, top = 0, offsetLeft = DEFAULT_OFFSET, offsetTop = DEFAULT_OFFSET, style = defaultStyles, unstyled = false, children, placement = "auto", ...rest }) => {
2303
+ const nodeRef = useRef(null);
2304
+ const clipRef = useRef(null);
2305
+ const [position, setPosition] = useState(null);
2306
+ useLayoutEffect(() => {
2307
+ const node = nodeRef.current;
2308
+ const wrapper = node?.parentElement;
2309
+ if (!node || !wrapper) return;
2310
+ if (clipRef.current?.wrapper !== wrapper) clipRef.current = {
2311
+ wrapper,
2312
+ clip: findClippingAncestor(wrapper)
2313
+ };
2314
+ const own = node.getBoundingClientRect();
2315
+ const wrapperRect = wrapper.getBoundingClientRect();
2316
+ const clipRect = clipRef.current.clip?.getBoundingClientRect() ?? {
2317
+ left: 0,
2318
+ top: 0,
2319
+ right: window.innerWidth,
2320
+ bottom: window.innerHeight
2321
+ };
2322
+ const next = getBoundedPosition({
2323
+ left,
2324
+ top,
2325
+ offsetLeft,
2326
+ offsetTop,
2327
+ placement,
2328
+ box: {
2329
+ width: own.width,
2330
+ height: own.height
2331
+ },
2332
+ wrapper: {
2333
+ left: 0,
2334
+ top: 0,
2335
+ right: wrapperRect.width,
2336
+ bottom: wrapperRect.height
2337
+ },
2338
+ bounds: {
2339
+ left: clipRect.left - wrapperRect.left,
2340
+ top: clipRect.top - wrapperRect.top,
2341
+ right: clipRect.right - wrapperRect.left,
2342
+ bottom: clipRect.bottom - wrapperRect.top
2343
+ }
2344
+ });
2345
+ setPosition((current) => current && current.x === next.x && current.y === next.y ? current : next);
2346
+ }, [
2347
+ left,
2348
+ top,
2349
+ offsetLeft,
2350
+ offsetTop,
2351
+ children,
2352
+ placement
2353
+ ]);
2354
+ const x = position?.x ?? left + offsetLeft;
2355
+ const y = position?.y ?? top + (placement === "below-axis" ? POINTER_HEIGHT : offsetTop);
2356
+ return /* @__PURE__ */ jsxs(Tooltip, {
2357
+ ref: nodeRef,
2358
+ style: {
2359
+ position: "absolute",
2360
+ left: 0,
2361
+ top: 0,
2362
+ transform: `translate(${x}px, ${y}px)`,
2363
+ ...!unstyled && style
2364
+ },
2365
+ ...rest,
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]
2379
+ });
2380
+ };
2381
+ //#endregion
2382
+ //#region src/components/tooltip/xy-chart-tooltip.tsx
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
+ };
2400
+ const DEFAULT_GLYPH_RADIUS = 4;
2401
+ const FALLBACK_COLOR = "#222";
2402
+ const DEFAULT_TOOLTIP_Z_INDEX = 3;
2403
+ const PORTAL_OPTIONS = /* @__PURE__ */ new Set([
2404
+ "scroll",
2405
+ "debounce",
2406
+ "resizeObserverPolyfill"
2407
+ ]);
2408
+ const isValidNumber = (value) => typeof value === "number" && Number.isFinite(value);
2409
+ const scaleBandwidth = (scale) => {
2410
+ const bandwidth = scale?.bandwidth;
2411
+ return typeof bandwidth === "function" ? Number(bandwidth()) : 0;
2412
+ };
2413
+ const DefaultGlyph$1 = ({ x, y, size, color, glyphStyle, seriesKey }) => {
2414
+ const { theme } = useContext(DataContext) || {};
2415
+ return /* @__PURE__ */ jsx("circle", {
2416
+ cx: x,
2417
+ cy: y,
2418
+ r: size,
2419
+ fill: color,
2420
+ stroke: theme?.backgroundColor,
2421
+ strokeWidth: CROSSHAIR_STROKE_WIDTH,
2422
+ paintOrder: "fill",
2423
+ ...glyphStyle
2424
+ });
2425
+ };
2426
+ const defaultRenderGlyph$1 = ({ key, ...props }) => /* @__PURE__ */ jsx(DefaultGlyph$1, {
2427
+ seriesKey: key,
2428
+ ...props
2429
+ }, key);
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 }) => {
2431
+ const tooltipProps = Object.fromEntries(Object.entries(rest).filter(([key]) => !PORTAL_OPTIONS.has(key)));
2432
+ const { colorScale, theme, innerHeight = 0, innerWidth = 0, margin, xScale, yScale, dataRegistry } = useContext(DataContext) || {};
2433
+ const tooltipContent = renderTooltip ? renderTooltip({
2434
+ ...tooltipContext,
2435
+ colorScale
2436
+ }) : null;
2437
+ if (tooltipContent == null) return null;
2438
+ const scaleX = xScale;
2439
+ const scaleY = yScale;
2440
+ const getDatumLeftTop = (key, datum) => {
2441
+ const entry = dataRegistry?.get(key);
2442
+ return {
2443
+ left: scaleX && entry?.xAccessor ? Number(scaleX(entry.xAccessor(datum))) + scaleBandwidth(xScale) / 2 : void 0,
2444
+ top: scaleY && entry?.yAccessor ? Number(scaleY(entry.yAccessor(datum))) + scaleBandwidth(yScale) / 2 : void 0
2445
+ };
2446
+ };
2447
+ const nearestDatum = tooltipContext.tooltipData?.nearestDatum;
2448
+ let { tooltipLeft, tooltipTop } = tooltipContext;
2449
+ if (nearestDatum && (snapTooltipToDatumX || snapTooltipToDatumY || tooltipPlacement === "below-axis")) {
2450
+ const { left, top } = getDatumLeftTop(nearestDatum.key, nearestDatum.datum);
2451
+ if ((snapTooltipToDatumX || tooltipPlacement === "below-axis") && isValidNumber(left)) tooltipLeft = left;
2452
+ if (snapTooltipToDatumY && isValidNumber(top)) tooltipTop = top;
2453
+ }
2454
+ const glyphs = [];
2455
+ if (showDatumGlyph || showSeriesGlyphs) {
2456
+ const size = Number(glyphStyle?.radius ?? DEFAULT_GLYPH_RADIUS);
2457
+ const labelColor = theme?.htmlLabel?.color ?? FALLBACK_COLOR;
2458
+ const fallbackColor = showSeriesGlyphs ? labelColor : theme?.gridStyles?.stroke ?? labelColor;
2459
+ let entries = [];
2460
+ if (showSeriesGlyphs) entries = Object.values(tooltipContext.tooltipData?.datumByKey ?? {});
2461
+ else if (nearestDatum) entries = [nearestDatum];
2462
+ for (const { key, datum, index } of entries) {
2463
+ const { left, top } = getDatumLeftTop(key, datum);
2464
+ if (!isValidNumber(left) || !isValidNumber(top)) continue;
2465
+ glyphs.push(/* @__PURE__ */ jsx("g", {
2466
+ className: "visx-tooltip-glyph",
2467
+ transform: `translate(${left}, ${top})`,
2468
+ children: renderGlyph({
2469
+ key,
2470
+ color: colorScale?.(key) ?? fallbackColor,
2471
+ datum,
2472
+ index,
2473
+ size,
2474
+ x: 0,
2475
+ y: 0,
2476
+ glyphStyle,
2477
+ isNearestDatum: nearestDatum?.key === key
2478
+ })
2479
+ }, key));
2480
+ }
2481
+ }
2482
+ const crosshairStroke = theme?.gridStyles?.stroke ?? theme?.htmlLabel?.color ?? FALLBACK_COLOR;
2483
+ const marginTop = margin?.top ?? 0;
2484
+ const marginLeft = margin?.left ?? 0;
2485
+ const TooltipComponent = detectBounds || tooltipPlacement === "below-axis" ? BoundedTooltip : Tooltip;
2486
+ const boxStyle = {
2487
+ ...defaultStyles,
2488
+ zIndex,
2489
+ backgroundColor: theme?.backgroundColor ?? "white",
2490
+ boxShadow: `0 1px 2px ${theme?.htmlLabel?.color ? `${theme.htmlLabel.color}55` : "#22222255"}`,
2491
+ ...theme?.htmlLabel,
2492
+ ...style
2493
+ };
2494
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("g", {
2495
+ className: "visx-tooltip-overlay",
2496
+ pointerEvents: "none",
2497
+ children: [
2498
+ showVerticalCrosshair && isValidNumber(tooltipLeft) && /* @__PURE__ */ jsx("line", {
2499
+ className: "visx-crosshair visx-crosshair-vertical",
2500
+ stroke: crosshairStroke,
2501
+ strokeWidth: CROSSHAIR_STROKE_WIDTH,
2502
+ ...crosshairPaintProps(verticalCrosshairStyle),
2503
+ x1: tooltipLeft,
2504
+ x2: tooltipLeft,
2505
+ y1: marginTop,
2506
+ y2: marginTop + innerHeight
2507
+ }),
2508
+ showHorizontalCrosshair && isValidNumber(tooltipTop) && /* @__PURE__ */ jsx("line", {
2509
+ className: "visx-crosshair visx-crosshair-horizontal",
2510
+ stroke: crosshairStroke,
2511
+ strokeWidth: CROSSHAIR_STROKE_WIDTH,
2512
+ ...crosshairPaintProps(horizontalCrosshairStyle),
2513
+ x1: marginLeft,
2514
+ x2: marginLeft + innerWidth,
2515
+ y1: tooltipTop,
2516
+ y2: tooltipTop
2517
+ }),
2518
+ glyphs
2519
+ ]
2520
+ }), container && createPortal(/* @__PURE__ */ jsx(TooltipComponent, {
2521
+ left: tooltipLeft,
2522
+ top: tooltipPlacement === "below-axis" ? marginTop + innerHeight + (margin?.bottom ?? 0) : tooltipTop,
2523
+ style: boxStyle,
2524
+ applyPositionStyle: true,
2525
+ ...tooltipProps,
2526
+ ...tooltipPlacement === "below-axis" && { placement: tooltipPlacement },
2527
+ children: tooltipContent
2528
+ }), container)] });
2529
+ };
2530
+ /**
2531
+ * In-tree replacement for `@visx/xychart`'s `Tooltip`.
2532
+ *
2533
+ * visx renders its tooltip box, glyphs and crosshairs through portals appended
2534
+ * to `document.body`. That puts them in the page's root stacking context, where
2535
+ * they paint above every sticky or fixed element that lives inside a nested
2536
+ * stacking context, and no z-index on that element can change the order. This
2537
+ * component draws the glyphs and crosshairs straight into the chart SVG and
2538
+ * renders the tooltip box into the SVG's parent element, so all of them stack
2539
+ * as ordinary descendants of the chart.
2540
+ *
2541
+ * Render it as a child of `XYChart`. The element wrapping that `XYChart` must
2542
+ * be `position: relative` with the SVG at its origin, and `isolation: isolate`:
2543
+ * the box is placed with the SVG-local coordinates visx reports, and the
2544
+ * isolation keeps the box's `zIndex` from competing with page chrome. The box
2545
+ * flips and clamps to stay inside the nearest ancestor that clips its overflow
2546
+ * (or the viewport), so it may extend past the chart wrapper but is never cut
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.
2549
+ *
2550
+ * @param props - visx's `Tooltip` options. `scroll`, `debounce` and `resizeObserverPolyfill` are accepted and ignored.
2551
+ * @return An anchor in the SVG, plus the overlay and the tooltip box while the tooltip is open.
2552
+ */
2553
+ const XyChartTooltip = (props) => {
2554
+ const tooltipContext = useContext(TooltipContext);
2555
+ const [container, setContainer] = useState(null);
2556
+ const anchorRef = useCallback((node) => {
2557
+ setContainer(node?.ownerSVGElement?.parentElement ?? null);
2558
+ }, []);
2559
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("g", { ref: anchorRef }), tooltipContext?.tooltipOpen && /* @__PURE__ */ jsx(XyChartTooltipContent, {
2560
+ ...props,
2561
+ tooltipContext,
2562
+ container
2563
+ })] });
2564
+ };
2565
+ //#endregion
2129
2566
  //#region src/components/tooltip/accessible-tooltip.tsx
2130
2567
  const AccessibleTooltip = ({ renderTooltip, selectedIndex, tooltipRef, keyboardFocusedClassName, series = [], mode = "group", verticalCrosshairStyle, horizontalCrosshairStyle, ...props }) => {
2131
2568
  const tooltipContext = useContext(TooltipContext);
2132
2569
  const scopeElement = useChartScopeElement();
2133
- const gridStroke = useGlobalChartsTheme().gridStyles?.stroke;
2134
2570
  const crosshairStroke = useMemo(() => {
2135
- const stroke = gridStroke ? resolveCssVariable(gridStroke, scopeElement) : null;
2571
+ const stroke = resolveCssVariable(CATALOG_POINTERS.grid, scopeElement);
2136
2572
  return stroke ? { stroke } : void 0;
2137
- }, [gridStroke, scopeElement]);
2573
+ }, [scopeElement]);
2574
+ const standaloneScopeClass = useStandaloneScopeClass();
2138
2575
  const tooltipData = useMemo(() => {
2139
2576
  if (mode !== "individual") return [];
2140
2577
  if (series.length === 0) return [];
@@ -2195,11 +2632,11 @@ const AccessibleTooltip = ({ renderTooltip, selectedIndex, tooltipRef, keyboardF
2195
2632
  tabIndex: -1,
2196
2633
  role: "tooltip",
2197
2634
  "aria-atomic": "true",
2198
- className: clsx(CHART_SCOPE_CLASS, keyboardFocusedClassName),
2635
+ className: clsx(standaloneScopeClass, keyboardFocusedClassName),
2199
2636
  children: tooltipContent
2200
2637
  }, `chart-tooltip-${selectedIndex}`);
2201
2638
  return /* @__PURE__ */ jsx("div", {
2202
- className: CHART_SCOPE_CLASS,
2639
+ className: standaloneScopeClass,
2203
2640
  role: "tooltip",
2204
2641
  "aria-live": "polite",
2205
2642
  children: tooltipContent
@@ -2209,9 +2646,10 @@ const AccessibleTooltip = ({ renderTooltip, selectedIndex, tooltipRef, keyboardF
2209
2646
  renderTooltip,
2210
2647
  selectedIndex,
2211
2648
  tooltipRef,
2212
- keyboardFocusedClassName
2649
+ keyboardFocusedClassName,
2650
+ standaloneScopeClass
2213
2651
  ]);
2214
- return /* @__PURE__ */ jsx(Tooltip, {
2652
+ return /* @__PURE__ */ jsx(XyChartTooltip, {
2215
2653
  ...props,
2216
2654
  verticalCrosshairStyle: {
2217
2655
  ...crosshairStroke,
@@ -2224,11 +2662,14 @@ const AccessibleTooltip = ({ renderTooltip, selectedIndex, tooltipRef, keyboardF
2224
2662
  renderTooltip: focusableRenderTooltip
2225
2663
  });
2226
2664
  };
2227
- const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating, setIsNavigating, chartRef, totalPoints, onActivate }) => {
2665
+ const useKeyboardNavigation = ({ selectedIndex, setSelectedIndex, isNavigating, setIsNavigating, chartRef, totalPoints, onActivate, preventTooltipScroll = false }) => {
2228
2666
  return {
2229
2667
  tooltipRef: useCallback((element) => {
2230
- if (element && selectedIndex !== void 0) element.focus();
2231
- }, [selectedIndex]),
2668
+ if (element && selectedIndex !== void 0) {
2669
+ if (preventTooltipScroll) element.focus({ preventScroll: true });
2670
+ else element.focus();
2671
+ }
2672
+ }, [preventTooltipScroll, selectedIndex]),
2232
2673
  onChartFocus: useCallback(() => {
2233
2674
  if (!isNavigating && selectedIndex !== void 0) setSelectedIndex(0);
2234
2675
  }, [
@@ -2559,72 +3000,90 @@ const getCurveType = (type, smoothing) => {
2559
3000
  default: return curveLinear;
2560
3001
  }
2561
3002
  };
2562
- const dateTimeFormat = (options) => {
2563
- let formatter;
2564
- return (timestamp) => {
2565
- if (!Number.isFinite(Number(timestamp))) return new Date(timestamp).toLocaleDateString();
2566
- formatter = formatter ?? new Intl.DateTimeFormat(void 0, options);
2567
- return formatter.format(timestamp);
2568
- };
2569
- };
2570
- const formatYearTick = dateTimeFormat({ year: "numeric" });
2571
- const formatDateTick = dateTimeFormat({
3003
+ const YEAR_TICK = { year: "numeric" };
3004
+ const DATE_TICK = {
2572
3005
  month: "short",
2573
3006
  day: "numeric"
2574
- });
2575
- const formatHourTick = dateTimeFormat({
2576
- hour: "numeric",
2577
- hour12: true
2578
- });
2579
- const formatMonthTick = dateTimeFormat({ month: "short" });
3007
+ };
3008
+ const HOUR_TICK = { hour: "numeric" };
3009
+ const MONTH_TICK = { month: "short" };
2580
3010
  const boundaryFormat = (isAnchor, atBoundary, between) => {
2581
3011
  const format = (timestamp) => isAnchor(new Date(timestamp)) ? atBoundary(timestamp) : between(timestamp);
2582
3012
  format.isAnchor = isAnchor;
2583
3013
  return format;
2584
3014
  };
2585
- const formatDateOrHourTick = boundaryFormat((date) => date.getHours() === 0 && date.getMinutes() === 0, formatDateTick, formatHourTick);
2586
- const formatMonthOrYearTick = boundaryFormat((date) => date.getMonth() === 0, formatYearTick, formatMonthTick);
3015
+ const tickFormats = (formatting) => {
3016
+ const clock = createZonedClock(formatting.timeZone);
3017
+ const year = createDateFormatter(YEAR_TICK, formatting);
3018
+ const date = createDateFormatter(DATE_TICK, formatting);
3019
+ const hour = createDateFormatter(HOUR_TICK, formatting);
3020
+ const month = createDateFormatter(MONTH_TICK, formatting);
3021
+ const datedTick = (timestamp) => date(timestamp);
3022
+ datedTick.anchorsAtLabelChange = true;
3023
+ return {
3024
+ year,
3025
+ date: datedTick,
3026
+ dateOrHour: boundaryFormat((value) => clock(value).hour === 0, date, hour),
3027
+ hour,
3028
+ monthOrYear: boundaryFormat((value) => clock(value).month === 1, year, month)
3029
+ };
3030
+ };
2587
3031
  const getSpan = (sortedData) => {
2588
- const bounds = sortedData.map((datom) => [datom.data.at(0)?.date, datom.data.at(-1)?.date]).filter(([first, last]) => first !== void 0 && last !== void 0);
3032
+ const bounds = sortedData.map((datom) => [datom.data.at(0)?.date, datom.data.at(-1)?.date]).filter(([first, last]) => Number.isFinite(Number(first)) && Number.isFinite(Number(last)));
2589
3033
  if (!bounds.length) return null;
2590
3034
  return {
2591
3035
  minX: Math.min(...bounds.map(([first]) => Number(first))),
2592
3036
  maxX: Math.max(...bounds.map(([, last]) => Number(last)))
2593
3037
  };
2594
3038
  };
2595
- const getPointSpacingInHours = (sortedData) => {
2596
- return sortedData.reduce((spacing, datom) => datom.data.reduce((seriesSpacing, point, index) => {
2597
- const previous = datom.data[index - 1];
2598
- if (previous?.date === void 0 || point?.date === void 0) return seriesSpacing;
2599
- return Math.min(seriesSpacing, Math.abs(differenceInHours(point.date, previous.date)));
2600
- }, spacing), Number.POSITIVE_INFINITY);
2601
- };
2602
- const SUB_DAILY_SPACING_HOURS = 23;
2603
- const MONTHLY_SPACING_HOURS = 672;
2604
- const getBucketResolution = (sortedData, tickResolution) => {
2605
- if (tickResolution) return tickResolution === "week" ? "day" : tickResolution;
2606
- const spacingInHours = getPointSpacingInHours(sortedData);
2607
- if (spacingInHours < SUB_DAILY_SPACING_HOURS) return "hour";
2608
- if (!Number.isFinite(spacingInHours) || spacingInHours < MONTHLY_SPACING_HOURS) return "day";
2609
- return spacingInHours < 12 * MONTHLY_SPACING_HOURS ? "month" : "year";
2610
- };
2611
- const getFormatter = (sortedData, tickResolution) => {
3039
+ /**
3040
+ * The most informative tick format for a bucket resolution and the span on screen.
3041
+ *
3042
+ * @param sortedData - Series as returned by `useChartDataTransform`.
3043
+ * @param tickResolution - Caller-declared bucket resolution, when known.
3044
+ * @param formatting - Host locale and time zone.
3045
+ * @param domain - The effective scale domain, or undefined for the data's own extent.
3046
+ * @return The formatter the axis labels its ticks with.
3047
+ */
3048
+ const getFormatter = (sortedData, tickResolution, formatting = {}, domain) => {
3049
+ const format = tickFormats(formatting);
2612
3050
  const resolution = getBucketResolution(sortedData, tickResolution);
2613
- if (resolution === "year") return formatYearTick;
2614
- if (resolution === "month") return formatMonthOrYearTick;
2615
- const span = getSpan(sortedData);
2616
- if (!span) return formatDateTick;
3051
+ if (resolution === "year") return format.year;
3052
+ if (resolution === "month") return format.monthOrYear;
3053
+ const span = domain ? {
3054
+ minX: domain[0].getTime(),
3055
+ maxX: domain[1].getTime()
3056
+ } : getSpan(sortedData);
3057
+ if (!span) return format.date;
2617
3058
  const diffInHours = Math.abs(differenceInHours(span.maxX, span.minX));
2618
3059
  if (resolution === "hour") {
2619
- if (diffInHours <= 24) return formatHourTick;
2620
- if (diffInHours <= 168) return formatDateOrHourTick;
3060
+ if (diffInHours < 24) return format.hour;
3061
+ if (diffInHours <= 168) return format.dateOrHour;
2621
3062
  }
2622
- return Math.abs(differenceInYears(span.maxX, span.minX)) <= 1 ? formatDateTick : formatYearTick;
3063
+ return Math.abs(differenceInYears(span.maxX, span.minX)) <= 1 ? format.date : format.year;
3064
+ };
3065
+ /**
3066
+ * The instants a set of series spans, as a scale domain.
3067
+ *
3068
+ * @param sortedData - Series as returned by `useChartDataTransform`.
3069
+ * @return Earliest and latest dated point, or undefined when nothing is dated.
3070
+ */
3071
+ const getSeriesExtent = (sortedData) => {
3072
+ const span = getSpan(sortedData);
3073
+ return span ? [new Date(span.minX), new Date(span.maxX)] : void 0;
3074
+ };
3075
+ const getAnchorIndices = (domain, tickFormatter, labels) => {
3076
+ if (tickFormatter.anchorsAtLabelChange) return labels.reduce((indices, label, index) => {
3077
+ if (index === 0 || label !== labels[index - 1]) indices.push(index);
3078
+ return indices;
3079
+ }, []);
3080
+ const { isAnchor } = tickFormatter;
3081
+ if (!isAnchor) return [];
3082
+ return domain.reduce((indices, date, index) => {
3083
+ if (isAnchor(date)) indices.push(index);
3084
+ return indices;
3085
+ }, []);
2623
3086
  };
2624
- const getAnchorIndices = (domain, isAnchor) => domain.reduce((indices, date, index) => {
2625
- if (isAnchor(date)) indices.push(index);
2626
- return indices;
2627
- }, []);
2628
3087
  const getCandidateSteps = (length, maxTicks, anchorSpacing) => {
2629
3088
  const steps = /* @__PURE__ */ new Set();
2630
3089
  for (let count = maxTicks; count > 1; count--) {
@@ -2639,26 +3098,25 @@ const getCandidateSteps = (length, maxTicks, anchorSpacing) => {
2639
3098
  return steps;
2640
3099
  };
2641
3100
  /**
2642
- * Tick values for a band time axis.
3101
+ * Tick values sampled by index, for the bar chart's band axis and for evenly
3102
+ * spaced points on a continuous one.
2643
3103
  *
2644
- * visx samples a band domain by index from offset zero, blind to which labels
2645
- * carry a boundary and without collapsing repeats — so the tick that prints the
2646
- * year or the date often isn't sampled at all, and a long series can show the
2647
- * same label twice. Choose the values instead: sweep the evenly spaced
2648
- * candidates, including those that step from anchor to anchor, and keep the one
2649
- * that reaches the most anchors without thinning the axis or putting two
2650
- * identical labels side by side.
3104
+ * visx samples by index from offset zero, blind to which labels carry a boundary
3105
+ * and without collapsing repeats — so the tick that prints the year or the date
3106
+ * often isn't sampled at all, and a long series can show the same label twice.
3107
+ * Choose the values instead: sweep the evenly spaced candidates, including those
3108
+ * that step from anchor to anchor, and keep the one that reaches the most anchors
3109
+ * without thinning the axis or putting two identical labels side by side.
2651
3110
  *
2652
- * @param domain - Band domain, in axis order.
3111
+ * @param domain - Buckets in axis order.
2653
3112
  * @param tickFormatter - Formatter the axis will render these values with.
2654
3113
  * @param maxTicks - Most ticks the axis should carry.
2655
3114
  * @return Values to hand the axis as `tickValues`.
2656
3115
  */
2657
3116
  const getBandTickValues = (domain, tickFormatter, maxTicks) => {
2658
3117
  if (!domain.length) return [];
2659
- const { isAnchor } = tickFormatter;
2660
- const anchorIndices = isAnchor ? getAnchorIndices(domain, isAnchor) : [];
2661
3118
  const domainLabels = domain.map((date) => tickFormatter(date.getTime()));
3119
+ const anchorIndices = getAnchorIndices(domain, tickFormatter, domainLabels);
2662
3120
  const candidates = [];
2663
3121
  const consider = (indices) => {
2664
3122
  if (!indices.length || indices.length > maxTicks) return;
@@ -2670,15 +3128,108 @@ const getBandTickValues = (domain, tickFormatter, maxTicks) => {
2670
3128
  for (let index = offset; index < domain.length; index += step) indices.push(index);
2671
3129
  consider(indices);
2672
3130
  }
2673
- for (let stride = 1; stride <= anchorIndices.length; stride++) consider(anchorIndices.filter((_, position) => position % stride === 0));
3131
+ const minStride = Math.max(1, Math.ceil(anchorIndices.length / maxTicks));
3132
+ for (let stride = minStride; stride <= anchorIndices.length; stride++) {
3133
+ const stepped = [];
3134
+ for (let position = 0; position < anchorIndices.length; position += stride) stepped.push(anchorIndices[position]);
3135
+ consider(stepped);
3136
+ }
2674
3137
  if (!candidates.length) return [domain[0]];
2675
3138
  const densest = candidates.reduce((most, indices) => Math.max(most, indices.length), 0);
2676
- const anchorsIn = (indices) => isAnchor ? indices.filter((index) => isAnchor(domain[index])).length : 0;
3139
+ const anchorIndexSet = new Set(anchorIndices);
3140
+ const anchorsIn = (indices) => indices.filter((index) => anchorIndexSet.has(index)).length;
2677
3141
  return candidates.filter((indices) => indices.length >= densest - 1).reduce((chosen, indices) => {
2678
3142
  const gain = anchorsIn(indices) - anchorsIn(chosen);
2679
3143
  return gain > 0 || gain === 0 && indices.length > chosen.length ? indices : chosen;
2680
3144
  }).map((index) => domain[index]);
2681
3145
  };
3146
+ /**
3147
+ * The most x-axis ticks a chart of this width has room for.
3148
+ *
3149
+ * @param chartWidth - Chart width in pixels.
3150
+ * @return A tick budget of at least one.
3151
+ */
3152
+ const getMaxTicksForWidth = (chartWidth) => Math.max(1, Math.floor(chartWidth / X_TICK_WIDTH));
3153
+ const MIN_GAP_IN_LABELS = .9;
3154
+ const nearestValue = (sorted, target) => {
3155
+ let low = 0;
3156
+ let high = sorted.length - 1;
3157
+ while (low < high) {
3158
+ const middle = Math.floor((low + high) / 2);
3159
+ if (sorted[middle] < target) low = middle + 1;
3160
+ else high = middle;
3161
+ }
3162
+ const above = sorted[low];
3163
+ const below = low > 0 ? sorted[low - 1] : above;
3164
+ return Math.abs(above - target) <= Math.abs(below - target) ? above : below;
3165
+ };
3166
+ const getPositionTickValues = (points, tickFormatter, maxTicks, axis) => {
3167
+ const times = points.map((point) => point.getTime());
3168
+ const { first, last } = axis;
3169
+ if (maxTicks <= 1 || times.length === 1 || first === last) return [points[0]];
3170
+ const step = (last - first) / (maxTicks - 1);
3171
+ const labels = points.map((point) => tickFormatter(point.getTime()));
3172
+ const minGap = (last - first) / maxTicks * MIN_GAP_IN_LABELS;
3173
+ const anchors = getAnchorIndices(points, tickFormatter, labels).map((index) => times[index]);
3174
+ const chosen = [];
3175
+ let lastLabel = null;
3176
+ for (let index = 0; index < maxTicks; index++) {
3177
+ const target = first + index * step;
3178
+ const anchor = anchors.length ? nearestValue(anchors, target) : null;
3179
+ let tick = anchor !== null && Math.abs(anchor - target) <= step / 2 ? anchor : null;
3180
+ if (tick === null) {
3181
+ const point = nearestValue(times, target);
3182
+ tick = Math.abs(point - target) <= step / 2 ? point : target;
3183
+ }
3184
+ const previous = chosen[chosen.length - 1];
3185
+ if (previous !== void 0 && (tick <= previous || tick - previous < minGap)) continue;
3186
+ const label = tickFormatter(tick);
3187
+ if (label === lastLabel) continue;
3188
+ chosen.push(tick);
3189
+ lastLabel = label;
3190
+ }
3191
+ return chosen.map((timestamp) => new Date(timestamp));
3192
+ };
3193
+ const isIndexSamplingUnusable = (ticks, points, maxTicks, axis) => {
3194
+ if (ticks.length < 2 || points.length < 2) return false;
3195
+ const span = axis.last - axis.first;
3196
+ if (!span) return false;
3197
+ return ticks.slice(1).reduce((nearest, tick, index) => Math.min(nearest, tick.getTime() - ticks[index].getTime()), Infinity) < span / maxTicks * MIN_GAP_IN_LABELS || ticks.length < Math.min(maxTicks, points.length) / 3;
3198
+ };
3199
+ /**
3200
+ * Tick values for a continuous time axis, chosen from the points it draws.
3201
+ *
3202
+ * Sampled by index while the points are evenly spaced, which keeps the axis on
3203
+ * the calendar boundaries `getBandTickValues` steers towards, and by position
3204
+ * once they are not, where an index stride crowds one end of the scale.
3205
+ *
3206
+ * Values are taken from the data wherever there is one to take, so a local time
3207
+ * that does not exist cannot become a tick and DST needs no special case. Only a
3208
+ * target stranded in a gap is constructed, as a plain instant.
3209
+ *
3210
+ * @param sortedData - Series as returned by `useChartDataTransform`.
3211
+ * @param domain - The effective scale domain, or undefined for all data.
3212
+ * @param tickFormatter - The formatter the ticks will be labeled with.
3213
+ * @param maxTicks - The most ticks the axis has room for.
3214
+ * @return Tick dates, or null when no series carries a usable date.
3215
+ */
3216
+ const getTimeAxisTickValues = (sortedData, domain, tickFormatter, maxTicks) => {
3217
+ const byTimestamp = /* @__PURE__ */ new Map();
3218
+ for (const series of sortedData) for (const point of series.data) {
3219
+ const date = point.date;
3220
+ if (date instanceof Date && Number.isFinite(date.getTime())) byTimestamp.set(date.getTime(), date);
3221
+ }
3222
+ if (!byTimestamp.size) return null;
3223
+ const min = domain?.[0]?.getTime();
3224
+ const max = domain?.[1]?.getTime();
3225
+ const visible = [...byTimestamp.keys()].sort((a, b) => a - b).filter((timestamp) => (min === void 0 || timestamp >= min) && (max === void 0 || timestamp <= max)).map((timestamp) => byTimestamp.get(timestamp));
3226
+ const axis = {
3227
+ first: min ?? visible[0].getTime(),
3228
+ last: max ?? visible[visible.length - 1].getTime()
3229
+ };
3230
+ const ticks = getBandTickValues(visible, tickFormatter, maxTicks);
3231
+ return isIndexSamplingUnusable(ticks, visible, maxTicks, axis) ? getPositionTickValues(visible, tickFormatter, maxTicks, axis) : ticks;
3232
+ };
2682
3233
  const guessOptimalNumTicks = (data, chartWidth, tickFormatter) => {
2683
3234
  const span = getSpan(data);
2684
3235
  if (!span) return 1;
@@ -2693,7 +3244,43 @@ const guessOptimalNumTicks = (data, chartWidth, tickFormatter) => {
2693
3244
  if (ticks.some((tick, idx) => idx > 0 && tick === ticks[idx - 1])) continue;
2694
3245
  return ticks.length;
2695
3246
  }
2696
- return secondBestGuess;
3247
+ return Math.max(1, secondBestGuess);
3248
+ };
3249
+ //#endregion
3250
+ //#region src/charts/private/time-axis-options.ts
3251
+ const toDateDomain = (domain) => domain ? [new Date(domain[0]), new Date(domain[1])] : void 0;
3252
+ /**
3253
+ * The x-axis options for a continuous time chart, shared by LineChart and AreaChart.
3254
+ *
3255
+ * Kept in one place so the tick-selection logic isn't duplicated between them.
3256
+ *
3257
+ * @param args - Named arguments.
3258
+ * @param args.dataSorted - Series as returned by `useChartDataTransform`.
3259
+ * @param args.width - Chart width in pixels, which bounds how many ticks fit.
3260
+ * @param args.axisOptions - The caller's `options.axis.x`.
3261
+ * @param args.scaleDomain - The caller's `options.xScale.domain`.
3262
+ * @param args.zoomDomain - The chart's current zoom window, when zoomed.
3263
+ * @param args.formatting - Host locale and time zone.
3264
+ * @param args.isSeriesRendered - Whether visx mounts a series, i.e. the legend shows it. Defaults to all of them.
3265
+ * @return Options ready to spread into the chart's `axis.x`; `orientation` is always set.
3266
+ */
3267
+ const buildTimeAxisOptions = ({ dataSorted, width, axisOptions, scaleDomain, zoomDomain, formatting, isSeriesRendered }) => {
3268
+ const { tickResolution, tickFormat, tickValues: callerTickValues, numTicks: callerNumTicks, orientation: callerOrientation, display: callerDisplay, ...rest } = axisOptions ?? {};
3269
+ const rendered = isSeriesRendered ? dataSorted.filter(isSeriesRendered) : dataSorted;
3270
+ const effectiveDomain = zoomDomain ?? toDateDomain(scaleDomain) ?? getSeriesExtent(rendered);
3271
+ const ownFormatter = getFormatter(rendered, tickResolution, formatting, effectiveDomain);
3272
+ const formatter = tickFormat || ownFormatter;
3273
+ const ownTickValues = tickFormat || callerTickValues ? null : getTimeAxisTickValues(rendered, effectiveDomain, ownFormatter, callerNumTicks ?? getMaxTicksForWidth(width));
3274
+ const tickValues = callerTickValues ?? (ownTickValues?.length ? ownTickValues : void 0);
3275
+ const numTicks = callerNumTicks ?? (tickValues ? void 0 : guessOptimalNumTicks(rendered, width, formatter));
3276
+ return {
3277
+ orientation: callerOrientation ?? "bottom",
3278
+ tickFormat: formatter,
3279
+ display: callerDisplay ?? true,
3280
+ ...rest,
3281
+ ...tickValues ? { tickValues } : {},
3282
+ ...numTicks === void 0 ? {} : { numTicks }
3283
+ };
2697
3284
  };
2698
3285
  //#endregion
2699
3286
  //#region src/charts/private/with-responsive/with-responsive.module.scss
@@ -2974,6 +3561,9 @@ function ZoomResetButton({ onClick }) {
2974
3561
  });
2975
3562
  }
2976
3563
  //#endregion
3564
+ //#region src/charts/private/xy-plot/xy-plot.module.scss
3565
+ var xy_plot_module_default = { "xy-plot": "a8ccharts-bIyhgG-xy-plot" };
3566
+ //#endregion
2977
3567
  //#region src/charts/line-chart/line-chart.module.scss
2978
3568
  var line_chart_module_default = {
2979
3569
  "line-chart": "a8ccharts-inuQka-line-chart",
@@ -3214,12 +3804,14 @@ const getVerticalAnchor = (subjectType, isFlippedVertically, y, yMax, height) =>
3214
3804
  };
3215
3805
  const LineChartAnnotation = ({ datum, title, subtitle, subjectType = "circle", styles: datumStyles, testId, renderLabel, renderLabelPopover }) => {
3216
3806
  const providerTheme = useGlobalChartsTheme();
3217
- const scopeElement = useChartScopeElement();
3218
3807
  const { xScale, yScale } = useContext(DataContext) || {};
3219
3808
  const labelRef = useRef(null);
3220
3809
  const [height, setHeight] = useState(null);
3221
- const styles = deepmerge(providerTheme.annotationStyles ?? {}, datumStyles ?? {});
3222
- const resolveColor = (value) => value ? resolveCssVariable(value, scopeElement) ?? value : value;
3810
+ const styles = useMemo(() => deepmerge.all([
3811
+ ANNOTATION_POINTERS,
3812
+ providerTheme.annotationStyles ?? {},
3813
+ datumStyles ?? {}
3814
+ ]), [providerTheme.annotationStyles, datumStyles]);
3223
3815
  useEffect(() => {
3224
3816
  if (labelRef.current?.getBBox) {
3225
3817
  const bbox = labelRef.current.getBBox();
@@ -3305,15 +3897,8 @@ const LineChartAnnotation = ({ datum, title, subtitle, subjectType = "circle", s
3305
3897
  dx,
3306
3898
  dy,
3307
3899
  children: [
3308
- /* @__PURE__ */ jsx(Connector, {
3309
- ...styles?.connector,
3310
- stroke: resolveColor(styles?.connector?.stroke)
3311
- }),
3312
- subjectType === "circle" && /* @__PURE__ */ jsx(CircleSubject, {
3313
- ...styles?.circleSubject,
3314
- fill: resolveColor(styles?.circleSubject?.fill),
3315
- stroke: resolveColor(styles?.circleSubject?.stroke)
3316
- }),
3900
+ /* @__PURE__ */ jsx(Connector, { ...styles?.connector }),
3901
+ subjectType === "circle" && /* @__PURE__ */ jsx(CircleSubject, { ...styles?.circleSubject }),
3317
3902
  subjectType === "line-vertical" && /* @__PURE__ */ jsx(LineSubject, {
3318
3903
  min: yMax,
3319
3904
  max: yMin,
@@ -3347,8 +3932,6 @@ const LineChartAnnotation = ({ datum, title, subtitle, subjectType = "circle", s
3347
3932
  title,
3348
3933
  subtitle,
3349
3934
  ...styles?.label,
3350
- anchorLineStroke: resolveColor(styles?.label?.anchorLineStroke),
3351
- backgroundFill: resolveColor(styles?.label?.backgroundFill),
3352
3935
  ...labelPosition,
3353
3936
  horizontalAnchor: getHorizontalAnchor(subjectType, isFlippedHorizontally),
3354
3937
  verticalAnchor: getVerticalAnchor(subjectType, isFlippedVertically, y, yMax, height ?? ANNOTATION_INIT_HEIGHT)
@@ -3396,16 +3979,34 @@ const toNumber = (val) => {
3396
3979
  const num = typeof val === "number" ? val : parseFloat(val);
3397
3980
  return isNaN(num) ? void 0 : num;
3398
3981
  };
3982
+ const TOOLTIP_DATE = {};
3983
+ const TOOLTIP_FORMAT_BY_RESOLUTION$1 = {
3984
+ hour: {
3985
+ year: "numeric",
3986
+ month: "numeric",
3987
+ day: "numeric",
3988
+ hour: "numeric"
3989
+ },
3990
+ day: TOOLTIP_DATE,
3991
+ month: TOOLTIP_DATE,
3992
+ year: TOOLTIP_DATE
3993
+ };
3994
+ const TooltipDate = ({ date, displayResolution }) => {
3995
+ const formatting = useChartFormatting();
3996
+ const format = useMemo(() => createDateFormatter(TOOLTIP_FORMAT_BY_RESOLUTION$1[displayResolution], formatting), [displayResolution, formatting]);
3997
+ return /* @__PURE__ */ jsx(Fragment$1, { children: date ? format(date) : null });
3998
+ };
3399
3999
  /**
3400
4000
  * Default visx-tooltip render that prints the hovered date as a heading and
3401
4001
  * one row per visible series (label + formatted value), sorted descending by
3402
4002
  * value. Reused by AreaChart, which has the same multi-series shape.
3403
4003
  *
3404
- * @param params - visx `RenderTooltipParams< DataPointDate >`.
4004
+ * @param params - visx tooltip data and the chart's optional `bucketInfo`.
4005
+ * @param contentStyle - Explicit tooltip content color overrides.
3405
4006
  * @return Tooltip JSX, or `null` when no datum is hovered.
3406
4007
  */
3407
- const renderDefaultTooltip = (params) => {
3408
- const { tooltipData } = params;
4008
+ const renderDefaultTooltip = (params, contentStyle) => {
4009
+ const { tooltipData, bucketInfo } = params;
3409
4010
  const nearestDatum = tooltipData?.nearestDatum?.datum;
3410
4011
  if (!nearestDatum) return null;
3411
4012
  const tooltipPoints = Object.entries(tooltipData?.datumByKey || {}).map(([key, { datum }]) => ({
@@ -3414,9 +4015,13 @@ const renderDefaultTooltip = (params) => {
3414
4015
  })).sort((a, b) => b.value - a.value);
3415
4016
  return /* @__PURE__ */ jsxs("div", {
3416
4017
  className: line_chart_module_default["line-chart__tooltip"],
4018
+ style: contentStyle,
3417
4019
  children: [/* @__PURE__ */ jsx("div", {
3418
4020
  className: line_chart_module_default["line-chart__tooltip-date"],
3419
- children: nearestDatum.date?.toLocaleDateString()
4021
+ children: /* @__PURE__ */ jsx(TooltipDate, {
4022
+ date: nearestDatum.date,
4023
+ displayResolution: bucketInfo?.displayResolution ?? "day"
4024
+ })
3420
4025
  }), tooltipPoints.map((point) => /* @__PURE__ */ jsxs(Stack, {
3421
4026
  direction: "row",
3422
4027
  align: "center",
@@ -3460,14 +4065,14 @@ const LineChartScalesRef = ({ chartRef, width, height, margin }) => {
3460
4065
  ]);
3461
4066
  return null;
3462
4067
  };
3463
- 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) => {
3464
4069
  const legendInteractive = legend.interactive ?? false;
3465
4070
  const legendCollapseGroups = legend.collapseGroups ?? false;
3466
4071
  const legendShape = legend.shape ?? "line";
3467
4072
  const legendPosition = legend.position ?? "bottom";
3468
- const providerTheme = useGlobalChartsTheme();
4073
+ const formatting = useChartFormatting();
3469
4074
  const theme = useXYChartTheme(data);
3470
- const resolvedBackgroundColor = theme.backgroundColor ?? providerTheme.backgroundColor;
4075
+ const resolvedBackgroundColor = theme.backgroundColor;
3471
4076
  const chartId = useChartId(providedChartId);
3472
4077
  const hiddenSeries = useDefaultHiddenSeries(chartId, defaultHiddenSeries);
3473
4078
  const isSeriesVisible = useCallback((seriesLabel) => !hiddenSeries.has(seriesLabel), [hiddenSeries]);
@@ -3538,20 +4143,21 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
3538
4143
  setIsNavigating,
3539
4144
  chartRef,
3540
4145
  totalPoints: dataSorted[0]?.data.length || 0,
3541
- onActivate: activateSelectedPoint
4146
+ onActivate: activateSelectedPoint,
4147
+ preventTooltipScroll: tooltipPlacement === "below-axis"
3542
4148
  });
3543
4149
  const chartOptions = useMemo(() => {
3544
- const { tickResolution, tickFormat, ...xAxisOptions } = options?.axis?.x ?? {};
3545
- const formatter = tickFormat || getFormatter(dataSorted, tickResolution);
3546
4150
  return {
3547
4151
  axis: {
3548
- x: {
3549
- orientation: "bottom",
3550
- numTicks: guessOptimalNumTicks(dataSorted, width, formatter),
3551
- tickFormat: formatter,
3552
- display: true,
3553
- ...xAxisOptions
3554
- },
4152
+ x: buildTimeAxisOptions({
4153
+ dataSorted,
4154
+ width,
4155
+ axisOptions: options?.axis?.x,
4156
+ scaleDomain: options?.xScale?.domain,
4157
+ zoomDomain: zoom.domain,
4158
+ formatting,
4159
+ isSeriesRendered: (series) => isSeriesVisible(series.label)
4160
+ }),
3555
4161
  y: {
3556
4162
  orientation: "left",
3557
4163
  numTicks: 4,
@@ -3578,7 +4184,14 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
3578
4184
  dataSorted,
3579
4185
  width,
3580
4186
  zoom.domain,
3581
- stableYDomain
4187
+ stableYDomain,
4188
+ formatting,
4189
+ isSeriesVisible
4190
+ ]);
4191
+ const bucketInfo = useMemo(() => getBucketInfo(dataSorted.filter((series) => isSeriesVisible(series.label)), options?.axis?.x?.tickResolution), [
4192
+ dataSorted,
4193
+ isSeriesVisible,
4194
+ options?.axis?.x?.tickResolution
3582
4195
  ]);
3583
4196
  const tooltipRenderGlyph = useMemo(() => {
3584
4197
  return (props) => {
@@ -3640,6 +4253,29 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
3640
4253
  xAccessor: (d) => d?.date,
3641
4254
  yAccessor: (d) => d?.value
3642
4255
  };
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({
4265
+ ...params,
4266
+ 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
+ ]);
3643
4279
  if (error) return /* @__PURE__ */ jsx("div", {
3644
4280
  className: clsx("line-chart", line_chart_module_default["line-chart"]),
3645
4281
  children: error
@@ -3688,7 +4324,7 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
3688
4324
  onBlur: onChartBlur,
3689
4325
  children: chartHeight > 0 && /* @__PURE__ */ jsxs("div", {
3690
4326
  ref: chartRef,
3691
- style: { position: "relative" },
4327
+ className: xy_plot_module_default["xy-plot"],
3692
4328
  children: [zoomable && zoom.domain && /* @__PURE__ */ jsx(ZoomResetButton, { onClick: zoom.reset }), /* @__PURE__ */ jsxs(XYChart, {
3693
4329
  theme,
3694
4330
  width,
@@ -3778,13 +4414,17 @@ const LineChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
3778
4414
  withTooltips && /* @__PURE__ */ jsx(AccessibleTooltip, {
3779
4415
  detectBounds: true,
3780
4416
  snapTooltipToDatumX: true,
4417
+ tooltipPlacement,
4418
+ style: resolvedTooltipStyle,
3781
4419
  snapTooltipToDatumY: true,
3782
4420
  showSeriesGlyphs: true,
3783
- renderTooltip,
4421
+ renderTooltip: tooltipRenderer,
3784
4422
  renderGlyph: tooltipRenderGlyph,
3785
4423
  glyphStyle,
3786
4424
  showVerticalCrosshair: withTooltipCrosshairs?.showVertical,
3787
4425
  showHorizontalCrosshair: withTooltipCrosshairs?.showHorizontal,
4426
+ verticalCrosshairStyle: withTooltipCrosshairs?.verticalStyle,
4427
+ horizontalCrosshairStyle: withTooltipCrosshairs?.horizontalStyle,
3788
4428
  selectedIndex,
3789
4429
  tooltipRef,
3790
4430
  keyboardFocusedClassName: line_chart_module_default["line-chart__tooltip--keyboard-focused"],
@@ -3918,12 +4558,11 @@ const HoverGlyphs = ({ visibleSeries, stacked, stackOffset, getElementStyles, st
3918
4558
  };
3919
4559
  //#endregion
3920
4560
  //#region src/charts/area-chart/area-chart.tsx
3921
- const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, height, className, margin, withTooltips = true, withTooltipCrosshairs, showLegend = false, legend = {}, stacked = true, stackOffset = "none", smoothing = true, curveType, fillOpacity, withStroke, renderTooltip = renderDefaultTooltip, animation, options = {}, onPointerDown, onPointerUp, onPointerMove, onPointerOut, zoomable = false, rescaleYOnVisibilityChange, rescaleYOnLegendToggle, defaultHiddenSeries, children, gridVisibility, gap = "md" }, ref) => {
4561
+ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, height, className, margin, withTooltips = true, withTooltipCrosshairs, showLegend = false, legend = {}, stacked = true, stackOffset = "none", smoothing = true, curveType, fillOpacity, withStroke, renderTooltip = renderDefaultTooltip, animation, options = {}, onPointerDown, onPointerUp, onPointerMove, onPointerOut, zoomable = false, rescaleYOnVisibilityChange = true, defaultHiddenSeries, children, gridVisibility, gap = "md" }, ref) => {
3922
4562
  const legendInteractive = legend.interactive ?? false;
3923
4563
  const legendShape = legend.shape ?? "rect";
3924
4564
  const legendPosition = legend.position ?? "bottom";
3925
- const rescaleYOnVisibility = rescaleYOnVisibilityChange ?? rescaleYOnLegendToggle ?? true;
3926
- const providerTheme = useGlobalChartsTheme();
4565
+ const formatting = useChartFormatting();
3927
4566
  const theme = useXYChartTheme(data);
3928
4567
  const chartId = useChartId(providedChartId);
3929
4568
  const hiddenSeries = useDefaultHiddenSeries(chartId, defaultHiddenSeries);
@@ -3973,7 +4612,7 @@ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
3973
4612
  totalPoints: dataSorted[0]?.data.length || 0
3974
4613
  });
3975
4614
  const fixedYDomain = useMemo(() => {
3976
- if (rescaleYOnVisibility || !dataSorted.length || !dataSorted[0].data.length || stacked && stackOffset !== "none") return;
4615
+ if (rescaleYOnVisibilityChange || !dataSorted.length || !dataSorted[0].data.length || stacked && stackOffset !== "none") return;
3977
4616
  if (stacked) {
3978
4617
  const numPoints = Math.max(...dataSorted.map((s) => s.data.length));
3979
4618
  let posMax = 0;
@@ -4007,20 +4646,19 @@ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4007
4646
  dataSorted,
4008
4647
  stacked,
4009
4648
  stackOffset,
4010
- rescaleYOnVisibility
4649
+ rescaleYOnVisibilityChange
4011
4650
  ]);
4012
4651
  const chartOptions = useMemo(() => {
4013
- const { tickResolution, tickFormat, ...xAxisOptions } = options?.axis?.x ?? {};
4014
- const formatter = tickFormat || getFormatter(dataSorted, tickResolution);
4015
4652
  return {
4016
4653
  axis: {
4017
- x: {
4018
- orientation: "bottom",
4019
- numTicks: guessOptimalNumTicks(dataSorted, width, formatter),
4020
- tickFormat: formatter,
4021
- display: true,
4022
- ...xAxisOptions
4023
- },
4654
+ x: buildTimeAxisOptions({
4655
+ dataSorted,
4656
+ width,
4657
+ axisOptions: options?.axis?.x,
4658
+ scaleDomain: options?.xScale?.domain,
4659
+ zoomDomain: zoom.domain,
4660
+ formatting
4661
+ }),
4024
4662
  y: {
4025
4663
  orientation: "left",
4026
4664
  numTicks: 4,
@@ -4048,7 +4686,8 @@ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4048
4686
  width,
4049
4687
  stacked,
4050
4688
  fixedYDomain,
4051
- zoom.domain
4689
+ zoom.domain,
4690
+ formatting
4052
4691
  ]);
4053
4692
  const defaultMargin = useChartMargin(height, chartOptions, dataSorted, theme);
4054
4693
  const error = validateData$3(dataSorted);
@@ -4084,9 +4723,17 @@ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4084
4723
  };
4085
4724
  const zeroYAccessor = useCallback(() => 0, []);
4086
4725
  const visibleLabels = useMemo(() => new Set(seriesWithVisibility.filter((s) => s.isVisible).map((s) => s.series.label)), [seriesWithVisibility]);
4726
+ const bucketInfo = useMemo(() => getBucketInfo(dataSorted.filter((series) => visibleLabels.has(series.label)), options?.axis?.x?.tickResolution), [
4727
+ dataSorted,
4728
+ visibleLabels,
4729
+ options?.axis?.x?.tickResolution
4730
+ ]);
4087
4731
  const filteredRenderTooltip = useCallback((params) => {
4088
4732
  const datumByKey = params?.tooltipData?.datumByKey;
4089
- if (!datumByKey) return renderTooltip(params);
4733
+ if (!datumByKey) return renderTooltip({
4734
+ ...params,
4735
+ bucketInfo
4736
+ });
4090
4737
  const filtered = Object.fromEntries(Object.entries(datumByKey).filter(([key]) => visibleLabels.has(key)));
4091
4738
  if (Object.keys(filtered).length === 0) return null;
4092
4739
  const nearestDatum = params?.tooltipData?.nearestDatum;
@@ -4096,13 +4743,18 @@ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4096
4743
  };
4097
4744
  return renderTooltip({
4098
4745
  ...params,
4746
+ bucketInfo,
4099
4747
  tooltipData: {
4100
4748
  ...params.tooltipData,
4101
4749
  datumByKey: filtered,
4102
4750
  nearestDatum: nextNearest
4103
4751
  }
4104
4752
  });
4105
- }, [renderTooltip, visibleLabels]);
4753
+ }, [
4754
+ renderTooltip,
4755
+ visibleLabels,
4756
+ bucketInfo
4757
+ ]);
4106
4758
  const resolvedFillOpacity = fillOpacity ?? (stacked ? .85 : .4);
4107
4759
  const resolvedWithStroke = withStroke ?? !stacked;
4108
4760
  if (error) return /* @__PURE__ */ jsx("div", {
@@ -4177,7 +4829,7 @@ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4177
4829
  onBlur: onChartBlur,
4178
4830
  children: chartHeight > 0 && /* @__PURE__ */ jsxs("div", {
4179
4831
  ref: chartRef,
4180
- style: { position: "relative" },
4832
+ className: xy_plot_module_default["xy-plot"],
4181
4833
  children: [zoomable && zoom.domain && /* @__PURE__ */ jsx(ZoomResetButton, { onClick: zoom.reset }), /* @__PURE__ */ jsxs(XYChart, {
4182
4834
  theme,
4183
4835
  width,
@@ -4233,7 +4885,7 @@ const AreaChartInternal = forwardRef(({ data, chartId: providedChartId, width, h
4233
4885
  stacked,
4234
4886
  stackOffset,
4235
4887
  getElementStyles,
4236
- strokeColor: theme.backgroundColor ?? providerTheme.backgroundColor
4888
+ strokeColor: theme.backgroundColor
4237
4889
  })] }),
4238
4890
  /* @__PURE__ */ jsx(AreaChartScalesRef, {
4239
4891
  chartRef: internalChartRef,
@@ -4401,8 +5053,7 @@ const TOOLTIP_FORMAT_BY_RESOLUTION = {
4401
5053
  year: "numeric",
4402
5054
  month: "long",
4403
5055
  day: "numeric",
4404
- hour: "numeric",
4405
- hour12: true
5056
+ hour: "numeric"
4406
5057
  },
4407
5058
  day: {
4408
5059
  year: "numeric",
@@ -4421,16 +5072,16 @@ const TOOLTIP_FORMAT_BY_RESOLUTION = {
4421
5072
  *
4422
5073
  * @param data - Date-based series, already parsed and sorted by `useChartDataTransform`.
4423
5074
  * @param tickResolution - Caller-declared bucket resolution, when known.
5075
+ * @param formatting - Host locale and time zone.
4424
5076
  * @return Tooltip label formatter.
4425
5077
  */
4426
- const getTooltipFormatter = (data, tickResolution) => {
4427
- if (tickResolution === "week") return (timestamp) => sprintf(__("Week of %s", "jetpack-charts"), new Date(timestamp).toLocaleDateString(void 0, {
4428
- year: "numeric",
4429
- month: "long",
4430
- day: "numeric"
4431
- }));
5078
+ const getTooltipFormatter = (data, tickResolution, formatting) => {
5079
+ if (tickResolution === "week") {
5080
+ const formatDay = createDateFormatter(TOOLTIP_FORMAT_BY_RESOLUTION.day, formatting);
5081
+ return (timestamp) => sprintf(__("Week of %s", "jetpack-charts"), formatDay(timestamp));
5082
+ }
4432
5083
  const format = TOOLTIP_FORMAT_BY_RESOLUTION[getBucketResolution(data, tickResolution)] ?? TOOLTIP_FORMAT_BY_RESOLUTION.day;
4433
- return (timestamp) => new Date(timestamp).toLocaleString(void 0, format);
5084
+ return createDateFormatter(format, formatting);
4434
5085
  };
4435
5086
  const identity = (label) => label;
4436
5087
  const byBucket = (format) => (bucket) => typeof bucket === "string" ? bucket : format(Number(bucket));
@@ -4481,6 +5132,7 @@ const getGroupPadding = (scale) => {
4481
5132
  */
4482
5133
  function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = ALL_RENDERED) {
4483
5134
  const stableOptions = useDeepMemo(options);
5135
+ const formatting = useChartFormatting();
4484
5136
  const axisConfig = useMemo(() => {
4485
5137
  const { labelOverflow: xLabelOverflow, tickResolution: xTickResolution, tickFormat: xTickFormat, ...xAxisOptions } = stableOptions.axis?.x || {};
4486
5138
  const { labelOverflow: yLabelOverflow, tickResolution: yTickResolution, tickFormat: yTickFormat, ...yAxisOptions } = stableOptions.axis?.y || {};
@@ -4507,9 +5159,9 @@ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = A
4507
5159
  zero: false
4508
5160
  };
4509
5161
  const hasLabels = Boolean(data?.[0]?.data?.[0]?.label);
4510
- const timeTickFormatter = hasLabels ? null : getFormatter(data, tickResolution);
5162
+ const timeTickFormatter = hasLabels ? null : getFormatter(data, tickResolution, formatting);
4511
5163
  const labelFormatter = timeTickFormatter ? byBucket(timeTickFormatter) : identity;
4512
- const tooltipDatumFormatter = hasLabels ? labelFormatter : byBucket(getTooltipFormatter(data, tickResolution));
5164
+ const tooltipDatumFormatter = hasLabels ? labelFormatter : byBucket(getTooltipFormatter(data, tickResolution, formatting));
4513
5165
  const valueFormatter = formatNumberCompact;
4514
5166
  const bandDomain = timeTickFormatter ? getBandDomain(data, isSeriesRendered) : null;
4515
5167
  const valueAccessor = (d) => {
@@ -4545,7 +5197,8 @@ function useBarChartOptions(data, horizontal, options = {}, isSeriesRendered = A
4545
5197
  }, [
4546
5198
  data,
4547
5199
  tickResolution,
4548
- isSeriesRendered
5200
+ isSeriesRendered,
5201
+ formatting
4549
5202
  ]);
4550
5203
  return useMemo(() => {
4551
5204
  const { xTickFormat: defaultXTickFormat, yTickFormat: defaultYTickFormat, tooltipLabelFormatter: defaultTooltipLabelFormatter, xAccessor, yAccessor, gridVisibility, xScale: baseXScale, yScale: baseYScale } = defaultOptions[horizontal ? "horizontal" : "vertical"];
@@ -5054,6 +5707,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
5054
5707
  onBlur: onChartBlur,
5055
5708
  children: chartHeight > 0 && /* @__PURE__ */ jsx("div", {
5056
5709
  ref: chartRef,
5710
+ className: xy_plot_module_default["xy-plot"],
5057
5711
  children: /* @__PURE__ */ jsxs(XYChart, {
5058
5712
  theme,
5059
5713
  width,
@@ -5221,7 +5875,7 @@ const BarListChartInternal = ({ data, width, height, options = {}, margin = {
5221
5875
  right: 20,
5222
5876
  bottom: 0,
5223
5877
  top: 0
5224
- }, ...rest }) => {
5878
+ }, className, ...rest }) => {
5225
5879
  const { getElementStyles } = useGlobalChartsContext();
5226
5880
  const tintedData = useMemo(() => {
5227
5881
  if (data.length > 1) return data;
@@ -5273,6 +5927,7 @@ const BarListChartInternal = ({ data, width, height, options = {}, margin = {
5273
5927
  return /* @__PURE__ */ jsx(BarChart, {
5274
5928
  orientation: "horizontal",
5275
5929
  gridVisibility: "none",
5930
+ className: clsx("bar-list-chart", className),
5276
5931
  data: tintedData,
5277
5932
  width,
5278
5933
  height,
@@ -5309,6 +5964,9 @@ const BarListChartResponsive = withResponsive(BarListChart);
5309
5964
  var conversion_funnel_chart_module_default = {
5310
5965
  "bar-container": "a8ccharts-mGEVca-bar-container",
5311
5966
  "change-indicator": "a8ccharts-mGEVca-change-indicator",
5967
+ "change-indicator--negative": "a8ccharts-mGEVca-change-indicator--negative",
5968
+ "change-indicator--positive": "a8ccharts-mGEVca-change-indicator--positive",
5969
+ "conversion-funnel-chart": "a8ccharts-mGEVca-conversion-funnel-chart",
5312
5970
  "conversion-funnel-chart--loading": "a8ccharts-mGEVca-conversion-funnel-chart--loading",
5313
5971
  "empty-state": "a8ccharts-mGEVca-empty-state",
5314
5972
  "funnel-bar": "a8ccharts-mGEVca-funnel-bar",
@@ -5389,23 +6047,14 @@ const useFunnelSelection = (hideTooltip) => {
5389
6047
  */
5390
6048
  const ConversionFunnelChartInternal = ({ mainRate, changeIndicator, steps, loading = false, animation, className, chartId: providedChartId, height, style, renderStepLabel, renderStepRate, renderMainMetric, renderTooltip }) => {
5391
6049
  const chartId = useChartId(providedChartId);
5392
- const { conversionFunnelChart: conversionFunnelChartSettings } = useGlobalChartsTheme();
5393
6050
  const { getElementStyles, isColorPaletteResolved } = useGlobalChartsContext();
5394
6051
  const chartRef = useRef(null);
5395
6052
  const selectedBarRef = useRef(null);
5396
6053
  const [scopeNode, setScopeNode] = useState(null);
5397
6054
  const { tooltipData, tooltipLeft, tooltipTop, tooltipOpen, showTooltip, hideTooltip } = useTooltip();
5398
6055
  const { handleBarClick, handleBarKeyDown, clearSelection, getStepState } = useFunnelSelection(hideTooltip);
5399
- const { containerRef: portalContainerRef, TooltipInPortal, containerBounds } = useTooltipInPortal({
5400
- detectBounds: true,
5401
- scroll: true
5402
- });
5403
- const portalContainerRefRef = useRef(portalContainerRef);
5404
- useEffect(() => {
5405
- portalContainerRefRef.current = portalContainerRef;
5406
- }, [portalContainerRef]);
6056
+ const standaloneScopeClass = useStandaloneScopeClass();
5407
6057
  const setChartRef = useCallback((node) => {
5408
- portalContainerRefRef.current(node);
5409
6058
  chartRef.current = node;
5410
6059
  setScopeNode(node);
5411
6060
  }, []);
@@ -5422,30 +6071,22 @@ const ConversionFunnelChartInternal = ({ mainRate, changeIndicator, steps, loadi
5422
6071
  });
5423
6072
  }, [showTooltip]);
5424
6073
  const getMouseTooltipCoords = useCallback((event) => {
5425
- if (containerBounds.width === 0 || containerBounds.height === 0) return null;
6074
+ const bounds = chartRef.current?.getBoundingClientRect();
6075
+ if (!bounds) return null;
5426
6076
  return {
5427
- x: event.clientX - containerBounds.left,
5428
- y: event.clientY - containerBounds.top
6077
+ x: event.clientX - bounds.left,
6078
+ y: event.clientY - bounds.top
5429
6079
  };
5430
- }, [
5431
- containerBounds.width,
5432
- containerBounds.height,
5433
- containerBounds.left,
5434
- containerBounds.top
5435
- ]);
6080
+ }, []);
5436
6081
  const getKeyboardTooltipCoords = useCallback((event) => {
5437
- if (containerBounds.width === 0 || containerBounds.height === 0) return null;
6082
+ const bounds = chartRef.current?.getBoundingClientRect();
6083
+ if (!bounds) return null;
5438
6084
  const rect = event.currentTarget.getBoundingClientRect();
5439
6085
  return {
5440
- x: rect.left + rect.width / 2 - containerBounds.left,
5441
- y: rect.top - containerBounds.top
6086
+ x: rect.left + rect.width / 2 - bounds.left,
6087
+ y: rect.top - bounds.top
5442
6088
  };
5443
- }, [
5444
- containerBounds.width,
5445
- containerBounds.height,
5446
- containerBounds.left,
5447
- containerBounds.top
5448
- ]);
6089
+ }, []);
5449
6090
  const handleStepInteraction = useCallback((step, event, interactionType) => {
5450
6091
  selectedBarRef.current = event.currentTarget;
5451
6092
  const { isClicked } = getStepState(step.id);
@@ -5506,19 +6147,14 @@ const ConversionFunnelChartInternal = ({ mainRate, changeIndicator, steps, loadi
5506
6147
  };
5507
6148
  }, [clearSelectionAndRef]);
5508
6149
  const resolvedHeight = height ?? style?.height ?? "100%";
5509
- const { primaryColor, backgroundColor, positiveChangeColor, negativeChangeColor } = conversionFunnelChartSettings;
5510
- const { color: barColor } = getElementStyles ? getElementStyles({
5511
- index: 0,
5512
- overrideColor: primaryColor
5513
- }) : { color: primaryColor || "#000000" };
5514
- const changeColor = changeIndicator?.startsWith("+") ? positiveChangeColor : negativeChangeColor;
5515
- const barBackgroundColor = backgroundColor || hexToRgba(barColor, .08) || "rgba(0, 0, 0, 0.08)";
6150
+ const { color: barColor } = getElementStyles ? getElementStyles({ index: 0 }) : { color: "#000000" };
6151
+ const isPositiveChange = changeIndicator?.startsWith("+");
6152
+ const changeColor = isPositiveChange ? CATALOG_POINTERS.trendUp : CATALOG_POINTERS.trendDown;
5516
6153
  const renderDefaultMainMetric = () => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
5517
6154
  className: conversion_funnel_chart_module_default["main-rate"],
5518
6155
  children: formatPercentage(mainRate)
5519
6156
  }), changeIndicator && /* @__PURE__ */ jsx("span", {
5520
- className: conversion_funnel_chart_module_default["change-indicator"],
5521
- style: { color: changeColor },
6157
+ className: clsx(conversion_funnel_chart_module_default["change-indicator"], conversion_funnel_chart_module_default[isPositiveChange ? "change-indicator--positive" : "change-indicator--negative"]),
5522
6158
  children: changeIndicator
5523
6159
  })] });
5524
6160
  const renderDefaultTooltip = (step) => /* @__PURE__ */ jsxs(Stack, {
@@ -5556,7 +6192,7 @@ const ConversionFunnelChartInternal = ({ mainRate, changeIndicator, steps, loadi
5556
6192
  align: "center",
5557
6193
  justify: "center",
5558
6194
  ref: setScopeNode,
5559
- className: clsx(conversion_funnel_chart_module_default["conversion-funnel-chart"], loading && conversion_funnel_chart_module_default["conversion-funnel-chart--loading"], className),
6195
+ className: clsx("conversion-funnel-chart", conversion_funnel_chart_module_default["conversion-funnel-chart"], loading && conversion_funnel_chart_module_default["conversion-funnel-chart--loading"], className),
5560
6196
  style: {
5561
6197
  ...style,
5562
6198
  height: resolvedHeight
@@ -5567,16 +6203,16 @@ const ConversionFunnelChartInternal = ({ mainRate, changeIndicator, steps, loadi
5567
6203
  })
5568
6204
  });
5569
6205
  const maxRate = Math.max(...steps.map((step) => step.rate));
5570
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Stack, {
6206
+ return /* @__PURE__ */ jsxs(Stack, {
5571
6207
  direction: "column",
5572
6208
  gap: "xl",
5573
6209
  ref: setChartRef,
5574
- className: clsx(conversion_funnel_chart_module_default["conversion-funnel-chart"], loading && conversion_funnel_chart_module_default["conversion-funnel-chart--loading"], className),
6210
+ className: clsx("conversion-funnel-chart", conversion_funnel_chart_module_default["conversion-funnel-chart"], loading && conversion_funnel_chart_module_default["conversion-funnel-chart--loading"], className),
5575
6211
  style: {
5576
6212
  ...style,
5577
6213
  height: resolvedHeight
5578
6214
  },
5579
- children: /* @__PURE__ */ jsxs(ChartScopeContext.Provider, {
6215
+ children: [/* @__PURE__ */ jsxs(ChartScopeContext.Provider, {
5580
6216
  value: scopeNode,
5581
6217
  children: [renderMainMetric ? renderMainMetric({
5582
6218
  mainRate,
@@ -5628,7 +6264,6 @@ const ConversionFunnelChartInternal = ({ mainRate, changeIndicator, steps, loadi
5628
6264
  role: "button",
5629
6265
  tabIndex: isBlurred ? -1 : 0,
5630
6266
  "aria-label": step.label,
5631
- style: { backgroundColor: barBackgroundColor },
5632
6267
  children: /* @__PURE__ */ jsx("div", {
5633
6268
  className: clsx(conversion_funnel_chart_module_default["funnel-bar"], { [conversion_funnel_chart_module_default["funnel-bar--animated"]]: animation && !loading && !prefersReducedMotion }),
5634
6269
  style: {
@@ -5640,23 +6275,23 @@ const ConversionFunnelChartInternal = ({ mainRate, changeIndicator, steps, loadi
5640
6275
  }, step.id);
5641
6276
  })
5642
6277
  })]
5643
- })
5644
- }), tooltipOpen && tooltipData && (() => {
5645
- const tooltipContent = renderTooltip ? renderTooltip({
5646
- step: tooltipData,
5647
- index: steps.findIndex((s) => s.id === tooltipData.id),
5648
- top: tooltipTop,
5649
- left: tooltipLeft,
5650
- className: conversion_funnel_chart_module_default["tooltip-wrapper"]
5651
- }) : renderDefaultTooltip(tooltipData);
5652
- if (!tooltipContent) return null;
5653
- return /* @__PURE__ */ jsx(TooltipInPortal, {
5654
- top: tooltipTop,
5655
- left: tooltipLeft,
5656
- className: clsx("a8c-charts-scope", conversion_funnel_chart_module_default["tooltip-wrapper"]),
5657
- children: tooltipContent
5658
- }, Math.random());
5659
- })()] });
6278
+ }), tooltipOpen && tooltipData && (() => {
6279
+ const tooltipContent = renderTooltip ? renderTooltip({
6280
+ step: tooltipData,
6281
+ index: steps.findIndex((s) => s.id === tooltipData.id),
6282
+ top: tooltipTop,
6283
+ left: tooltipLeft,
6284
+ className: conversion_funnel_chart_module_default["tooltip-wrapper"]
6285
+ }) : renderDefaultTooltip(tooltipData);
6286
+ if (!tooltipContent) return null;
6287
+ return /* @__PURE__ */ jsx(BoundedTooltip, {
6288
+ top: tooltipTop,
6289
+ left: tooltipLeft,
6290
+ className: clsx(standaloneScopeClass, conversion_funnel_chart_module_default["tooltip-wrapper"]),
6291
+ children: tooltipContent
6292
+ });
6293
+ })()]
6294
+ });
5660
6295
  };
5661
6296
  /**
5662
6297
  * ConversionFunnelChart component with provider wrapper
@@ -5802,7 +6437,7 @@ function normalizeGeoChartError(eventArgs) {
5802
6437
  * @return A React component displaying an interactive map with data visualization
5803
6438
  */
5804
6439
  const GeoChartInternal = ({ className, data, width, height, region = "world", resolution = "countries", onError, renderPlaceholder }) => {
5805
- const { getElementStyles, theme: { geoChart: { featureFillColor }, backgroundColor } } = useGlobalChartsContext();
6440
+ const { getElementStyles } = useGlobalChartsContext();
5806
6441
  const scopeElement = useChartScopeElement();
5807
6442
  const containerRef = useRef(null);
5808
6443
  const reportedErrorIdsRef = useRef(/* @__PURE__ */ new Set());
@@ -5836,8 +6471,8 @@ const GeoChartInternal = ({ className, data, width, height, region = "world", re
5836
6471
  });
5837
6472
  const fullColorHex = getElementStyles({ index: 0 }).color;
5838
6473
  const lightColorHex = lightenHexColor(fullColorHex, .8);
5839
- const backgroundColorHex = normalizeColorToHex(backgroundColor, scopeElement, resolveCssVariable) || DEFAULT_BACKGROUND_COLOR;
5840
- const defaultFillColorHex = normalizeColorToHex(featureFillColor, scopeElement, resolveCssVariable) || DEFAULT_FEATURE_FILL_COLOR;
6474
+ const backgroundColorHex = normalizeColorToHex(CATALOG_POINTERS.background, scopeElement, resolveCssVariable) || DEFAULT_BACKGROUND_COLOR;
6475
+ const defaultFillColorHex = normalizeColorToHex(CATALOG_POINTERS.surfaceSecondary, scopeElement, resolveCssVariable) || DEFAULT_FEATURE_FILL_COLOR;
5841
6476
  const sanitizedData = useMemo(() => {
5842
6477
  if (data.length === 0) return {
5843
6478
  data,
@@ -5898,8 +6533,7 @@ const GeoChartInternal = ({ className, data, width, height, region = "world", re
5898
6533
  className: clsx("geo-chart", geo_chart_module_default.container, className),
5899
6534
  style: {
5900
6535
  width,
5901
- height,
5902
- backgroundColor
6536
+ height
5903
6537
  },
5904
6538
  children: /* @__PURE__ */ jsx(Chart, {
5905
6539
  chartType: "GeoChart",
@@ -5943,12 +6577,17 @@ var heatmap_chart_module_default = {
5943
6577
  "heatmap-chart__cell--placeholder": "a8ccharts-O3YMOW-heatmap-chart__cell--placeholder",
5944
6578
  "heatmap-chart__cell--selected": "a8ccharts-O3YMOW-heatmap-chart__cell--selected",
5945
6579
  "heatmap-chart__cell--strong": "a8ccharts-O3YMOW-heatmap-chart__cell--strong",
6580
+ "heatmap-chart__cell--summary": "a8ccharts-O3YMOW-heatmap-chart__cell--summary",
5946
6581
  "heatmap-chart__cell-value": "a8ccharts-O3YMOW-heatmap-chart__cell-value",
5947
6582
  "heatmap-chart__col-label": "a8ccharts-O3YMOW-heatmap-chart__col-label",
6583
+ "heatmap-chart__col-label--summary": "a8ccharts-O3YMOW-heatmap-chart__col-label--summary",
5948
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",
5949
6587
  "heatmap-chart__grid": "a8ccharts-O3YMOW-heatmap-chart__grid",
5950
6588
  "heatmap-chart__grid--compact": "a8ccharts-O3YMOW-heatmap-chart__grid--compact",
5951
6589
  "heatmap-chart__grid--height-capped": "a8ccharts-O3YMOW-heatmap-chart__grid--height-capped",
6590
+ "heatmap-chart__legend-label": "a8ccharts-O3YMOW-heatmap-chart__legend-label",
5952
6591
  "heatmap-chart__legend-swatch": "a8ccharts-O3YMOW-heatmap-chart__legend-swatch",
5953
6592
  "heatmap-chart__row": "a8ccharts-O3YMOW-heatmap-chart__row",
5954
6593
  "heatmap-chart__row-label": "a8ccharts-O3YMOW-heatmap-chart__row-label",
@@ -5958,17 +6597,21 @@ var heatmap_chart_module_default = {
5958
6597
  //#region src/charts/heatmap-chart/private/use-heatmap-colors.ts
5959
6598
  const isPresent = (value) => value !== null && value !== void 0 && !isNaN(value);
5960
6599
  /**
5961
- * 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.
5962
6602
  * @param data - The heatmap columns
5963
6603
  * @return Tuple of [min, max] values
5964
6604
  */
5965
6605
  const getValueExtent = (data) => {
5966
6606
  let min = Infinity;
5967
6607
  let max = -Infinity;
5968
- for (const column of data) for (const cell of column.data) {
5969
- if (!isPresent(cell.value)) continue;
5970
- if (cell.value < min) min = cell.value;
5971
- 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
+ }
5972
6615
  }
5973
6616
  if (min === Infinity) return [0, 0];
5974
6617
  return [min, max];
@@ -5989,6 +6632,119 @@ const getNormalizedValue = (value, extent) => {
5989
6632
  return Math.min(1, Math.max(0, (value - min) / (max - min)));
5990
6633
  };
5991
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
5992
6748
  //#region src/charts/heatmap-chart/private/build-calendar-data.ts
5993
6749
  /** Rows that get a weekday label (Mon, Wed, Fri with a Monday week start). */
5994
6750
  const LABELLED_ROWS = [
@@ -6003,69 +6759,88 @@ const LABELLED_ROWS = [
6003
6759
  * @param bound - Requested bound as `yyyy-MM-dd`, if any.
6004
6760
  * @param fallback - The series' own bound on this side.
6005
6761
  * @param direction - Which way the bound is allowed to move.
6006
- * @return The bound to draw to.
6762
+ * @return The day key to draw to.
6007
6763
  */
6008
6764
  const widenTo = (bound, fallback, direction) => {
6009
- if (!bound) return fallback;
6010
- const parsed = parseISO(bound);
6011
- if (isNaN(parsed.getTime())) return fallback;
6012
- if (direction === "earlier") return parsed < fallback ? parsed : fallback;
6013
- return parsed > fallback ? parsed : fallback;
6014
- };
6015
- const toDate = (point) => {
6016
- if (point.date instanceof Date && !isNaN(point.date.getTime())) return point.date;
6017
- if (point.dateString) {
6018
- const parsed = parseISO(point.dateString);
6019
- 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;
6020
6769
  }
6021
- return null;
6770
+ if (direction === "earlier") return key < fallback ? key : fallback;
6771
+ return key > fallback ? key : fallback;
6022
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
+ */
6023
6780
  const buildCalendarHeatmapData = (series, options = {}) => {
6024
6781
  const weekStartsOn = options.weekStartsOn ?? 1;
6025
6782
  const hideOutOfRangeDays = options.hideOutOfRangeDays ?? true;
6026
- const entries = series.map((point) => ({
6027
- date: toDate(point),
6028
- value: point.value
6029
- })).filter((entry) => entry.date !== null);
6030
- 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 {
6031
6803
  data: [],
6032
6804
  rowLabels: []
6033
6805
  };
6034
- const valueByDay = /* @__PURE__ */ new Map();
6035
- let minDate = entries[0].date;
6036
- let maxDate = entries[0].date;
6037
- for (const { date, value } of entries) {
6038
- valueByDay.set(format(date, "yyyy-MM-dd"), value);
6039
- if (date < minDate) minDate = date;
6040
- if (date > maxDate) maxDate = date;
6041
- }
6042
- const minDayKey = format(minDate, "yyyy-MM-dd");
6043
- const maxDayKey = format(maxDate, "yyyy-MM-dd");
6044
- const gridMinDate = widenTo(options.gridSpan?.start, minDate, "earlier");
6045
- const gridMaxDate = widenTo(options.gridSpan?.end, maxDate, "later");
6046
- const gridMaxDayKey = format(gridMaxDate, "yyyy-MM-dd");
6047
- const gridStart = startOfWeek(gridMinDate, { weekStartsOn });
6048
- const gridMinDayKey = format(gridMinDate < minDate ? gridStart : gridMinDate, "yyyy-MM-dd");
6049
- const weekCount = differenceInCalendarWeeks(gridMaxDate, gridStart, { weekStartsOn }) + 1;
6050
- 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)) : "");
6051
6826
  const MIN_FIRST_MONTH_WEEKS = 2;
6052
- const firstMonth = gridStart.getMonth();
6827
+ const firstMonth = gridStart.getUTCMonth();
6053
6828
  let firstMonthWeeks = 0;
6054
- while (firstMonthWeeks < weekCount && addDays(gridStart, firstMonthWeeks * 7).getMonth() === firstMonth) firstMonthWeeks++;
6829
+ while (firstMonthWeeks < weekCount && addCivilDays(gridStart, firstMonthWeeks * 7).getUTCMonth() === firstMonth) firstMonthWeeks++;
6055
6830
  const showFirstMonthLabel = !(firstMonthWeeks < weekCount) || firstMonthWeeks >= MIN_FIRST_MONTH_WEEKS;
6056
6831
  const data = [];
6057
6832
  let previousMonth = -1;
6058
6833
  for (let week = 0; week < weekCount; week++) {
6059
- const columnStart = addDays(gridStart, week * 7);
6060
- const month = columnStart.getMonth();
6061
- 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) : "";
6062
6837
  previousMonth = month;
6063
6838
  const cells = [];
6064
6839
  for (let row = 0; row < 7; row++) {
6065
- const day = addDays(gridStart, week * 7 + row);
6066
- const key = format(day, "yyyy-MM-dd");
6840
+ const day = addCivilDays(gridStart, week * 7 + row);
6841
+ const key = civilKey(day);
6067
6842
  const cell = {
6068
- label: format(day, "EEE, MMM d, yyyy"),
6843
+ label: formatDay(day),
6069
6844
  value: valueByDay.has(key) ? valueByDay.get(key) : null
6070
6845
  };
6071
6846
  if (key < gridMinDayKey || key > gridMaxDayKey) {
@@ -6091,7 +6866,7 @@ const HeatmapContext = createContext(null);
6091
6866
  //#region src/charts/heatmap-chart/private/heatmap-legend.tsx
6092
6867
  const HeatmapLegend = ({ steps = 5, lessLabel, moreLabel }) => {
6093
6868
  const context = useContext(HeatmapContext);
6094
- const { legend, backgroundColor } = useGlobalChartsTheme();
6869
+ const { legend } = useGlobalChartsTheme();
6095
6870
  if (!context) return null;
6096
6871
  const { primaryColorHex } = context;
6097
6872
  const labelStyle = legend.labelStyles;
@@ -6102,6 +6877,7 @@ const HeatmapLegend = ({ steps = 5, lessLabel, moreLabel }) => {
6102
6877
  children: [
6103
6878
  /* @__PURE__ */ jsx(Text$1, {
6104
6879
  variant: "body-sm",
6880
+ className: heatmap_chart_module_default["heatmap-chart__legend-label"],
6105
6881
  style: labelStyle,
6106
6882
  children: lessLabel ?? __("Less", "jetpack-charts")
6107
6883
  }),
@@ -6115,7 +6891,6 @@ const HeatmapLegend = ({ steps = 5, lessLabel, moreLabel }) => {
6115
6891
  className: heatmap_chart_module_default["heatmap-chart__legend-swatch"],
6116
6892
  style: {
6117
6893
  "--a8c-charts-color-heatmap-primary": primaryColorHex,
6118
- "--a8c-charts-color-heatmap-background": backgroundColor,
6119
6894
  "--a8c-charts-heatmap-cell-intensity": intensity
6120
6895
  }
6121
6896
  }, index);
@@ -6123,6 +6898,7 @@ const HeatmapLegend = ({ steps = 5, lessLabel, moreLabel }) => {
6123
6898
  }),
6124
6899
  /* @__PURE__ */ jsx(Text$1, {
6125
6900
  variant: "body-sm",
6901
+ className: heatmap_chart_module_default["heatmap-chart__legend-label"],
6126
6902
  style: labelStyle,
6127
6903
  children: moreLabel ?? __("More", "jetpack-charts")
6128
6904
  })
@@ -6132,7 +6908,8 @@ const HeatmapLegend = ({ steps = 5, lessLabel, moreLabel }) => {
6132
6908
  //#endregion
6133
6909
  //#region src/charts/heatmap-chart/heatmap-chart.tsx
6134
6910
  const CELL_MIX_FLOOR = .15;
6135
- 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 }) => {
6136
6913
  const chartId = useChartId(providedChartId);
6137
6914
  const { getElementStyles, theme } = useGlobalChartsContext();
6138
6915
  const scopeElement = useChartScopeElement();
@@ -6140,17 +6917,14 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6140
6917
  const { nonLegendChildren } = useChartChildren(children, "HeatmapChart");
6141
6918
  const [selectedIndex, setSelectedIndex] = useState();
6142
6919
  const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, showTooltip, hideTooltip } = useTooltip();
6143
- const { containerRef, containerBounds, TooltipInPortal } = useTooltipInPortal({
6144
- detectBounds: true,
6145
- scroll: true
6146
- });
6147
- const containerBoundsRef = useRef(containerBounds);
6148
- containerBoundsRef.current = containerBounds;
6920
+ const standaloneScopeClass = useStandaloneScopeClass();
6921
+ const containerRef = useRef(null);
6922
+ const getTooltipOrigin = useCallback(() => containerRef.current?.closest(`[data-chart-id="heatmap-chart-${chartId}"]`)?.getBoundingClientRect() ?? null, [chartId]);
6149
6923
  const { color: primaryColorHex } = getElementStyles({
6150
6924
  index: 0,
6151
- overrideColor: primaryColor || heatmapChartSettings.primaryColor
6925
+ overrideColor: primaryColor
6152
6926
  });
6153
- const chartBackgroundHex = normalizeColorToHex(theme.backgroundColor, scopeElement, resolveCssVariable);
6927
+ const chartBackgroundHex = normalizeColorToHex(CATALOG_POINTERS.background, scopeElement, resolveCssVariable);
6154
6928
  const primaryHex = normalizeColorToHex(primaryColorHex);
6155
6929
  const cellHasLightText = (intensity) => isValidHexColor(primaryHex) && isValidHexColor(chartBackgroundHex) && prefersLightText(mixHexColors(primaryHex, chartBackgroundHex, 1 - (CELL_MIX_FLOOR + .85 * intensity)));
6156
6930
  const extent = useMemo(() => getValueExtent(data), [data]);
@@ -6226,19 +7000,21 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6226
7000
  ]);
6227
7001
  const handleCellMouseMove = useCallback((event) => {
6228
7002
  if (!withTooltips) return;
7003
+ const origin = getTooltipOrigin();
7004
+ if (!origin) return;
6229
7005
  const target = event.currentTarget;
6230
7006
  const columnIndex = Number(target.dataset.column);
6231
7007
  const rowIndex = Number(target.dataset.row);
6232
- const bounds = containerBoundsRef.current;
6233
7008
  showTooltip({
6234
- tooltipLeft: event.clientX - bounds.left,
6235
- tooltipTop: event.clientY - bounds.top,
7009
+ tooltipLeft: event.clientX - origin.left,
7010
+ tooltipTop: event.clientY - origin.top,
6236
7011
  tooltipData: buildTooltipData(columnIndex, rowIndex)
6237
7012
  });
6238
7013
  }, [
6239
7014
  withTooltips,
6240
7015
  showTooltip,
6241
- buildTooltipData
7016
+ buildTooltipData,
7017
+ getTooltipOrigin
6242
7018
  ]);
6243
7019
  const handleCellMouseLeave = useCallback(() => {
6244
7020
  if (withTooltips && selectedIndex === void 0) hideTooltip();
@@ -6249,13 +7025,14 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6249
7025
  ]);
6250
7026
  useEffect(() => {
6251
7027
  if (!withTooltips || selectedIndex === void 0) return;
7028
+ const origin = getTooltipOrigin();
7029
+ if (!origin) return;
6252
7030
  const col = Math.floor(selectedIndex / rows);
6253
7031
  const row = selectedIndex % rows;
6254
7032
  const rect = (typeof document !== "undefined" ? document.getElementById(`${chartId}-cell-${col}-${row}`) : null)?.getBoundingClientRect();
6255
- const bounds = containerBoundsRef.current;
6256
7033
  showTooltip({
6257
- tooltipLeft: rect ? rect.left + rect.width / 2 - bounds.left : 0,
6258
- tooltipTop: rect ? rect.top + rect.height / 2 - bounds.top : 0,
7034
+ tooltipLeft: rect ? rect.left + rect.width / 2 - origin.left : 0,
7035
+ tooltipTop: rect ? rect.top + rect.height / 2 - origin.top : 0,
6259
7036
  tooltipData: buildTooltipData(col, row)
6260
7037
  });
6261
7038
  }, [
@@ -6264,7 +7041,8 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6264
7041
  rows,
6265
7042
  chartId,
6266
7043
  buildTooltipData,
6267
- showTooltip
7044
+ showTooltip,
7045
+ getTooltipOrigin
6268
7046
  ]);
6269
7047
  const defaultRenderTooltip = useCallback((info) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("strong", { children: info.cellLabel || `${info.columnLabel ?? ""} ${info.rowLabel ?? ""}`.trim() }), /* @__PURE__ */ jsx("div", { children: info.value === null ? __("No data", "jetpack-charts") : formatNumber(info.value) })] }), []);
6270
7048
  if (!columns || !rows) return /* @__PURE__ */ jsx(Center, {
@@ -6282,14 +7060,20 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6282
7060
  const rowTrack = compact ? "var(--a8c-charts-dimension-heatmap-cell-size)" : `minmax(${minCellHeight ?? 0}px, ${maxCellHeight ? `${maxCellHeight}px` : "1fr"})`;
6283
7061
  const gridStyle = {
6284
7062
  "--a8c-charts-color-heatmap-primary": primaryColorHex,
6285
- "--a8c-charts-color-heatmap-background": theme.backgroundColor,
6286
- 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})`}`,
6287
7064
  gridTemplateRows: `auto repeat(${rows}, ${rowTrack})`
6288
7065
  };
6289
7066
  if (compact) {
6290
7067
  gridStyle["--a8c-charts-dimension-heatmap-cell-gap"] = `${compactCellGap}px`;
6291
7068
  gridStyle["--a8c-charts-dimension-heatmap-cell-size"] = `${compactCellSize}px`;
6292
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
+ };
6293
7077
  const activeDescendant = selectedIndex !== void 0 ? `${chartId}-cell-${Math.floor(selectedIndex / rows)}-${selectedIndex % rows}` : void 0;
6294
7078
  const heightCapped = !compact && Boolean(maxCellHeight);
6295
7079
  return /* @__PURE__ */ jsx(HeatmapContext.Provider, {
@@ -6327,7 +7111,10 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6327
7111
  "aria-hidden": "true",
6328
7112
  className: heatmap_chart_module_default["heatmap-chart__row"],
6329
7113
  children: [/* @__PURE__ */ jsx("span", {}), data.map((column, columnIndex) => /* @__PURE__ */ jsx("span", {
6330
- 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
+ }),
6331
7118
  children: column.label
6332
7119
  }, `col-${columnIndex}`))]
6333
7120
  }), Array.from({ length: rows }).map((_row, rowIndex) => {
@@ -6352,7 +7139,8 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6352
7139
  }, `cell-${columnIndex}-${rowIndex}`);
6353
7140
  const value = cell?.value ?? null;
6354
7141
  const present = isPresent(value);
6355
- const normalized = present ? getNormalizedValue(value, extent) : 0;
7142
+ const filled = present && !column.summary;
7143
+ const normalized = filled ? getNormalizedValue(value, extent) : 0;
6356
7144
  const flatIndex = columnIndex * rows + rowIndex;
6357
7145
  const info = buildTooltipData(columnIndex, rowIndex);
6358
7146
  const accessibleLabel = `${info.cellLabel || `${info.columnLabel ?? ""} ${info.rowLabel ?? ""}`.trim()}: ${info.value === null ? __("No data", "jetpack-charts") : formatNumber(info.value)}`;
@@ -6365,14 +7153,16 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6365
7153
  "data-column": columnIndex,
6366
7154
  "data-row": rowIndex,
6367
7155
  className: clsx(heatmap_chart_module_default["heatmap-chart__cell"], {
6368
- [heatmap_chart_module_default["heatmap-chart__cell--filled"]]: present,
6369
- [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),
6370
7160
  [heatmap_chart_module_default["heatmap-chart__cell--selected"]]: selectedIndex === flatIndex
6371
7161
  }),
6372
- style: present ? { "--a8c-charts-heatmap-cell-intensity": normalized } : void 0,
7162
+ style: filled ? { "--a8c-charts-heatmap-cell-intensity": normalized } : void 0,
6373
7163
  onMouseMove: handleCellMouseMove,
6374
7164
  onMouseLeave: handleCellMouseLeave,
6375
- children: drawValues && present && /* @__PURE__ */ jsx("span", {
7165
+ children: (drawValues || column.summary) && present && /* @__PURE__ */ jsx("span", {
6376
7166
  className: heatmap_chart_module_default["heatmap-chart__cell-value"],
6377
7167
  children: formatNumberCompact(value)
6378
7168
  })
@@ -6380,11 +7170,11 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6380
7170
  })]
6381
7171
  }, `row-${rowIndex}`);
6382
7172
  })]
6383
- }), withTooltips && tooltipOpen && tooltipData && /* @__PURE__ */ jsx(TooltipInPortal, {
7173
+ }), withTooltips && tooltipOpen && tooltipData && /* @__PURE__ */ jsx(BoundedTooltip, {
6384
7174
  top: tooltipTop,
6385
7175
  left: tooltipLeft,
6386
7176
  children: /* @__PURE__ */ jsx("div", {
6387
- className: "a8c-charts-scope",
7177
+ className: standaloneScopeClass,
6388
7178
  role: "tooltip",
6389
7179
  tabIndex: -1,
6390
7180
  children: (renderTooltip ?? defaultRenderTooltip)(tooltipData)
@@ -6408,7 +7198,43 @@ const HeatmapChartResponsiveInner = (props) => /* @__PURE__ */ jsx(HeatmapChartW
6408
7198
  HeatmapChartResponsiveInner.displayName = "HeatmapChart";
6409
7199
  const HeatmapChartResponsive = attachSubComponents(withResponsive(HeatmapChartResponsiveInner), { Legend: HeatmapLegend });
6410
7200
  //#endregion
6411
- //#region ../../../node_modules/.pnpm/is-plain-object@5.0.0/node_modules/is-plain-object/dist/is-plain-object.mjs
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
7237
+ //#region ../../../node_modules/.pnpm/is-plain-object@5.1.0/node_modules/is-plain-object/dist/is-plain-object.mjs
6412
7238
  /*!
6413
7239
  * is-plain-object <https://github.com/jonschlinkert/is-plain-object>
6414
7240
  *
@@ -6475,7 +7301,7 @@ function paramCase(input, options) {
6475
7301
  return dotCase(input, __assign({ delimiter: "-" }, options));
6476
7302
  }
6477
7303
  //#endregion
6478
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
6479
7305
  function useUpdateEffect(effect, deps) {
6480
7306
  const mountedRef = useRef$1(false);
6481
7307
  useEffect$1(() => {
@@ -8045,7 +8871,7 @@ _createEmotion.css;
8045
8871
  _createEmotion.sheet;
8046
8872
  _createEmotion.cache;
8047
8873
  //#endregion
8048
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
8049
8875
  var isSerializedStyles = (o) => typeof o !== "undefined" && o !== null && ["name", "styles"].every((p) => typeof o[p] !== "undefined");
8050
8876
  var useCx = () => {
8051
8877
  const cache = __unsafe_useEmotionCache();
@@ -8152,7 +8978,7 @@ function memize(fn, options) {
8152
8978
  return memoized;
8153
8979
  }
8154
8980
  //#endregion
8155
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
8156
8982
  var white = "#fff";
8157
8983
  var GRAY = {
8158
8984
  900: "#1e1e1e",
@@ -8231,7 +9057,7 @@ var COLORS = Object.freeze({
8231
9057
  ui: UI
8232
9058
  });
8233
9059
  //#endregion
8234
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
8235
9061
  var CONTROL_HEIGHT = "36px";
8236
9062
  var CONTROL_PROPS = {
8237
9063
  controlPaddingX: 12,
@@ -8246,9 +9072,6 @@ var CONTROL_PROPS = {
8246
9072
  };
8247
9073
  var config_values_default = Object.assign({}, CONTROL_PROPS, {
8248
9074
  colorDivider: "rgba(0, 0, 0, 0.1)",
8249
- colorScrollbarThumb: "rgba(0, 0, 0, 0.2)",
8250
- colorScrollbarThumbHover: "rgba(0, 0, 0, 0.5)",
8251
- colorScrollbarTrack: "rgba(0, 0, 0, 0.04)",
8252
9075
  elevationIntensity: 1,
8253
9076
  radiusXSmall: "1px",
8254
9077
  radiusSmall: "2px",
@@ -8259,7 +9082,6 @@ var config_values_default = Object.assign({}, CONTROL_PROPS, {
8259
9082
  borderWidth: "1px",
8260
9083
  borderWidthFocus: "1.5px",
8261
9084
  borderWidthTab: "4px",
8262
- spinnerSize: 16,
8263
9085
  fontSize: "13px",
8264
9086
  fontSizeH1: "calc(2.44 * 13px)",
8265
9087
  fontSizeH2: "calc(1.95 * 13px)",
@@ -8336,10 +9158,10 @@ var require_es6 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8336
9158
  };
8337
9159
  }));
8338
9160
  //#endregion
8339
- //#region ../../../node_modules/.pnpm/@wordpress+warning@3.53.0/node_modules/@wordpress/warning/build-module/utils.mjs
9161
+ //#region ../../../node_modules/.pnpm/@wordpress+warning@3.54.0/node_modules/@wordpress/warning/build-module/utils.mjs
8340
9162
  var logged = /* @__PURE__ */ new Set();
8341
9163
  //#endregion
8342
- //#region ../../../node_modules/.pnpm/@wordpress+warning@3.53.0/node_modules/@wordpress/warning/build-module/index.mjs
9164
+ //#region ../../../node_modules/.pnpm/@wordpress+warning@3.54.0/node_modules/@wordpress/warning/build-module/index.mjs
8343
9165
  function isDev() {
8344
9166
  return globalThis.SCRIPT_DEBUG === true;
8345
9167
  }
@@ -8353,7 +9175,7 @@ function warning(message) {
8353
9175
  logged.add(message);
8354
9176
  }
8355
9177
  //#endregion
8356
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
8357
9179
  var import_es6 = /* @__PURE__ */ __toESM(require_es6(), 1);
8358
9180
  var ComponentsContext = createContext$1(
8359
9181
  /** @type {Record<string, any>} */
@@ -8380,18 +9202,18 @@ var BaseContextSystemProvider = ({ children, value }) => {
8380
9202
  };
8381
9203
  memo(BaseContextSystemProvider);
8382
9204
  //#endregion
8383
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
8384
9206
  var COMPONENT_NAMESPACE = "data-wp-component";
8385
9207
  var CONNECTED_NAMESPACE = "data-wp-c16t";
8386
9208
  var CONNECT_STATIC_NAMESPACE = "__contextSystemKey__";
8387
9209
  //#endregion
8388
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
8389
9211
  function getStyledClassName(namespace) {
8390
9212
  return `components-${paramCase(namespace)}`;
8391
9213
  }
8392
9214
  var getStyledClassNameFromKey = memize(getStyledClassName);
8393
9215
  //#endregion
8394
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
8395
9217
  function contextConnect(Component, namespace) {
8396
9218
  return _contextConnect(Component, namespace, { forwardsRef: true });
8397
9219
  }
@@ -8408,7 +9230,7 @@ function _contextConnect(Component, namespace, options) {
8408
9230
  });
8409
9231
  }
8410
9232
  //#endregion
8411
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
8412
9234
  function getNamespace(componentName) {
8413
9235
  return { [COMPONENT_NAMESPACE]: componentName };
8414
9236
  }
@@ -8416,7 +9238,7 @@ function getConnectedNamespace() {
8416
9238
  return { [CONNECTED_NAMESPACE]: true };
8417
9239
  }
8418
9240
  //#endregion
8419
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
8420
9242
  function useContextSystem(props, namespace) {
8421
9243
  const contextSystemProps = useComponentsContext();
8422
9244
  if (typeof namespace === "undefined") globalThis.SCRIPT_DEBUG === true && warning("useContextSystem: Please provide a namespace");
@@ -8436,7 +9258,7 @@ function useContextSystem(props, namespace) {
8436
9258
  return finalComponentProps;
8437
9259
  }
8438
9260
  //#endregion
8439
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
8440
9262
  var visuallyHidden = {
8441
9263
  border: 0,
8442
9264
  clip: "rect(1px, 1px, 1px, 1px)",
@@ -8452,7 +9274,7 @@ var visuallyHidden = {
8452
9274
  wordBreak: "normal"
8453
9275
  };
8454
9276
  //#endregion
8455
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
8456
9278
  var customAttributeRegExp = /^(data|aria|x)-/i;
8457
9279
  var eventHandlerRegExp = /^on[A-Z]/;
8458
9280
  var svgElementNames = new Set(`animate animateMotion animateTransform circle clipPath defs desc ellipse
@@ -8542,7 +9364,7 @@ function UnforwardedPolymorphicElement({ as, ...props }, ref) {
8542
9364
  }
8543
9365
  var PolymorphicElement = forwardRef$1(UnforwardedPolymorphicElement);
8544
9366
  //#endregion
8545
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
8546
9368
  function UnforwardedView({ css, ...restProps }, ref) {
8547
9369
  return /* @__PURE__ */ jsx(PolymorphicElement, {
8548
9370
  ref,
@@ -8551,7 +9373,7 @@ function UnforwardedView({ css, ...restProps }, ref) {
8551
9373
  }
8552
9374
  var component_default$2 = Object.assign(forwardRef$1(UnforwardedView), { selector: ".components-view" });
8553
9375
  //#endregion
8554
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
8555
9377
  function UnconnectedVisuallyHidden(props, forwardedRef) {
8556
9378
  const { style: styleProp, ...contextProps } = useContextSystem(props, "VisuallyHidden");
8557
9379
  return /* @__PURE__ */ jsx(component_default$2, {
@@ -8566,7 +9388,7 @@ function UnconnectedVisuallyHidden(props, forwardedRef) {
8566
9388
  }
8567
9389
  var component_default$1 = contextConnect(UnconnectedVisuallyHidden, "VisuallyHidden");
8568
9390
  //#endregion
8569
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
8570
9392
  var breakpoints = [
8571
9393
  "40em",
8572
9394
  "52em",
@@ -8600,7 +9422,7 @@ function useResponsiveValue(values, options = {}) {
8600
9422
  return array[index >= array.length ? array.length - 1 : index];
8601
9423
  }
8602
9424
  //#endregion
8603
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
8604
9426
  var ALIGNMENTS = {
8605
9427
  bottom: {
8606
9428
  alignItems: "flex-end",
@@ -8648,7 +9470,7 @@ function getAlignmentProps(alignment) {
8648
9470
  return alignment ? ALIGNMENTS[alignment] : {};
8649
9471
  }
8650
9472
  //#endregion
8651
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
8652
9474
  function useGrid(props) {
8653
9475
  const { align, alignment, className, columnGap, columns = 2, gap = 3, isInline = false, justify, rowGap, rows, templateColumns, templateRows, ...otherProps } = useContextSystem(props, "Grid");
8654
9476
  const column = useResponsiveValue(Array.isArray(columns) ? columns : [columns]);
@@ -8669,7 +9491,7 @@ function useGrid(props) {
8669
9491
  justifyContent: justify,
8670
9492
  verticalAlign: isInline ? "middle" : void 0,
8671
9493
  ...alignmentProps
8672
- }, process.env.NODE_ENV === "production" ? "" : ";label:gridClasses;", process.env.NODE_ENV === "production" ? "" : "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImhvb2sudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBdURzQiIsImZpbGUiOiJob29rLnRzIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBFeHRlcm5hbCBkZXBlbmRlbmNpZXNcbiAqL1xuaW1wb3J0IHsgY3NzIH0gZnJvbSAnQGVtb3Rpb24vcmVhY3QnO1xuXG4vKipcbiAqIFdvcmRQcmVzcyBkZXBlbmRlbmNpZXNcbiAqL1xuaW1wb3J0IHsgdXNlTWVtbyB9IGZyb20gJ0B3b3JkcHJlc3MvZWxlbWVudCc7XG5cbi8qKlxuICogSW50ZXJuYWwgZGVwZW5kZW5jaWVzXG4gKi9cbmltcG9ydCB0eXBlIHsgV29yZFByZXNzQ29tcG9uZW50UHJvcHMgfSBmcm9tICcuLi9jb250ZXh0JztcbmltcG9ydCB7IHVzZUNvbnRleHRTeXN0ZW0gfSBmcm9tICcuLi9jb250ZXh0JztcbmltcG9ydCB7IGdldEFsaWdubWVudFByb3BzIH0gZnJvbSAnLi91dGlscyc7XG5pbXBvcnQgeyB1c2VSZXNwb25zaXZlVmFsdWUgfSBmcm9tICcuLi91dGlscy91c2UtcmVzcG9uc2l2ZS12YWx1ZSc7XG5pbXBvcnQgQ09ORklHIGZyb20gJy4uL3V0aWxzL2NvbmZpZy12YWx1ZXMnO1xuaW1wb3J0IHsgdXNlQ3ggfSBmcm9tICcuLi91dGlscy9ob29rcy91c2UtY3gnO1xuaW1wb3J0IHR5cGUgeyBHcmlkUHJvcHMgfSBmcm9tICcuL3R5cGVzJztcblxuZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gdXNlR3JpZChcblx0cHJvcHM6IFdvcmRQcmVzc0NvbXBvbmVudFByb3BzPCBHcmlkUHJvcHMsICdkaXYnID5cbikge1xuXHRjb25zdCB7XG5cdFx0YWxpZ24sXG5cdFx0YWxpZ25tZW50LFxuXHRcdGNsYXNzTmFtZSxcblx0XHRjb2x1bW5HYXAsXG5cdFx0Y29sdW1ucyA9IDIsXG5cdFx0Z2FwID0gMyxcblx0XHRpc0lubGluZSA9IGZhbHNlLFxuXHRcdGp1c3RpZnksXG5cdFx0cm93R2FwLFxuXHRcdHJvd3MsXG5cdFx0dGVtcGxhdGVDb2x1bW5zLFxuXHRcdHRlbXBsYXRlUm93cyxcblx0XHQuLi5vdGhlclByb3BzXG5cdH0gPSB1c2VDb250ZXh0U3lzdGVtKCBwcm9wcywgJ0dyaWQnICk7XG5cblx0Y29uc3QgY29sdW1uc0FzQXJyYXkgPSBBcnJheS5pc0FycmF5KCBjb2x1bW5zICkgPyBjb2x1bW5zIDogWyBjb2x1bW5zIF07XG5cdGNvbnN0IGNvbHVtbiA9IHVzZVJlc3BvbnNpdmVWYWx1ZSggY29sdW1uc0FzQXJyYXkgKTtcblx0Y29uc3Qgcm93c0FzQXJyYXkgPSBBcnJheS5pc0FycmF5KCByb3dzICkgPyByb3dzIDogWyByb3dzIF07XG5cdGNvbnN0IHJvdyA9IHVzZVJlc3BvbnNpdmVWYWx1ZSggcm93c0FzQXJyYXkgKTtcblxuXHRjb25zdCBncmlkVGVtcGxhdGVDb2x1bW5zID1cblx0XHR0ZW1wbGF0ZUNvbHVtbnMgfHwgKCAhISBjb2x1bW5zICYmIGByZXBlYXQoICR7IGNvbHVtbiB9LCAxZnIgKWAgKTtcblx0Y29uc3QgZ3JpZFRlbXBsYXRlUm93cyA9XG5cdFx0dGVtcGxhdGVSb3dzIHx8ICggISEgcm93cyAmJiBgcmVwZWF0KCAkeyByb3cgfSwgMWZyIClgICk7XG5cblx0Y29uc3QgY3ggPSB1c2VDeCgpO1xuXG5cdGNvbnN0IGNsYXNzZXMgPSB1c2VNZW1vKCAoKSA9PiB7XG5cdFx0Y29uc3QgYWxpZ25tZW50UHJvcHMgPSBnZXRBbGlnbm1lbnRQcm9wcyggYWxpZ25tZW50ICk7XG5cblx0XHRjb25zdCBncmlkQ2xhc3NlcyA9IGNzcygge1xuXHRcdFx0YWxpZ25JdGVtczogYWxpZ24sXG5cdFx0XHRkaXNwbGF5OiBpc0lubGluZSA/ICdpbmxpbmUtZ3JpZCcgOiAnZ3JpZCcsXG5cdFx0XHRnYXA6IGBjYWxjKCAkeyBDT05GSUcuZ3JpZEJhc2UgfSAqICR7IGdhcCB9IClgLFxuXHRcdFx0Z3JpZFRlbXBsYXRlQ29sdW1uczogZ3JpZFRlbXBsYXRlQ29sdW1ucyB8fCB1bmRlZmluZWQsXG5cdFx0XHRncmlkVGVtcGxhdGVSb3dzOiBncmlkVGVtcGxhdGVSb3dzIHx8IHVuZGVmaW5lZCxcblx0XHRcdGdyaWRSb3dHYXA6IHJvd0dhcCxcblx0XHRcdGdyaWRDb2x1bW5HYXA6IGNvbHVtbkdhcCxcblx0XHRcdGp1c3RpZnlDb250ZW50OiBqdXN0aWZ5LFxuXHRcdFx0dmVydGljYWxBbGlnbjogaXNJbmxpbmUgPyAnbWlkZGxlJyA6IHVuZGVmaW5lZCxcblx0XHRcdC4uLmFsaWdubWVudFByb3BzLFxuXHRcdH0gKTtcblxuXHRcdHJldHVybiBjeCggZ3JpZENsYXNzZXMsIGNsYXNzTmFtZSApO1xuXHR9LCBbXG5cdFx0YWxpZ24sXG5cdFx0YWxpZ25tZW50LFxuXHRcdGNsYXNzTmFtZSxcblx0XHRjb2x1bW5HYXAsXG5cdFx0Y3gsXG5cdFx0Z2FwLFxuXHRcdGdyaWRUZW1wbGF0ZUNvbHVtbnMsXG5cdFx0Z3JpZFRlbXBsYXRlUm93cyxcblx0XHRpc0lubGluZSxcblx0XHRqdXN0aWZ5LFxuXHRcdHJvd0dhcCxcblx0XSApO1xuXG5cdHJldHVybiB7IC4uLm90aGVyUHJvcHMsIGNsYXNzTmFtZTogY2xhc3NlcyB9O1xufVxuIl19 */");
9494
+ }, process.env.NODE_ENV === "production" ? "" : ";label:gridClasses;", process.env.NODE_ENV === "production" ? "" : "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImhvb2sudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBNENzQiIsImZpbGUiOiJob29rLnRzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgY3NzIH0gZnJvbSAnQGVtb3Rpb24vcmVhY3QnO1xuaW1wb3J0IHsgdXNlTWVtbyB9IGZyb20gJ0B3b3JkcHJlc3MvZWxlbWVudCc7XG5pbXBvcnQgdHlwZSB7IFdvcmRQcmVzc0NvbXBvbmVudFByb3BzIH0gZnJvbSAnLi4vY29udGV4dCc7XG5pbXBvcnQgeyB1c2VDb250ZXh0U3lzdGVtIH0gZnJvbSAnLi4vY29udGV4dCc7XG5pbXBvcnQgeyBnZXRBbGlnbm1lbnRQcm9wcyB9IGZyb20gJy4vdXRpbHMnO1xuaW1wb3J0IHsgdXNlUmVzcG9uc2l2ZVZhbHVlIH0gZnJvbSAnLi4vdXRpbHMvdXNlLXJlc3BvbnNpdmUtdmFsdWUnO1xuaW1wb3J0IENPTkZJRyBmcm9tICcuLi91dGlscy9jb25maWctdmFsdWVzJztcbmltcG9ydCB7IHVzZUN4IH0gZnJvbSAnLi4vdXRpbHMvaG9va3MvdXNlLWN4JztcbmltcG9ydCB0eXBlIHsgR3JpZFByb3BzIH0gZnJvbSAnLi90eXBlcyc7XG5cbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIHVzZUdyaWQoXG5cdHByb3BzOiBXb3JkUHJlc3NDb21wb25lbnRQcm9wczwgR3JpZFByb3BzLCAnZGl2JyA+XG4pIHtcblx0Y29uc3Qge1xuXHRcdGFsaWduLFxuXHRcdGFsaWdubWVudCxcblx0XHRjbGFzc05hbWUsXG5cdFx0Y29sdW1uR2FwLFxuXHRcdGNvbHVtbnMgPSAyLFxuXHRcdGdhcCA9IDMsXG5cdFx0aXNJbmxpbmUgPSBmYWxzZSxcblx0XHRqdXN0aWZ5LFxuXHRcdHJvd0dhcCxcblx0XHRyb3dzLFxuXHRcdHRlbXBsYXRlQ29sdW1ucyxcblx0XHR0ZW1wbGF0ZVJvd3MsXG5cdFx0Li4ub3RoZXJQcm9wc1xuXHR9ID0gdXNlQ29udGV4dFN5c3RlbSggcHJvcHMsICdHcmlkJyApO1xuXG5cdGNvbnN0IGNvbHVtbnNBc0FycmF5ID0gQXJyYXkuaXNBcnJheSggY29sdW1ucyApID8gY29sdW1ucyA6IFsgY29sdW1ucyBdO1xuXHRjb25zdCBjb2x1bW4gPSB1c2VSZXNwb25zaXZlVmFsdWUoIGNvbHVtbnNBc0FycmF5ICk7XG5cdGNvbnN0IHJvd3NBc0FycmF5ID0gQXJyYXkuaXNBcnJheSggcm93cyApID8gcm93cyA6IFsgcm93cyBdO1xuXHRjb25zdCByb3cgPSB1c2VSZXNwb25zaXZlVmFsdWUoIHJvd3NBc0FycmF5ICk7XG5cblx0Y29uc3QgZ3JpZFRlbXBsYXRlQ29sdW1ucyA9XG5cdFx0dGVtcGxhdGVDb2x1bW5zIHx8ICggISEgY29sdW1ucyAmJiBgcmVwZWF0KCAkeyBjb2x1bW4gfSwgMWZyIClgICk7XG5cdGNvbnN0IGdyaWRUZW1wbGF0ZVJvd3MgPVxuXHRcdHRlbXBsYXRlUm93cyB8fCAoICEhIHJvd3MgJiYgYHJlcGVhdCggJHsgcm93IH0sIDFmciApYCApO1xuXG5cdGNvbnN0IGN4ID0gdXNlQ3goKTtcblxuXHRjb25zdCBjbGFzc2VzID0gdXNlTWVtbyggKCkgPT4ge1xuXHRcdGNvbnN0IGFsaWdubWVudFByb3BzID0gZ2V0QWxpZ25tZW50UHJvcHMoIGFsaWdubWVudCApO1xuXG5cdFx0Y29uc3QgZ3JpZENsYXNzZXMgPSBjc3MoIHtcblx0XHRcdGFsaWduSXRlbXM6IGFsaWduLFxuXHRcdFx0ZGlzcGxheTogaXNJbmxpbmUgPyAnaW5saW5lLWdyaWQnIDogJ2dyaWQnLFxuXHRcdFx0Z2FwOiBgY2FsYyggJHsgQ09ORklHLmdyaWRCYXNlIH0gKiAkeyBnYXAgfSApYCxcblx0XHRcdGdyaWRUZW1wbGF0ZUNvbHVtbnM6IGdyaWRUZW1wbGF0ZUNvbHVtbnMgfHwgdW5kZWZpbmVkLFxuXHRcdFx0Z3JpZFRlbXBsYXRlUm93czogZ3JpZFRlbXBsYXRlUm93cyB8fCB1bmRlZmluZWQsXG5cdFx0XHRncmlkUm93R2FwOiByb3dHYXAsXG5cdFx0XHRncmlkQ29sdW1uR2FwOiBjb2x1bW5HYXAsXG5cdFx0XHRqdXN0aWZ5Q29udGVudDoganVzdGlmeSxcblx0XHRcdHZlcnRpY2FsQWxpZ246IGlzSW5saW5lID8gJ21pZGRsZScgOiB1bmRlZmluZWQsXG5cdFx0XHQuLi5hbGlnbm1lbnRQcm9wcyxcblx0XHR9ICk7XG5cblx0XHRyZXR1cm4gY3goIGdyaWRDbGFzc2VzLCBjbGFzc05hbWUgKTtcblx0fSwgW1xuXHRcdGFsaWduLFxuXHRcdGFsaWdubWVudCxcblx0XHRjbGFzc05hbWUsXG5cdFx0Y29sdW1uR2FwLFxuXHRcdGN4LFxuXHRcdGdhcCxcblx0XHRncmlkVGVtcGxhdGVDb2x1bW5zLFxuXHRcdGdyaWRUZW1wbGF0ZVJvd3MsXG5cdFx0aXNJbmxpbmUsXG5cdFx0anVzdGlmeSxcblx0XHRyb3dHYXAsXG5cdF0gKTtcblxuXHRyZXR1cm4geyAuLi5vdGhlclByb3BzLCBjbGFzc05hbWU6IGNsYXNzZXMgfTtcbn1cbiJdfQ== */");
8673
9495
  return cx(gridClasses, className);
8674
9496
  }, [
8675
9497
  align,
@@ -8690,7 +9512,7 @@ function useGrid(props) {
8690
9512
  };
8691
9513
  }
8692
9514
  //#endregion
8693
- //#region ../../../node_modules/.pnpm/@wordpress+components@37.0.0_@types+react-dom@18.3.7_@types+react@18.3.31__@types+react_e283446b3cbc213906bb0d97b3dd78c5/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
8694
9516
  function UnconnectedGrid(props, forwardedRef) {
8695
9517
  const gridProps = useGrid(props);
8696
9518
  return /* @__PURE__ */ jsx(component_default$2, {
@@ -8713,8 +9535,8 @@ const SUBPIXEL_TOLERANCE = .5;
8713
9535
  * Counts how many leading rows fit inside the content container.
8714
9536
  *
8715
9537
  * Rows are read from the DOM rather than derived from constants: row height
8716
- * depends on the theme's row gap, label wrapping, and whatever a caller renders
8717
- * as a label, so any restated number would drift.
9538
+ * depends on `--a8c-charts-dimension-leaderboard-row-gap`, label wrapping, and
9539
+ * whatever a caller renders as a label, so any restated number would drift.
8718
9540
  *
8719
9541
  * Every row is a direct grid child marked with `data-row-index`; interactive rows
8720
9542
  * use a button wrapper and non-interactive rows use a div wrapper.
@@ -8799,14 +9621,13 @@ function useFittedRowCount(enabled, rowCount, data) {
8799
9621
  * @return Array of legend items for the leaderboard chart
8800
9622
  */
8801
9623
  function useLeaderboardLegendItems({ data, primaryColor, secondaryColor, withComparison = false, withOverlayLabel = false, legendLabels }) {
8802
- const { leaderboardChart: leaderboardChartSettings } = useGlobalChartsTheme();
8803
9624
  const { getElementStyles } = useGlobalChartsContext();
8804
9625
  return useMemo(() => {
8805
9626
  if (!data || data.length === 0) return [];
8806
9627
  const items = [];
8807
9628
  const { color: resolvedPrimaryColor } = getElementStyles({
8808
9629
  index: 0,
8809
- overrideColor: primaryColor || leaderboardChartSettings.primaryColor
9630
+ overrideColor: primaryColor
8810
9631
  });
8811
9632
  items.push({
8812
9633
  label: legendLabels?.primary || __("Current period", "jetpack-charts"),
@@ -8815,7 +9636,7 @@ function useLeaderboardLegendItems({ data, primaryColor, secondaryColor, withCom
8815
9636
  if (withComparison && !withOverlayLabel) {
8816
9637
  const { color: resolvedSecondaryColor } = getElementStyles({
8817
9638
  index: 1,
8818
- overrideColor: secondaryColor || leaderboardChartSettings.secondaryColor
9639
+ overrideColor: secondaryColor
8819
9640
  });
8820
9641
  items.push({
8821
9642
  label: legendLabels?.comparison || __("Previous period", "jetpack-charts"),
@@ -8829,7 +9650,6 @@ function useLeaderboardLegendItems({ data, primaryColor, secondaryColor, withCom
8829
9650
  secondaryColor,
8830
9651
  withComparison,
8831
9652
  legendLabels,
8832
- leaderboardChartSettings,
8833
9653
  getElementStyles,
8834
9654
  withOverlayLabel
8835
9655
  ]);
@@ -8843,6 +9663,9 @@ var leaderboard_chart_module_default = {
8843
9663
  "chevron": "a8ccharts-GovfoW-chevron",
8844
9664
  "deltaPlaceholder": "a8ccharts-GovfoW-deltaPlaceholder",
8845
9665
  "deltaValue": "a8ccharts-GovfoW-deltaValue",
9666
+ "deltaValue--down": "a8ccharts-GovfoW-deltaValue--down",
9667
+ "deltaValue--neutral": "a8ccharts-GovfoW-deltaValue--neutral",
9668
+ "deltaValue--up": "a8ccharts-GovfoW-deltaValue--up",
8846
9669
  "emptyState": "a8ccharts-GovfoW-emptyState",
8847
9670
  "fitEmptyState": "a8ccharts-GovfoW-fitEmptyState",
8848
9671
  "interactiveRow": "a8ccharts-GovfoW-interactiveRow",
@@ -8861,6 +9684,14 @@ var leaderboard_chart_module_default = {
8861
9684
  };
8862
9685
  //#endregion
8863
9686
  //#region src/charts/leaderboard-chart/leaderboard-chart.tsx
9687
+ const ROW_GAP = "var(--a8c-charts-dimension-leaderboard-row-gap, 12px)";
9688
+ const COLUMN_GAP = "var(--a8c-charts-dimension-leaderboard-column-gap, 4px)";
9689
+ /** Indexed by `Math.sign( delta ) + 1`, so [negative, neutral, positive]. */
9690
+ const DELTA_TREND_CLASS = [
9691
+ "deltaValue--down",
9692
+ "deltaValue--neutral",
9693
+ "deltaValue--up"
9694
+ ];
8864
9695
  /**
8865
9696
  * Default value formatter using formatMetricValue
8866
9697
  *
@@ -8963,15 +9794,15 @@ const LeaderboardChartInternal = ({ data, chartId: providedChartId, width: propW
8963
9794
  ...legend.shapeStyles
8964
9795
  };
8965
9796
  const { legendChildren, nonLegendChildren } = useChartChildren(children, "LeaderboardChart");
8966
- const { labelSpacing, rowGap, columnGap, primaryColor: settingsPrimaryColor, secondaryColor: settingsSecondaryColor, deltaColors } = leaderboardChartSettings;
9797
+ const { labelSpacing } = leaderboardChartSettings;
8967
9798
  const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
8968
9799
  const { color: resolvedPrimaryColor } = getElementStyles({
8969
9800
  index: 0,
8970
- overrideColor: primaryColor || settingsPrimaryColor
9801
+ overrideColor: primaryColor
8971
9802
  });
8972
9803
  const { color: resolvedSecondaryColor } = getElementStyles({
8973
9804
  index: 1,
8974
- overrideColor: secondaryColor || settingsSecondaryColor
9805
+ overrideColor: secondaryColor
8975
9806
  });
8976
9807
  const legendItems = useLeaderboardLegendItems({
8977
9808
  data: data || [],
@@ -9027,7 +9858,7 @@ const LeaderboardChartInternal = ({ data, chartId: providedChartId, width: propW
9027
9858
  legendPosition,
9028
9859
  legendElement: false,
9029
9860
  legendChildren,
9030
- className: clsx(leaderboard_chart_module_default.leaderboardChart, {
9861
+ className: clsx("leaderboard-chart", leaderboard_chart_module_default.leaderboardChart, {
9031
9862
  [leaderboard_chart_module_default["leaderboardChart--responsive"]]: !propWidth && !propHeight,
9032
9863
  [leaderboard_chart_module_default["leaderboardChart--loading"]]: loading
9033
9864
  }, className),
@@ -9062,7 +9893,7 @@ const LeaderboardChartInternal = ({ data, chartId: providedChartId, width: propW
9062
9893
  legendPosition,
9063
9894
  legendElement,
9064
9895
  legendChildren,
9065
- className: clsx(leaderboard_chart_module_default.leaderboardChart, {
9896
+ className: clsx("leaderboard-chart", leaderboard_chart_module_default.leaderboardChart, {
9066
9897
  [leaderboard_chart_module_default["leaderboardChart--responsive"]]: !propWidth && !propHeight,
9067
9898
  [leaderboard_chart_module_default["leaderboardChart--loading"]]: loading
9068
9899
  }, className),
@@ -9084,8 +9915,8 @@ const LeaderboardChartInternal = ({ data, chartId: providedChartId, width: propW
9084
9915
  children: getAllHiddenMessage(legendInteractive, "series")
9085
9916
  }) : /* @__PURE__ */ jsx(component_default, {
9086
9917
  templateColumns: "minmax(0, 1fr) auto",
9087
- rowGap,
9088
- columnGap,
9918
+ rowGap: ROW_GAP,
9919
+ columnGap: COLUMN_GAP,
9089
9920
  "data-leaderboard-grid": true,
9090
9921
  children: data.map((entry, rowIndex) => {
9091
9922
  const rowStyle = shouldFitRows && rowIndex >= fittedCount ? { visibility: "hidden" } : void 0;
@@ -9094,8 +9925,7 @@ const LeaderboardChartInternal = ({ data, chartId: providedChartId, width: propW
9094
9925
  const hasDelta = hasDeltaValue(entry);
9095
9926
  const showComparisonValue = showComparisonColumn && hasDelta;
9096
9927
  const showComparisonPlaceholder = showComparisonColumn && !hasDelta;
9097
- const colorIndex = showComparisonValue ? Math.sign(entry.delta) + 1 : 1;
9098
- const deltaColor = deltaColors[colorIndex];
9928
+ const deltaTrendClass = leaderboard_chart_module_default[DELTA_TREND_CLASS[showComparisonValue ? Math.sign(entry.delta) + 1 : 1]];
9099
9929
  const rowCells = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Stack, {
9100
9930
  direction: "column",
9101
9931
  gap: labelSpacing,
@@ -9116,13 +9946,11 @@ const LeaderboardChartInternal = ({ data, chartId: providedChartId, width: propW
9116
9946
  children: [
9117
9947
  isPrimaryVisible && /* @__PURE__ */ jsx(Text$1, { children: valueFormatter(entry.currentValue) }),
9118
9948
  showComparisonValue && /* @__PURE__ */ jsx(Text$1, {
9119
- className: leaderboard_chart_module_default.deltaValue,
9120
- style: { color: deltaColor },
9949
+ className: clsx(leaderboard_chart_module_default.deltaValue, deltaTrendClass),
9121
9950
  children: deltaFormatter(entry.delta)
9122
9951
  }),
9123
9952
  showComparisonPlaceholder && /* @__PURE__ */ jsxs(Text$1, {
9124
- className: clsx(leaderboard_chart_module_default.deltaValue, leaderboard_chart_module_default.deltaPlaceholder),
9125
- style: { color: deltaColor },
9953
+ className: clsx(leaderboard_chart_module_default.deltaValue, leaderboard_chart_module_default.deltaPlaceholder, deltaTrendClass),
9126
9954
  children: [/* @__PURE__ */ jsx("span", {
9127
9955
  "aria-hidden": "true",
9128
9956
  children: "—"
@@ -9206,7 +10034,9 @@ function RadialWipeAnimation({ id, radius, innerRadius = 0, durationMs = 1e3, wi
9206
10034
  //#endregion
9207
10035
  //#region src/charts/pie-chart/pie-chart.module.scss
9208
10036
  var pie_chart_module_default = {
9209
- "pie-chart": "a8ccharts-gnszbG-pie-chart",
10037
+ "pie-chart__label-plate": "a8ccharts-gnszbG-pie-chart__label-plate",
10038
+ "pie-chart__label-text": "a8ccharts-gnszbG-pie-chart__label-text",
10039
+ "pie-chart__plot": "a8ccharts-gnszbG-pie-chart__plot",
9210
10040
  "pie-chart--responsive": "a8ccharts-gnszbG-pie-chart--responsive"
9211
10041
  };
9212
10042
  //#endregion
@@ -9261,11 +10091,8 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
9261
10091
  const providerTheme = useGlobalChartsTheme();
9262
10092
  const chartId = useChartId(providedChartId);
9263
10093
  const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } = useTooltip();
9264
- const { containerRef, TooltipInPortal, containerBounds } = useTooltipInPortal({
9265
- detectBounds: true,
9266
- scroll: true,
9267
- debounce: 0
9268
- });
10094
+ const standaloneScopeClass = useStandaloneScopeClass();
10095
+ const containerRef = useRef(null);
9269
10096
  const onMouseLeave = useCallback(() => {
9270
10097
  if (!withTooltips) return;
9271
10098
  hideTooltip();
@@ -9348,19 +10175,7 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
9348
10175
  width: propWidth || void 0,
9349
10176
  height: propHeight || void 0
9350
10177
  },
9351
- trailingContent: /* @__PURE__ */ jsxs(Fragment$1, { children: [
9352
- withTooltips && tooltipOpen && tooltipData && /* @__PURE__ */ jsx(TooltipInPortal, {
9353
- top: tooltipTop || 0,
9354
- left: tooltipLeft || 0,
9355
- children: /* @__PURE__ */ jsx("div", {
9356
- className: "a8c-charts-scope",
9357
- role: "tooltip",
9358
- children: renderTooltip({ tooltipData })
9359
- })
9360
- }),
9361
- htmlChildren,
9362
- otherChildren
9363
- ] }),
10178
+ trailingContent: /* @__PURE__ */ jsxs(Fragment$1, { children: [htmlChildren, otherChildren] }),
9364
10179
  children: ({ contentWidth, contentHeight }) => {
9365
10180
  const availableSize = Math.min(contentWidth > 0 ? contentWidth : 300, contentHeight > 0 ? contentHeight : 300);
9366
10181
  const actualSize = size ? Math.min(size, availableSize) : availableSize;
@@ -9373,9 +10188,10 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
9373
10188
  const innerRadius = thickness === 0 ? 0 : outerRadius * (1 - thickness);
9374
10189
  const maxCornerRadius = (outerRadius - innerRadius) / 2;
9375
10190
  const cornerRadius = cornerScale ? Math.min(cornerScale * outerRadius, maxCornerRadius) : 0;
9376
- return /* @__PURE__ */ jsx(Center, {
10191
+ return /* @__PURE__ */ jsxs(Center, {
9377
10192
  ref: containerRef,
9378
- children: /* @__PURE__ */ jsxs("svg", {
10193
+ className: pie_chart_module_default["pie-chart__plot"],
10194
+ children: [/* @__PURE__ */ jsxs("svg", {
9379
10195
  viewBox: `0 0 ${width} ${height}`,
9380
10196
  preserveAspectRatio: "xMidYMid meet",
9381
10197
  width,
@@ -9407,11 +10223,12 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
9407
10223
  const hasSpaceForLabel = arc.endAngle - arc.startAngle >= .25;
9408
10224
  const handleMouseMove = (event) => {
9409
10225
  if (!withTooltips) return;
9410
- if (containerBounds.width === 0 || containerBounds.height === 0) return;
10226
+ const bounds = containerRef.current?.getBoundingClientRect();
10227
+ if (!bounds) return;
9411
10228
  showTooltip({
9412
10229
  tooltipData: arc.data,
9413
- tooltipLeft: event.clientX - containerBounds.left + tooltipOffsetX,
9414
- tooltipTop: event.clientY - containerBounds.top + tooltipOffsetY
10230
+ tooltipLeft: event.clientX - bounds.left + tooltipOffsetX,
10231
+ tooltipTop: event.clientY - bounds.top + tooltipOffsetY
9415
10232
  });
9416
10233
  };
9417
10234
  const pathProps = {
@@ -9434,20 +10251,20 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
9434
10251
  const backgroundHeight = fontSize + 12;
9435
10252
  return /* @__PURE__ */ jsxs("g", {
9436
10253
  ...groupProps,
9437
- children: [/* @__PURE__ */ jsx("path", { ...pathProps }), showLabels && hasSpaceForLabel && /* @__PURE__ */ jsxs("g", { children: [providerTheme.labelBackgroundColor && /* @__PURE__ */ jsx("rect", {
10254
+ children: [/* @__PURE__ */ jsx("path", { ...pathProps }), showLabels && hasSpaceForLabel && /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("rect", {
10255
+ className: pie_chart_module_default["pie-chart__label-plate"],
9438
10256
  x: centroidX - backgroundWidth / 2,
9439
10257
  y: centroidY - backgroundHeight / 2,
9440
10258
  width: backgroundWidth,
9441
10259
  height: backgroundHeight,
9442
- fill: providerTheme.labelBackgroundColor,
9443
10260
  rx: 4,
9444
10261
  ry: 4,
9445
10262
  pointerEvents: "none"
9446
10263
  }), /* @__PURE__ */ jsx("text", {
10264
+ className: pie_chart_module_default["pie-chart__label-text"],
9447
10265
  x: centroidX,
9448
10266
  y: centroidY,
9449
10267
  dy: ".33em",
9450
- fill: providerTheme.labelTextColor || "#333",
9451
10268
  fontSize,
9452
10269
  textAnchor: "middle",
9453
10270
  pointerEvents: "none",
@@ -9458,7 +10275,15 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
9458
10275
  }
9459
10276
  }), !allSegmentsHidden && svgChildren]
9460
10277
  })]
9461
- })
10278
+ }), withTooltips && tooltipOpen && tooltipData && /* @__PURE__ */ jsx(BoundedTooltip, {
10279
+ top: tooltipTop || 0,
10280
+ left: tooltipLeft || 0,
10281
+ children: /* @__PURE__ */ jsx("div", {
10282
+ className: standaloneScopeClass,
10283
+ role: "tooltip",
10284
+ children: renderTooltip({ tooltipData })
10285
+ })
10286
+ })]
9462
10287
  });
9463
10288
  }
9464
10289
  })
@@ -9485,6 +10310,7 @@ var pie_semi_circle_chart_module_default = {
9485
10310
  "label": "a8ccharts-YtTOxW-label",
9486
10311
  "note": "a8ccharts-YtTOxW-note",
9487
10312
  "pie-semi-circle-chart": "a8ccharts-YtTOxW-pie-semi-circle-chart",
10313
+ "pie-semi-circle-chart__plot": "a8ccharts-YtTOxW-pie-semi-circle-chart__plot",
9488
10314
  "pie-semi-circle-chart--responsive": "a8ccharts-YtTOxW-pie-semi-circle-chart--responsive"
9489
10315
  };
9490
10316
  //#endregion
@@ -9534,23 +10360,17 @@ const PieSemiCircleChartInternal = ({ data, chartId: providedChartId, width: pro
9534
10360
  const legendPosition = legend.position ?? "bottom";
9535
10361
  const chartId = useChartId(providedChartId);
9536
10362
  const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } = useTooltip();
9537
- const { containerRef, TooltipInPortal, containerBounds } = useTooltipInPortal({
9538
- detectBounds: true,
9539
- scroll: true,
9540
- debounce: 0
9541
- });
10363
+ const standaloneScopeClass = useStandaloneScopeClass();
10364
+ const containerRef = useRef(null);
9542
10365
  const handleMouseMove = useCallback((event, arc) => {
9543
- if (containerBounds.width === 0 || containerBounds.height === 0) return;
10366
+ const bounds = containerRef.current?.getBoundingClientRect();
10367
+ if (!bounds) return;
9544
10368
  showTooltip({
9545
10369
  tooltipData: arc.data,
9546
- tooltipLeft: event.clientX - containerBounds.left + tooltipOffsetX,
9547
- tooltipTop: event.clientY - containerBounds.top + tooltipOffsetY
10370
+ tooltipLeft: event.clientX - bounds.left + tooltipOffsetX,
10371
+ tooltipTop: event.clientY - bounds.top + tooltipOffsetY
9548
10372
  });
9549
10373
  }, [
9550
- containerBounds.width,
9551
- containerBounds.height,
9552
- containerBounds.left,
9553
- containerBounds.top,
9554
10374
  showTooltip,
9555
10375
  tooltipOffsetX,
9556
10376
  tooltipOffsetY
@@ -9599,7 +10419,7 @@ const PieSemiCircleChartInternal = ({ data, chartId: providedChartId, width: pro
9599
10419
  const errorWidth = propHeight ? Math.min(propWidth || propHeight * 2, propHeight * 2) : effectiveWidth;
9600
10420
  const errorHeight = errorWidth / 2;
9601
10421
  return /* @__PURE__ */ jsx("div", {
9602
- className: pie_semi_circle_chart_module_default["pie-semi-circle-chart"],
10422
+ className: clsx("pie-semi-circle-chart", pie_semi_circle_chart_module_default["pie-semi-circle-chart"], className),
9603
10423
  children: /* @__PURE__ */ jsx("svg", {
9604
10424
  width: errorWidth,
9605
10425
  height: errorHeight,
@@ -9646,19 +10466,7 @@ const PieSemiCircleChartInternal = ({ data, chartId: providedChartId, width: pro
9646
10466
  width: propWidth || void 0,
9647
10467
  height: propHeight || void 0
9648
10468
  },
9649
- trailingContent: /* @__PURE__ */ jsxs(Fragment$1, { children: [
9650
- withTooltips && tooltipOpen && tooltipData && /* @__PURE__ */ jsx(TooltipInPortal, {
9651
- top: tooltipTop || 0,
9652
- left: tooltipLeft || 0,
9653
- children: /* @__PURE__ */ jsx("div", {
9654
- className: "a8c-charts-scope",
9655
- role: "tooltip",
9656
- children: renderTooltip({ tooltipData })
9657
- })
9658
- }),
9659
- htmlChildren,
9660
- otherChildren
9661
- ] }),
10469
+ trailingContent: /* @__PURE__ */ jsxs(Fragment$1, { children: [htmlChildren, otherChildren] }),
9662
10470
  children: ({ contentWidth, contentHeight }) => {
9663
10471
  const availableWidth = contentWidth > 0 ? contentWidth : effectiveWidth;
9664
10472
  const availableHeight = contentHeight > 0 ? contentHeight : propHeight || effectiveWidth / 2;
@@ -9666,9 +10474,10 @@ const PieSemiCircleChartInternal = ({ data, chartId: providedChartId, width: pro
9666
10474
  const height = width / 2;
9667
10475
  const radius = height;
9668
10476
  const innerRadius = radius * (1 - thickness);
9669
- return /* @__PURE__ */ jsx(Center, {
10477
+ return /* @__PURE__ */ jsxs(Center, {
9670
10478
  ref: containerRef,
9671
- children: /* @__PURE__ */ jsxs("svg", {
10479
+ className: pie_semi_circle_chart_module_default["pie-semi-circle-chart__plot"],
10480
+ children: [/* @__PURE__ */ jsxs("svg", {
9672
10481
  width,
9673
10482
  height,
9674
10483
  viewBox: `0 0 ${width} ${height}`,
@@ -9726,7 +10535,15 @@ const PieSemiCircleChartInternal = ({ data, chartId: providedChartId, width: pro
9726
10535
  !allSegmentsHidden && svgChildren
9727
10536
  ] })
9728
10537
  })]
9729
- })
10538
+ }), withTooltips && tooltipOpen && tooltipData && /* @__PURE__ */ jsx(BoundedTooltip, {
10539
+ top: tooltipTop || 0,
10540
+ left: tooltipLeft || 0,
10541
+ children: /* @__PURE__ */ jsx("div", {
10542
+ className: standaloneScopeClass,
10543
+ role: "tooltip",
10544
+ children: renderTooltip({ tooltipData })
10545
+ })
10546
+ })]
9730
10547
  });
9731
10548
  }
9732
10549
  })
@@ -9951,6 +10768,6 @@ function TrendIndicator({ direction, value, className, style, showIcon = true })
9951
10768
  });
9952
10769
  }
9953
10770
  //#endregion
9954
- 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, getColorDistance, hexToRgba, isValidHexColor, lightenHexColor, mergeThemes, mixHexColors, normalizeColorToHex, parseAsLocalDate, parseHslString, parseRgbString, prefersLightText, relativeLuminance, resolveCssVariable, 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 };
9955
10772
 
9956
10773
  //# sourceMappingURL=index.js.map