@seekalfred/primitives 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/README.md +19 -11
  2. package/dist/activity-heatmap/activity-heatmap.d.ts +92 -0
  3. package/dist/activity-heatmap/activity-heatmap.js +699 -0
  4. package/dist/activity-heatmap/activity-heatmap.js.map +1 -0
  5. package/dist/activity-heatmap/index.d.ts +2 -0
  6. package/dist/activity-heatmap/index.js +2 -0
  7. package/dist/alert/alert.d.ts +5 -1
  8. package/dist/alert/alert.js +68 -7
  9. package/dist/alert/alert.js.map +1 -1
  10. package/dist/alert-dialog/alert-dialog.d.ts +45 -18
  11. package/dist/alert-dialog/alert-dialog.js +128 -9
  12. package/dist/alert-dialog/alert-dialog.js.map +1 -1
  13. package/dist/alert-dialog/index.d.ts +2 -2
  14. package/dist/alert-dialog/index.js +2 -2
  15. package/dist/animated-counter/animated-counter.d.ts +38 -0
  16. package/dist/animated-counter/animated-counter.js +230 -0
  17. package/dist/animated-counter/animated-counter.js.map +1 -0
  18. package/dist/animated-counter/index.d.ts +2 -0
  19. package/dist/animated-counter/index.js +2 -0
  20. package/dist/announcement-bar/announcement-bar.d.ts +67 -0
  21. package/dist/announcement-bar/announcement-bar.js +364 -0
  22. package/dist/announcement-bar/announcement-bar.js.map +1 -0
  23. package/dist/announcement-bar/index.d.ts +2 -0
  24. package/dist/announcement-bar/index.js +2 -0
  25. package/dist/badge/badge.d.ts +1 -1
  26. package/dist/bar-chart/bar-chart.d.ts +51 -0
  27. package/dist/bar-chart/bar-chart.js +565 -0
  28. package/dist/bar-chart/bar-chart.js.map +1 -0
  29. package/dist/bar-chart/index.d.ts +2 -0
  30. package/dist/bar-chart/index.js +2 -0
  31. package/dist/breadcrumb/breadcrumb.d.ts +2 -2
  32. package/dist/breadcrumb/breadcrumb.js +12 -3
  33. package/dist/breadcrumb/breadcrumb.js.map +1 -1
  34. package/dist/brush-chart/brush-chart.d.ts +64 -0
  35. package/dist/brush-chart/brush-chart.js +1056 -0
  36. package/dist/brush-chart/brush-chart.js.map +1 -0
  37. package/dist/brush-chart/index.d.ts +2 -0
  38. package/dist/brush-chart/index.js +2 -0
  39. package/dist/button/button.d.ts +2 -2
  40. package/dist/button/button.js +31 -21
  41. package/dist/button/button.js.map +1 -1
  42. package/dist/button-group/button-group.js +3 -4
  43. package/dist/button-group/button-group.js.map +1 -1
  44. package/dist/card/card.js +1 -1
  45. package/dist/card/card.js.map +1 -1
  46. package/dist/card-stack/card-stack.d.ts +54 -0
  47. package/dist/card-stack/card-stack.js +492 -0
  48. package/dist/card-stack/card-stack.js.map +1 -0
  49. package/dist/card-stack/index.d.ts +2 -0
  50. package/dist/card-stack/index.js +2 -0
  51. package/dist/carousel/carousel.d.ts +56 -0
  52. package/dist/carousel/carousel.js +389 -0
  53. package/dist/carousel/carousel.js.map +1 -0
  54. package/dist/carousel/index.d.ts +2 -0
  55. package/dist/carousel/index.js +2 -0
  56. package/dist/checkbox/checkbox.js +4 -4
  57. package/dist/checkbox/checkbox.js.map +1 -1
  58. package/dist/chip-group/chip-group.d.ts +37 -0
  59. package/dist/chip-group/chip-group.js +148 -0
  60. package/dist/chip-group/chip-group.js.map +1 -0
  61. package/dist/chip-group/index.d.ts +2 -0
  62. package/dist/chip-group/index.js +2 -0
  63. package/dist/countdown/countdown.d.ts +45 -0
  64. package/dist/countdown/countdown.js +298 -0
  65. package/dist/countdown/countdown.js.map +1 -0
  66. package/dist/countdown/index.d.ts +2 -0
  67. package/dist/countdown/index.js +2 -0
  68. package/dist/date-picker/picker-parts.js +1 -1
  69. package/dist/date-picker/picker-parts.js.map +1 -1
  70. package/dist/dialog/dialog.js +1 -1
  71. package/dist/dialog/dialog.js.map +1 -1
  72. package/dist/donut-chart/donut-chart.d.ts +64 -0
  73. package/dist/donut-chart/donut-chart.js +576 -0
  74. package/dist/donut-chart/donut-chart.js.map +1 -0
  75. package/dist/donut-chart/index.d.ts +2 -0
  76. package/dist/donut-chart/index.js +2 -0
  77. package/dist/empty-state/empty-state.d.ts +21 -10
  78. package/dist/empty-state/empty-state.js +32 -26
  79. package/dist/empty-state/empty-state.js.map +1 -1
  80. package/dist/empty-state/index.d.ts +2 -2
  81. package/dist/empty-state/index.js +2 -2
  82. package/dist/file-drop-zone/file-drop-zone.js +1 -1
  83. package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
  84. package/dist/filter-bar/filter-bar.d.ts +47 -26
  85. package/dist/filter-bar/filter-bar.js +272 -156
  86. package/dist/filter-bar/filter-bar.js.map +1 -1
  87. package/dist/gauge/gauge.d.ts +45 -0
  88. package/dist/gauge/gauge.js +219 -0
  89. package/dist/gauge/gauge.js.map +1 -0
  90. package/dist/gauge/index.d.ts +2 -0
  91. package/dist/gauge/index.js +2 -0
  92. package/dist/in-view-title/in-view-title.d.ts +33 -0
  93. package/dist/in-view-title/in-view-title.js +119 -0
  94. package/dist/in-view-title/in-view-title.js.map +1 -0
  95. package/dist/in-view-title/index.d.ts +2 -0
  96. package/dist/in-view-title/index.js +2 -0
  97. package/dist/index.d.ts +49 -6
  98. package/dist/index.js +48 -8
  99. package/dist/lib/cn.js +11 -1
  100. package/dist/lib/cn.js.map +1 -1
  101. package/dist/lib/field-message.js +28 -10
  102. package/dist/lib/field-message.js.map +1 -1
  103. package/dist/lib/field-surface.js +9 -8
  104. package/dist/lib/field-surface.js.map +1 -1
  105. package/dist/lib/flag-emoji.js +50 -0
  106. package/dist/lib/flag-emoji.js.map +1 -0
  107. package/dist/lib/flip.js +11 -3
  108. package/dist/lib/flip.js.map +1 -1
  109. package/dist/lib/menu-highlight.js +64 -0
  110. package/dist/lib/menu-highlight.js.map +1 -0
  111. package/dist/lib/motion.js +9 -2
  112. package/dist/lib/motion.js.map +1 -1
  113. package/dist/lib/rolling-value.js +1 -1
  114. package/dist/lib/rolling-value.js.map +1 -1
  115. package/dist/lib/spring-tween.js +500 -0
  116. package/dist/lib/spring-tween.js.map +1 -0
  117. package/dist/lib/swap-text.js +3 -1
  118. package/dist/lib/swap-text.js.map +1 -1
  119. package/dist/line-chart/index.d.ts +2 -0
  120. package/dist/line-chart/index.js +2 -0
  121. package/dist/line-chart/line-chart.d.ts +76 -0
  122. package/dist/line-chart/line-chart.js +611 -0
  123. package/dist/line-chart/line-chart.js.map +1 -0
  124. package/dist/mention-input/index.d.ts +2 -0
  125. package/dist/mention-input/index.js +2 -0
  126. package/dist/mention-input/mention-input.d.ts +98 -0
  127. package/dist/mention-input/mention-input.js +576 -0
  128. package/dist/mention-input/mention-input.js.map +1 -0
  129. package/dist/menu/menu.d.ts +2 -0
  130. package/dist/menu/menu.js +23 -12
  131. package/dist/menu/menu.js.map +1 -1
  132. package/dist/money-input/index.d.ts +2 -0
  133. package/dist/money-input/index.js +2 -0
  134. package/dist/money-input/money-input.d.ts +65 -0
  135. package/dist/money-input/money-input.js +607 -0
  136. package/dist/money-input/money-input.js.map +1 -0
  137. package/dist/morph-select/index.d.ts +2 -0
  138. package/dist/morph-select/index.js +2 -0
  139. package/dist/morph-select/morph-select.d.ts +71 -0
  140. package/dist/morph-select/morph-select.js +693 -0
  141. package/dist/morph-select/morph-select.js.map +1 -0
  142. package/dist/multi-select/index.d.ts +2 -0
  143. package/dist/multi-select/index.js +2 -0
  144. package/dist/multi-select/multi-select.d.ts +36 -0
  145. package/dist/multi-select/multi-select.js +152 -0
  146. package/dist/multi-select/multi-select.js.map +1 -0
  147. package/dist/number-field/index.d.ts +2 -0
  148. package/dist/number-field/index.js +2 -0
  149. package/dist/number-field/number-field.d.ts +42 -0
  150. package/dist/number-field/number-field.js +199 -0
  151. package/dist/number-field/number-field.js.map +1 -0
  152. package/dist/onboarding-checklist/index.d.ts +2 -0
  153. package/dist/onboarding-checklist/index.js +2 -0
  154. package/dist/onboarding-checklist/onboarding-checklist.d.ts +55 -0
  155. package/dist/onboarding-checklist/onboarding-checklist.js +577 -0
  156. package/dist/onboarding-checklist/onboarding-checklist.js.map +1 -0
  157. package/dist/password-field/password-field.js +2 -2
  158. package/dist/password-field/password-field.js.map +1 -1
  159. package/dist/phone-input/index.d.ts +3 -0
  160. package/dist/phone-input/index.js +3 -0
  161. package/dist/phone-input/phone-countries.d.ts +29 -0
  162. package/dist/phone-input/phone-countries.js +170 -0
  163. package/dist/phone-input/phone-countries.js.map +1 -0
  164. package/dist/phone-input/phone-input.d.ts +66 -0
  165. package/dist/phone-input/phone-input.js +440 -0
  166. package/dist/phone-input/phone-input.js.map +1 -0
  167. package/dist/popover/popover.d.ts +12 -0
  168. package/dist/popover/popover.js +31 -7
  169. package/dist/popover/popover.js.map +1 -1
  170. package/dist/progress/progress.js +1 -1
  171. package/dist/radio-group/radio-group.js +1 -1
  172. package/dist/radio-group/radio-group.js.map +1 -1
  173. package/dist/reactions/index.d.ts +2 -0
  174. package/dist/reactions/index.js +2 -0
  175. package/dist/reactions/reactions.d.ts +50 -0
  176. package/dist/reactions/reactions.js +315 -0
  177. package/dist/reactions/reactions.js.map +1 -0
  178. package/dist/rich-text-editor/index.d.ts +3 -0
  179. package/dist/rich-text-editor/index.js +2 -0
  180. package/dist/rich-text-editor/rich-text-editor.d.ts +70 -0
  181. package/dist/rich-text-editor/rich-text-editor.js +909 -0
  182. package/dist/rich-text-editor/rich-text-editor.js.map +1 -0
  183. package/dist/rich-text-editor/rich-text-engine.d.ts +16 -0
  184. package/dist/rich-text-editor/rich-text-engine.js +366 -0
  185. package/dist/rich-text-editor/rich-text-engine.js.map +1 -0
  186. package/dist/rich-text-editor/rich-text-html.js +174 -0
  187. package/dist/rich-text-editor/rich-text-html.js.map +1 -0
  188. package/dist/rich-text-editor/rich-text-icons.js +43 -0
  189. package/dist/rich-text-editor/rich-text-icons.js.map +1 -0
  190. package/dist/ridgeline/index.d.ts +2 -0
  191. package/dist/ridgeline/index.js +2 -0
  192. package/dist/ridgeline/ridgeline.d.ts +55 -0
  193. package/dist/ridgeline/ridgeline.js +701 -0
  194. package/dist/ridgeline/ridgeline.js.map +1 -0
  195. package/dist/scroll-area/index.d.ts +2 -2
  196. package/dist/scroll-area/index.js +2 -2
  197. package/dist/scroll-area/scroll-area.d.ts +68 -31
  198. package/dist/scroll-area/scroll-area.js +208 -59
  199. package/dist/scroll-area/scroll-area.js.map +1 -1
  200. package/dist/scrub-input/evaluate.d.ts +14 -0
  201. package/dist/scrub-input/evaluate.js +72 -0
  202. package/dist/scrub-input/evaluate.js.map +1 -0
  203. package/dist/scrub-input/index.d.ts +3 -0
  204. package/dist/scrub-input/index.js +3 -0
  205. package/dist/scrub-input/scrub-input.d.ts +74 -0
  206. package/dist/scrub-input/scrub-input.js +486 -0
  207. package/dist/scrub-input/scrub-input.js.map +1 -0
  208. package/dist/search-field/search-field.d.ts +2 -1
  209. package/dist/search-field/search-field.js +4 -4
  210. package/dist/search-field/search-field.js.map +1 -1
  211. package/dist/select/select.d.ts +3 -1
  212. package/dist/select/select.js +20 -8
  213. package/dist/select/select.js.map +1 -1
  214. package/dist/separator/separator.d.ts +1 -1
  215. package/dist/sheet/sheet.js +2 -1
  216. package/dist/sheet/sheet.js.map +1 -1
  217. package/dist/shortcut-recorder/index.d.ts +3 -0
  218. package/dist/shortcut-recorder/index.js +3 -0
  219. package/dist/shortcut-recorder/shortcut-model.d.ts +27 -0
  220. package/dist/shortcut-recorder/shortcut-model.js +206 -0
  221. package/dist/shortcut-recorder/shortcut-model.js.map +1 -0
  222. package/dist/shortcut-recorder/shortcut-recorder.d.ts +93 -0
  223. package/dist/shortcut-recorder/shortcut-recorder.js +489 -0
  224. package/dist/shortcut-recorder/shortcut-recorder.js.map +1 -0
  225. package/dist/slider/index.d.ts +2 -2
  226. package/dist/slider/slider.d.ts +46 -14
  227. package/dist/slider/slider.js +151 -67
  228. package/dist/slider/slider.js.map +1 -1
  229. package/dist/slope-chart/index.d.ts +2 -0
  230. package/dist/slope-chart/index.js +2 -0
  231. package/dist/slope-chart/slope-chart.d.ts +54 -0
  232. package/dist/slope-chart/slope-chart.js +568 -0
  233. package/dist/slope-chart/slope-chart.js.map +1 -0
  234. package/dist/sparkline/index.d.ts +2 -0
  235. package/dist/sparkline/index.js +2 -0
  236. package/dist/sparkline/sparkline.d.ts +46 -0
  237. package/dist/sparkline/sparkline.js +390 -0
  238. package/dist/sparkline/sparkline.js.map +1 -0
  239. package/dist/split-bar/index.d.ts +2 -0
  240. package/dist/split-bar/index.js +2 -0
  241. package/dist/split-bar/split-bar.d.ts +62 -0
  242. package/dist/split-bar/split-bar.js +513 -0
  243. package/dist/split-bar/split-bar.js.map +1 -0
  244. package/dist/split-button/split-button.d.ts +1 -1
  245. package/dist/split-button/split-button.js +12 -11
  246. package/dist/split-button/split-button.js.map +1 -1
  247. package/dist/stat-card/index.d.ts +2 -2
  248. package/dist/stat-card/index.js +2 -2
  249. package/dist/stat-card/stat-card.d.ts +24 -4
  250. package/dist/stat-card/stat-card.js +114 -17
  251. package/dist/stat-card/stat-card.js.map +1 -1
  252. package/dist/stepper/index.d.ts +2 -0
  253. package/dist/stepper/index.js +2 -0
  254. package/dist/stepper/stepper.d.ts +51 -0
  255. package/dist/stepper/stepper.js +215 -0
  256. package/dist/stepper/stepper.js.map +1 -0
  257. package/dist/streamgraph/index.d.ts +2 -0
  258. package/dist/streamgraph/index.js +2 -0
  259. package/dist/streamgraph/streamgraph.d.ts +70 -0
  260. package/dist/streamgraph/streamgraph.js +613 -0
  261. package/dist/streamgraph/streamgraph.js.map +1 -0
  262. package/dist/styles.css +1 -1
  263. package/dist/swipe-actions/index.d.ts +2 -0
  264. package/dist/swipe-actions/index.js +2 -0
  265. package/dist/swipe-actions/swipe-actions.d.ts +67 -0
  266. package/dist/swipe-actions/swipe-actions.js +552 -0
  267. package/dist/swipe-actions/swipe-actions.js.map +1 -0
  268. package/dist/switch/switch.js +2 -2
  269. package/dist/switch/switch.js.map +1 -1
  270. package/dist/table/index.d.ts +2 -2
  271. package/dist/table/index.js +2 -2
  272. package/dist/table/table.d.ts +56 -4
  273. package/dist/table/table.js +340 -165
  274. package/dist/table/table.js.map +1 -1
  275. package/dist/tabs/tabs.js +2 -2
  276. package/dist/tabs/tabs.js.map +1 -1
  277. package/dist/tag-input/index.d.ts +2 -0
  278. package/dist/tag-input/index.js +2 -0
  279. package/dist/tag-input/tag-input.d.ts +40 -0
  280. package/dist/tag-input/tag-input.js +269 -0
  281. package/dist/tag-input/tag-input.js.map +1 -0
  282. package/dist/text-morph/index.d.ts +2 -0
  283. package/dist/text-morph/index.js +2 -0
  284. package/dist/text-morph/text-morph.d.ts +23 -0
  285. package/dist/text-morph/text-morph.js +21 -0
  286. package/dist/text-morph/text-morph.js.map +1 -0
  287. package/dist/text-reveal/index.d.ts +2 -0
  288. package/dist/text-reveal/index.js +2 -0
  289. package/dist/text-reveal/text-reveal.d.ts +27 -0
  290. package/dist/text-reveal/text-reveal.js +51 -0
  291. package/dist/text-reveal/text-reveal.js.map +1 -0
  292. package/dist/text-shimmer/index.d.ts +2 -0
  293. package/dist/text-shimmer/index.js +2 -0
  294. package/dist/text-shimmer/text-shimmer.d.ts +29 -0
  295. package/dist/text-shimmer/text-shimmer.js +29 -0
  296. package/dist/text-shimmer/text-shimmer.js.map +1 -0
  297. package/dist/time-picker/time-picker.js +2 -2
  298. package/dist/time-picker/time-picker.js.map +1 -1
  299. package/dist/toast/index.d.ts +2 -2
  300. package/dist/toast/index.js +2 -2
  301. package/dist/toast/toast.d.ts +6 -0
  302. package/dist/toast/toast.js +82 -40
  303. package/dist/toast/toast.js.map +1 -1
  304. package/dist/tooltip/tooltip.d.ts +9 -2
  305. package/dist/tooltip/tooltip.js +7 -7
  306. package/dist/tooltip/tooltip.js.map +1 -1
  307. package/dist/treemap/index.d.ts +2 -0
  308. package/dist/treemap/index.js +2 -0
  309. package/dist/treemap/treemap.d.ts +53 -0
  310. package/dist/treemap/treemap.js +699 -0
  311. package/dist/treemap/treemap.js.map +1 -0
  312. package/dist/usage-meter/index.d.ts +2 -0
  313. package/dist/usage-meter/index.js +2 -0
  314. package/dist/usage-meter/usage-meter.d.ts +50 -0
  315. package/dist/usage-meter/usage-meter.js +440 -0
  316. package/dist/usage-meter/usage-meter.js.map +1 -0
  317. package/dist/waffle-chart/index.d.ts +2 -0
  318. package/dist/waffle-chart/index.js +2 -0
  319. package/dist/waffle-chart/waffle-chart.d.ts +57 -0
  320. package/dist/waffle-chart/waffle-chart.js +426 -0
  321. package/dist/waffle-chart/waffle-chart.js.map +1 -0
  322. package/package.json +165 -3
@@ -0,0 +1,1056 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { mergeRefs } from "../lib/merge-refs.js";
4
+ import { prefersReducedMotion, useIsomorphicLayoutEffect } from "../lib/motion.js";
5
+ import { SMOOTH, live, retarget, tween } from "../lib/spring-tween.js";
6
+ import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
7
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
+ //#region src/brush-chart/brush-chart.tsx
9
+ const DAY = 864e5;
10
+ /** Room above the top gridline, the gap between a point and its tooltip, and how close to an edge a press grabs it. */
11
+ const TOP = 28;
12
+ const GAP = 14;
13
+ const GRIP = 14;
14
+ /** Width of an edge handle's hit area. */
15
+ const HANDLE = 28;
16
+ const COMPACT = new Intl.NumberFormat("en-US", {
17
+ notation: "compact",
18
+ maximumFractionDigits: 1
19
+ });
20
+ const GROUPED = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 });
21
+ const utc = (options) => new Intl.DateTimeFormat("en-US", {
22
+ timeZone: "UTC",
23
+ ...options
24
+ });
25
+ const FULL_DATE = utc({
26
+ weekday: "short",
27
+ month: "short",
28
+ day: "numeric",
29
+ year: "numeric"
30
+ });
31
+ const DAY_TICK = utc({
32
+ month: "short",
33
+ day: "numeric"
34
+ });
35
+ const MONTH_TICK = utc({ month: "short" });
36
+ const YEAR_TICK = utc({ year: "numeric" });
37
+ const SHORT_DATE = utc({
38
+ month: "short",
39
+ day: "numeric",
40
+ year: "numeric"
41
+ });
42
+ const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
43
+ const time = (date) => typeof date === "number" ? date : date.getTime();
44
+ const clean = (value) => Number(value.toPrecision(12));
45
+ /** Clean gridlines from zero: the smallest step of 1, 2, 2.5 or 5 that covers the data in four rows or fewer. */
46
+ function niceTop(high) {
47
+ const hi = Math.max(high, 1);
48
+ const magnitude = 10 ** Math.floor(Math.log10(hi / 4));
49
+ for (const factor of [
50
+ 1,
51
+ 2,
52
+ 2.5,
53
+ 5,
54
+ 10
55
+ ]) {
56
+ const step = factor * magnitude;
57
+ const top = Math.ceil(hi / step) * step;
58
+ if (top / step <= 4 + 1e-9) return {
59
+ top: clean(top),
60
+ step: clean(step)
61
+ };
62
+ }
63
+ return {
64
+ top: hi,
65
+ step: hi / 4
66
+ };
67
+ }
68
+ /** Calendar ticks that fit the width: days, weeks, then months, quarters and years, each landing on a real boundary. */
69
+ function timeTicks(start, end, width) {
70
+ const room = Math.max(2, Math.floor(width / 76));
71
+ const span = end - start;
72
+ for (const days of [
73
+ 1,
74
+ 2,
75
+ 7,
76
+ 14
77
+ ]) {
78
+ if (span / (days * DAY) > room) continue;
79
+ const ticks = [];
80
+ let t = Math.ceil(start / DAY) * DAY;
81
+ if (days >= 7) while (new Date(t).getUTCDay() !== 1) t += DAY;
82
+ for (; t <= end; t += days * DAY) ticks.push({
83
+ t,
84
+ label: DAY_TICK.format(t)
85
+ });
86
+ return ticks;
87
+ }
88
+ for (const months of [
89
+ 1,
90
+ 2,
91
+ 3,
92
+ 6,
93
+ 12
94
+ ]) {
95
+ if (span / (months * 30.44 * DAY) > room) continue;
96
+ const ticks = [];
97
+ const first = new Date(start);
98
+ let y = first.getUTCFullYear();
99
+ let m = first.getUTCMonth() + (first.getUTCDate() > 1 || first.getUTCHours() > 0 ? 1 : 0);
100
+ for (;;) {
101
+ y += Math.floor(m / 12);
102
+ m %= 12;
103
+ const t = Date.UTC(y, m, 1);
104
+ if (t > end) break;
105
+ if (m % months === 0) ticks.push({
106
+ t,
107
+ label: m === 0 ? YEAR_TICK.format(t) : MONTH_TICK.format(t)
108
+ });
109
+ m += 1;
110
+ }
111
+ return ticks;
112
+ }
113
+ const ticks = [];
114
+ for (let y = new Date(start).getUTCFullYear() + 1; Date.UTC(y, 0, 1) <= end; y++) ticks.push({
115
+ t: Date.UTC(y, 0, 1),
116
+ label: String(y)
117
+ });
118
+ return ticks;
119
+ }
120
+ /** First index at or after t. */
121
+ function lowerBound(points, t) {
122
+ let low = 0;
123
+ let high = points.length;
124
+ while (low < high) {
125
+ const middle = low + high >> 1;
126
+ if (points[middle].t < t) low = middle + 1;
127
+ else high = middle;
128
+ }
129
+ return low;
130
+ }
131
+ /** A line through the points in view. Past two points per pixel, each pixel column keeps only its low and high so spikes survive. */
132
+ function linePath(points, from, to, x, y, width) {
133
+ const slice = points.slice(Math.max(0, from), Math.min(points.length, to));
134
+ if (!slice.length) return "";
135
+ const parts = [];
136
+ const add = (point) => parts.push(`${x(point.t).toFixed(1)},${y(point.value).toFixed(1)}`);
137
+ if (slice.length <= width * 2) for (const point of slice) add(point);
138
+ else {
139
+ let column = NaN;
140
+ let low = null;
141
+ let high = null;
142
+ const flush = () => {
143
+ if (!low || !high) return;
144
+ for (const point of low.t < high.t ? [low, high] : [high, low]) add(point);
145
+ };
146
+ for (const point of slice) {
147
+ const at = Math.floor(x(point.t));
148
+ if (at !== column) {
149
+ flush();
150
+ column = at;
151
+ low = high = point;
152
+ continue;
153
+ }
154
+ if (point.value < low.value) low = point;
155
+ if (point.value > high.value) high = point;
156
+ }
157
+ flush();
158
+ }
159
+ return `M${parts.join("L")}`;
160
+ }
161
+ /** A centred seven point average and its low to high band, each averaged once more so the edges ease instead of stepping. */
162
+ function smoothed(points) {
163
+ const around = (index, read) => {
164
+ const out = [];
165
+ for (let at = Math.max(0, index - 3); at <= Math.min(points.length - 1, index + 3); at++) out.push(read(at));
166
+ return out;
167
+ };
168
+ const raw = points.map((_, index) => {
169
+ const values = around(index, (at) => points[at].value);
170
+ return {
171
+ value: values.reduce((sum, value) => sum + value, 0) / values.length,
172
+ low: Math.min(...values),
173
+ high: Math.max(...values)
174
+ };
175
+ });
176
+ return points.map((point, index) => {
177
+ const near = around(index, (at) => raw[at]);
178
+ return {
179
+ t: point.t,
180
+ value: raw[index].value,
181
+ low: near.reduce((sum, item) => sum + item.low, 0) / near.length,
182
+ high: near.reduce((sum, item) => sum + item.high, 0) / near.length
183
+ };
184
+ });
185
+ }
186
+ const BrushChart = forwardRef(function BrushChart({ data, label, unit = "", formatValue = (value) => GROUPED.format(value), formatTick = (value) => COMPACT.format(value), formatDate = (date) => FULL_DATE.format(date), annotations, range, defaultRange, onRangeChange, minSpan = 7 * DAY, height = 240, overviewHeight = 52, emptyLabel = "No data yet", className }, ref) {
187
+ const uid = `brush${useId().replace(/[^a-zA-Z0-9-]/g, "")}`;
188
+ const figure = useRef(null);
189
+ const refs = useMemo(() => mergeRefs(ref, figure), [ref]);
190
+ const plot = useRef(null);
191
+ const plotSvg = useRef(null);
192
+ const strip = useRef(null);
193
+ const tip = useRef(null);
194
+ const gutter = useRef(null);
195
+ const axis = useRef(null);
196
+ const points = useMemo(() => data.map((item) => ({
197
+ t: time(item.date),
198
+ value: item.value
199
+ })), [data]);
200
+ const smooth = useMemo(() => smoothed(points), [points]);
201
+ const events = useMemo(() => (annotations ?? []).map((item) => ({
202
+ ...item,
203
+ t: time(item.date)
204
+ })).sort((a, b) => a.t - b.t), [annotations]);
205
+ const empty = points.length < 2;
206
+ const first = points[0]?.t ?? 0;
207
+ const lastT = points[points.length - 1]?.t ?? 1;
208
+ const whole = [first, lastT];
209
+ const fit = ([start, end]) => {
210
+ const span = clamp(end - start, Math.min(minSpan, lastT - first), lastT - first);
211
+ const s = clamp(start, first, lastT - span);
212
+ return [s, s + span];
213
+ };
214
+ const view = useRef(range ?? defaultRange ?? whole);
215
+ const [shown, setShown] = useState(view.current);
216
+ const touched = useRef(false);
217
+ const settleTimer = useRef(0);
218
+ const flight = useRef(null);
219
+ const flush = useCallback(() => {
220
+ window.clearTimeout(settleTimer.current);
221
+ settleTimer.current = 0;
222
+ setShown((now) => now[0] === view.current[0] && now[1] === view.current[1] ? now : [...view.current]);
223
+ }, []);
224
+ const settleSoon = useCallback(() => {
225
+ if (!settleTimer.current) settleTimer.current = window.setTimeout(flush, 100);
226
+ }, [flush]);
227
+ const latest = useRef({
228
+ points,
229
+ smooth,
230
+ events,
231
+ first,
232
+ lastT,
233
+ height,
234
+ overviewHeight,
235
+ empty
236
+ });
237
+ const activeRef = useRef({
238
+ index: null,
239
+ event: null
240
+ });
241
+ const plotW = useRef(0);
242
+ const stripW = useRef(0);
243
+ const topLive = useRef(live(0));
244
+ const aimed = useRef(null);
245
+ const paintRef = useRef(() => {});
246
+ const [plotWidth, setPlotWidth] = useState(0);
247
+ const [stripWidth, setStripWidth] = useState(0);
248
+ /** One writer: the window, every path, the axes, the markers, the crosshair and the tooltip, straight onto the elements. */
249
+ const paint = useCallback(() => {
250
+ const L = latest.current;
251
+ const w = plotW.current;
252
+ const root = plotSvg.current;
253
+ const sw = stripW.current;
254
+ if (!root || L.empty) return;
255
+ const [start, end] = view.current;
256
+ const span = Math.max(1, end - start);
257
+ const P = L.points;
258
+ const x = (t) => (t - start) / span * w;
259
+ const from = Math.max(0, lowerBound(P, start) - 1);
260
+ const to = Math.min(P.length, lowerBound(P, end) + 1);
261
+ let peak = 0;
262
+ for (let index = from; index < to; index++) peak = Math.max(peak, P[index].value);
263
+ const want = niceTop(peak * 1.04).top;
264
+ if (aimed.current === null) {
265
+ aimed.current = want;
266
+ topLive.current.value = want;
267
+ } else if (aimed.current !== want) {
268
+ aimed.current = want;
269
+ retarget(topLive.current, want, SMOOTH, () => paintRef.current());
270
+ }
271
+ const top = topLive.current.value || want;
272
+ const H = L.height;
273
+ const y = (value) => TOP + (1 - value / (top || 1)) * (H - TOP);
274
+ const pxPerPoint = w && P.length > 1 ? w * (L.lastT - L.first) / (P.length - 1) / span : w;
275
+ const detail = clamp((pxPerPoint - 2.75) / 1.5, 0, 1);
276
+ if (w) {
277
+ const line = linePath(P, from, to, x, y, w);
278
+ const area = line ? `${line}L${x(P[to - 1].t).toFixed(1)},${H}L${x(P[from].t).toFixed(1)},${H}Z` : "";
279
+ const S = L.smooth;
280
+ const trend = linePath(S, from, to, x, y, w * 4);
281
+ const upper = linePath(S.map((point) => ({
282
+ t: point.t,
283
+ value: point.high
284
+ })), from, to, x, y, w * 4);
285
+ const lower = linePath(S.map((point) => ({
286
+ t: point.t,
287
+ value: point.low
288
+ })), from, to, x, y, w * 4);
289
+ const band = trend ? `${upper}L${lower.slice(1).split("L").reverse().join("L")}Z` : "";
290
+ const set = (part, d, opacity) => {
291
+ const node = root.querySelector(`[data-part=${part}]`);
292
+ node?.setAttribute("d", d);
293
+ if (node) node.style.opacity = String(opacity);
294
+ };
295
+ set("line", line, detail);
296
+ set("area", area, detail);
297
+ set("trend", trend, 1 - detail);
298
+ set("band", band, 1 - detail);
299
+ }
300
+ for (const node of root.querySelectorAll("[data-grid]")) {
301
+ const value = Number(node.dataset.grid);
302
+ const yy = String(Math.round(y(value)) + .5);
303
+ node.setAttribute("y1", yy);
304
+ node.setAttribute("y2", yy);
305
+ node.style.visibility = value > top * 1.001 ? "hidden" : "visible";
306
+ }
307
+ for (const node of gutter.current?.querySelectorAll("[data-tick]") ?? []) {
308
+ const value = Number(node.dataset.tick);
309
+ node.style.translate = `0 ${Math.round(y(value))}px`;
310
+ node.style.visibility = value > top * 1.001 ? "hidden" : "visible";
311
+ }
312
+ for (const node of axis.current?.querySelectorAll("[data-t]") ?? []) {
313
+ const px = x(Number(node.dataset.t));
314
+ node.style.left = `${px}px`;
315
+ node.style.translate = `${px < 24 ? 0 : px > w - 24 ? -100 : -50}% 0`;
316
+ }
317
+ const marks = [...plot.current?.querySelectorAll("[data-event]") ?? []];
318
+ const inside = marks.filter((node) => {
319
+ const t = L.events[Number(node.dataset.event)].t;
320
+ return t >= start && t <= end;
321
+ });
322
+ marks.forEach((node) => {
323
+ const index = Number(node.dataset.event);
324
+ const t = L.events[index].t;
325
+ const visible = inside.includes(node);
326
+ node.toggleAttribute("data-out", !visible);
327
+ const px = x(t);
328
+ node.style.left = `${px}px`;
329
+ const rule = root.querySelector(`[data-rule="${index}"]`);
330
+ rule?.toggleAttribute("data-out", !visible);
331
+ rule?.setAttribute("x1", String(Math.round(px) + .5));
332
+ rule?.setAttribute("x2", String(Math.round(px) + .5));
333
+ node.removeAttribute("data-labelled");
334
+ node.removeAttribute("data-flip");
335
+ });
336
+ let edge = -Infinity;
337
+ inside.forEach((node, at) => {
338
+ const index = Number(node.dataset.event);
339
+ const px = x(L.events[index].t);
340
+ const text = L.events[index].label.length * 6.2 + 4;
341
+ const next = at + 1 < inside.length ? x(L.events[Number(inside[at + 1].dataset.event)].t) - 10 : w;
342
+ if (px + 14 >= edge && px + 14 + text <= next) {
343
+ node.setAttribute("data-labelled", "");
344
+ edge = px + 14 + text + 8;
345
+ return;
346
+ }
347
+ const before = at === 0 ? -Infinity : x(L.events[Number(inside[at - 1].dataset.event)].t);
348
+ if (px - 14 - text >= Math.max(0, edge) && (at === 0 || px - 14 - text >= before + 12)) {
349
+ node.setAttribute("data-labelled", "");
350
+ node.setAttribute("data-flip", "");
351
+ edge = px + 12;
352
+ return;
353
+ }
354
+ edge = Math.max(edge, px + 12);
355
+ });
356
+ const { index, event } = activeRef.current;
357
+ const at = index !== null && index >= from && index < to ? index : null;
358
+ const cx = at === null ? 0 : x(P[at].t);
359
+ const cy = at === null ? 0 : y(L.smooth[at].value + (P[at].value - L.smooth[at].value) * detail);
360
+ const crossX = String(Math.round(cx) + .5);
361
+ const cross = root.querySelector("[data-part=crosshair]");
362
+ cross?.setAttribute("x1", crossX);
363
+ cross?.setAttribute("x2", crossX);
364
+ if (cross) cross.style.opacity = at === null ? "0" : "";
365
+ const dot = root.querySelector("[data-part=dot]");
366
+ dot?.setAttribute("cx", String(cx));
367
+ dot?.setAttribute("cy", String(cy));
368
+ if (dot) dot.style.opacity = at === null ? "0" : "";
369
+ const bubble = tip.current;
370
+ if (bubble) {
371
+ const ax = event !== null ? x(L.events[event].t) : cx;
372
+ const ay = event !== null ? TOP : cy;
373
+ const tw = bubble.offsetWidth;
374
+ const th = bubble.offsetHeight;
375
+ let left = ax + GAP;
376
+ if (left + tw > w) left = ax - GAP - tw;
377
+ let topY = ay - th - GAP;
378
+ if (topY < 0) topY = ay + GAP;
379
+ bubble.style.translate = `${clamp(left, 0, Math.max(0, w - tw))}px ${clamp(topY, 0, Math.max(0, H - th))}px`;
380
+ }
381
+ const ox = (t) => (t - L.first) / (L.lastT - L.first || 1) * sw;
382
+ const wx0 = ox(start);
383
+ const wx1 = ox(end);
384
+ const box = strip.current;
385
+ if (box) {
386
+ const place = (selector, css) => {
387
+ const node = box.querySelector(selector);
388
+ if (node) Object.assign(node.style, css);
389
+ };
390
+ place("[data-part=window-box]", {
391
+ left: `${wx0}px`,
392
+ width: `${Math.max(0, wx1 - wx0)}px`
393
+ });
394
+ const inward = wx0 < HANDLE;
395
+ place("[data-handle=start]", { left: `${inward ? wx0 : wx0 - HANDLE}px` });
396
+ box.querySelector("[data-handle=start]")?.toggleAttribute("data-inward", inward);
397
+ place("[data-handle=end]", { left: `${wx1}px` });
398
+ place("[data-edge=start]", {
399
+ left: "0px",
400
+ width: `${Math.max(0, wx0)}px`
401
+ });
402
+ place("[data-edge=end]", {
403
+ left: `${wx1}px`,
404
+ right: "0px"
405
+ });
406
+ const clip = box.querySelector("[data-part=window-clip]");
407
+ clip?.setAttribute("x", String(wx0));
408
+ clip?.setAttribute("width", String(Math.max(0, wx1 - wx0)));
409
+ const text = `${SHORT_DATE.format(start)} to ${SHORT_DATE.format(end)}`;
410
+ const slider = (selector, min, max, now, valuetext) => {
411
+ const node = box.querySelector(selector);
412
+ if (!node) return;
413
+ node.setAttribute("aria-valuemin", String(Math.round(min)));
414
+ node.setAttribute("aria-valuemax", String(Math.round(max)));
415
+ node.setAttribute("aria-valuenow", String(Math.round(now)));
416
+ node.setAttribute("aria-valuetext", valuetext);
417
+ };
418
+ slider("[data-part=window]", L.first, L.lastT, start, text);
419
+ slider("[data-handle=start]", L.first, end, start, SHORT_DATE.format(start));
420
+ slider("[data-handle=end]", start, L.lastT, end, SHORT_DATE.format(end));
421
+ }
422
+ }, []);
423
+ useIsomorphicLayoutEffect(() => {
424
+ latest.current = {
425
+ points,
426
+ smooth,
427
+ events,
428
+ first,
429
+ lastT,
430
+ height,
431
+ overviewHeight,
432
+ empty
433
+ };
434
+ paintRef.current = paint;
435
+ paint();
436
+ });
437
+ const setView = (next) => {
438
+ view.current = next;
439
+ paint();
440
+ settleSoon();
441
+ };
442
+ const glideTo = (next) => {
443
+ flight.current?.();
444
+ const from = view.current;
445
+ flight.current = tween(SMOOTH, (p) => setView([from[0] + (next[0] - from[0]) * p, from[1] + (next[1] - from[1]) * p]), flush);
446
+ };
447
+ const commit = (next, smoothly) => {
448
+ const target = fit(next);
449
+ touched.current = true;
450
+ if (smoothly) glideTo(target);
451
+ else {
452
+ flight.current?.();
453
+ setView(target);
454
+ }
455
+ onRangeChange?.(target);
456
+ };
457
+ const rangeKey = range ? `${range[0]}:${range[1]}` : "";
458
+ useEffect(() => {
459
+ if (!range || empty) return;
460
+ const target = fit(range);
461
+ const now = view.current;
462
+ if (Math.abs(target[0] - now[0]) + Math.abs(target[1] - now[1]) > 1e3) glideTo(target);
463
+ }, [rangeKey, empty]);
464
+ useEffect(() => {
465
+ if (empty) return;
466
+ const target = touched.current || range || defaultRange ? fit(view.current) : whole;
467
+ if (target[0] !== view.current[0] || target[1] !== view.current[1]) {
468
+ view.current = target;
469
+ paint();
470
+ flush();
471
+ }
472
+ }, [
473
+ first,
474
+ lastT,
475
+ empty
476
+ ]);
477
+ useEffect(() => () => {
478
+ flight.current?.();
479
+ topLive.current.stop();
480
+ window.clearTimeout(settleTimer.current);
481
+ }, []);
482
+ useIsomorphicLayoutEffect(() => {
483
+ const nodes = [[
484
+ plot.current,
485
+ plotW,
486
+ setPlotWidth
487
+ ], [
488
+ strip.current,
489
+ stripW,
490
+ setStripWidth
491
+ ]];
492
+ const stops = [];
493
+ for (const [node, store, setter] of nodes) {
494
+ if (!node) continue;
495
+ const read = () => {
496
+ store.current = node.clientWidth;
497
+ setter(node.clientWidth);
498
+ paintRef.current();
499
+ };
500
+ read();
501
+ if (typeof ResizeObserver === "undefined") continue;
502
+ const observer = new ResizeObserver(read);
503
+ observer.observe(node);
504
+ stops.push(() => observer.disconnect());
505
+ }
506
+ return () => stops.forEach((stop) => stop());
507
+ }, []);
508
+ const [drawn, setDrawn] = useState(false);
509
+ useEffect(() => {
510
+ if (empty || drawn) return;
511
+ const node = figure.current;
512
+ if (!node || prefersReducedMotion() || typeof IntersectionObserver === "undefined") {
513
+ setDrawn(true);
514
+ return;
515
+ }
516
+ const observer = new IntersectionObserver(([entry]) => {
517
+ if (!entry?.isIntersecting) return;
518
+ observer.disconnect();
519
+ setDrawn(true);
520
+ }, { threshold: .3 });
521
+ observer.observe(node);
522
+ return () => observer.disconnect();
523
+ }, [drawn, empty]);
524
+ const [active, setActive] = useState(null);
525
+ const [hoverEvent, setHoverEvent] = useState(null);
526
+ const [start, end] = shown;
527
+ const from = Math.max(0, lowerBound(points, start) - 1);
528
+ const to = Math.min(points.length, lowerBound(points, end) + 1);
529
+ const index = active !== null && active >= from && active < to ? active : null;
530
+ const eventAt = index === null ? null : events.find((item) => Math.abs(item.t - points[index].t) < DAY / 2) ?? null;
531
+ const shownEvent = hoverEvent !== null ? events[hoverEvent] ?? null : null;
532
+ const tipOn = index !== null || shownEvent !== null;
533
+ useIsomorphicLayoutEffect(() => {
534
+ activeRef.current = {
535
+ index,
536
+ event: shownEvent ? events.indexOf(shownEvent) : null
537
+ };
538
+ paint();
539
+ });
540
+ const nearest = (clientX) => {
541
+ const rect = plot.current?.getBoundingClientRect();
542
+ if (!rect?.width || empty) return null;
543
+ const [s, e] = view.current;
544
+ const t = s + (clientX - rect.left) / rect.width * (e - s);
545
+ const at = clamp(lowerBound(points, t), 1, points.length - 1);
546
+ const lo = Math.max(0, lowerBound(points, s) - 1);
547
+ const hi = Math.min(points.length, lowerBound(points, e) + 1);
548
+ return clamp(t - points[at - 1].t < points[at].t - t ? at - 1 : at, lo, hi - 1);
549
+ };
550
+ const windowBounds = () => {
551
+ const [s, e] = view.current;
552
+ return [Math.max(0, lowerBound(points, s) - 1), Math.min(points.length, lowerBound(points, e) + 1)];
553
+ };
554
+ const onPlotKey = (event) => {
555
+ if (empty) return;
556
+ if (event.key === "Escape" && index !== null) {
557
+ event.preventDefault();
558
+ setActive(null);
559
+ return;
560
+ }
561
+ const [lo, hi] = windowBounds();
562
+ const current = index ?? hi - 1;
563
+ const page = Math.max(1, Math.round((hi - lo) / 8));
564
+ const next = {
565
+ ArrowLeft: current - 1,
566
+ ArrowRight: index === null ? hi - 1 : current + 1,
567
+ PageDown: current - page,
568
+ PageUp: current + page,
569
+ Home: lo,
570
+ End: hi - 1
571
+ }[event.key];
572
+ if (next === void 0) return;
573
+ event.preventDefault();
574
+ setActive(clamp(next, lo, hi - 1));
575
+ };
576
+ const drag = useRef(null);
577
+ const toTime = (clientX) => {
578
+ const rect = strip.current?.getBoundingClientRect();
579
+ return rect?.width ? first + clamp((clientX - rect.left) / rect.width, 0, 1) * (lastT - first) : first;
580
+ };
581
+ const onStripDown = (event) => {
582
+ if (empty || event.button > 0) return;
583
+ const rect = strip.current.getBoundingClientRect();
584
+ const px = event.clientX - rect.left;
585
+ const [s, e] = view.current;
586
+ const wx0 = (s - first) / (lastT - first) * rect.width;
587
+ const wx1 = (e - first) / (lastT - first) * rect.width;
588
+ const mode = Math.abs(px - wx0) <= GRIP && px <= (wx0 + wx1) / 2 ? "start" : Math.abs(px - wx1) <= GRIP ? "end" : px > wx0 && px < wx1 ? "pan" : "new";
589
+ drag.current = {
590
+ mode,
591
+ pointerX: event.clientX,
592
+ from: view.current,
593
+ anchor: toTime(event.clientX),
594
+ moved: false
595
+ };
596
+ try {
597
+ event.currentTarget.setPointerCapture?.(event.pointerId);
598
+ } catch {}
599
+ flight.current?.();
600
+ };
601
+ const onStripMove = (event) => {
602
+ const current = drag.current;
603
+ if (!current) return;
604
+ if (!current.moved && Math.abs(event.clientX - current.pointerX) < 3) return;
605
+ current.moved = true;
606
+ const t = toTime(event.clientX);
607
+ const shift = t - current.anchor;
608
+ const [s, e] = current.from;
609
+ const least = Math.min(minSpan, lastT - first);
610
+ const next = current.mode === "pan" ? [s + shift, e + shift] : current.mode === "start" ? [clamp(t, first, e - least), e] : current.mode === "end" ? [s, clamp(t, s + least, lastT)] : [Math.min(current.anchor, t), Math.max(current.anchor, t)];
611
+ if (current.mode === "new" && next[1] - next[0] < least) {
612
+ if (t >= current.anchor) next[1] = next[0] + least;
613
+ else next[0] = next[1] - least;
614
+ }
615
+ commit(next, false);
616
+ };
617
+ const onStripUp = () => {
618
+ const current = drag.current;
619
+ drag.current = null;
620
+ if (current?.moved) flush();
621
+ if (!current || current.moved || current.mode !== "new") return;
622
+ const half = (view.current[1] - view.current[0]) / 2;
623
+ commit([current.anchor - half, current.anchor + half], true);
624
+ };
625
+ const reset = () => commit(whole, true);
626
+ const stepSize = Math.max(DAY, (lastT - first) / Math.max(1, points.length - 1));
627
+ const onHandleKey = (event, edge) => {
628
+ const [s, e] = view.current;
629
+ const win = e - s;
630
+ const big = event.shiftKey || event.key.startsWith("Page");
631
+ const delta = {
632
+ ArrowLeft: -1,
633
+ ArrowDown: -1,
634
+ PageDown: -1,
635
+ ArrowRight: 1,
636
+ ArrowUp: 1,
637
+ PageUp: 1
638
+ }[event.key];
639
+ let next = null;
640
+ if (event.key === "Escape" || event.key === "0") next = whole;
641
+ else if (edge === "window" && (event.key === "+" || event.key === "=")) next = [s + win / 4, e - win / 4];
642
+ else if (edge === "window" && (event.key === "-" || event.key === "_")) next = [s - win / 2, e + win / 2];
643
+ else if (event.key === "Home") next = edge === "end" ? [s, s + Math.min(minSpan, lastT - first)] : [first, edge === "window" ? first + win : e];
644
+ else if (event.key === "End") next = edge === "start" ? [e - Math.min(minSpan, lastT - first), e] : [edge === "window" ? lastT - win : s, lastT];
645
+ else if (delta) {
646
+ const move = delta * (edge === "window" ? win * (big ? .5 : .1) : stepSize * (big ? 10 : 1));
647
+ next = edge === "window" ? [s + move, e + move] : edge === "start" ? [Math.min(s + move, e - minSpan), e] : [s, Math.max(e + move, s + minSpan)];
648
+ }
649
+ if (!next) return;
650
+ event.preventDefault();
651
+ commit(next, false);
652
+ flush();
653
+ };
654
+ const scale = niceTop(useMemo(() => {
655
+ let high = 0;
656
+ for (let at = from; at < to; at++) high = Math.max(high, points[at].value);
657
+ return high;
658
+ }, [
659
+ points,
660
+ from,
661
+ to
662
+ ]) * 1.04);
663
+ const detailShown = clamp(((plotWidth && points.length > 1 ? plotWidth * (lastT - first) / (points.length - 1) / Math.max(1, end - start) : plotWidth) - 2.75) / 1.5, 0, 1);
664
+ const ticks = scale.top > 0 ? Array.from({ length: Math.round(scale.top / scale.step) + 1 }, (_, at) => clean(at * scale.step)) : [];
665
+ const xTicks = plotWidth && !empty ? timeTicks(start, end, plotWidth) : [];
666
+ const overview = useMemo(() => {
667
+ if (!stripWidth || empty) return {
668
+ line: "",
669
+ area: "",
670
+ marks: []
671
+ };
672
+ const ox = (t) => (t - first) / (lastT - first || 1) * stripWidth;
673
+ const max = niceTop(smooth.reduce((m, point) => Math.max(m, point.value), 0)).top || 1;
674
+ const oy = (value) => 4 + (1 - value / max) * (overviewHeight - 4);
675
+ const path = linePath(smooth, 0, smooth.length, ox, oy, stripWidth * 4);
676
+ const marks = events.map((item) => {
677
+ const at = clamp(lowerBound(smooth, item.t), 0, smooth.length - 1);
678
+ return {
679
+ t: item.t,
680
+ x: ox(item.t),
681
+ y: oy(smooth[at].value)
682
+ };
683
+ });
684
+ return {
685
+ line: path,
686
+ area: `${path}L${stripWidth},${overviewHeight}L0,${overviewHeight}Z`,
687
+ marks
688
+ };
689
+ }, [
690
+ smooth,
691
+ events,
692
+ stripWidth,
693
+ overviewHeight,
694
+ empty,
695
+ first,
696
+ lastT
697
+ ]);
698
+ const format = (value) => `${formatValue(value)}${unit ? ` ${unit}` : ""}`;
699
+ const reading = index === null ? null : points[index];
700
+ const previous = index !== null && index >= 7 ? points[index - 7] : null;
701
+ const change = reading && previous && previous.value ? (reading.value - previous.value) / previous.value : null;
702
+ const windowText = `${SHORT_DATE.format(start)} to ${SHORT_DATE.format(end)}`;
703
+ const announce = reading ? `${formatDate(new Date(reading.t))}, ${format(reading.value)}${eventAt ? `. ${eventAt.label}` : ""}` : "";
704
+ const summary = empty ? `${label}. ${emptyLabel}.` : `${label}, ${SHORT_DATE.format(first)} to ${SHORT_DATE.format(lastT)}. Showing ${windowText}.`;
705
+ const rows = empty ? [] : points.slice(lowerBound(points, start), lowerBound(points, end + 1));
706
+ return /* @__PURE__ */ jsxs("figure", {
707
+ ref: refs,
708
+ "data-slot": "brush-chart",
709
+ "aria-label": label,
710
+ className: cn("brush-chart", className),
711
+ style: { "--series": "var(--color-chart-1)" },
712
+ children: [
713
+ /* @__PURE__ */ jsxs("div", {
714
+ "data-slot": "brush-chart-chart",
715
+ "data-scrubbing": tipOn || void 0,
716
+ className: "brush-chart-frame",
717
+ children: [
718
+ /* @__PURE__ */ jsxs("div", {
719
+ ref: plot,
720
+ "data-slot": "brush-chart-plot",
721
+ role: "slider",
722
+ tabIndex: empty ? -1 : 0,
723
+ "aria-label": `${label}, ${windowText}. Use left and right arrows to read values.`,
724
+ "aria-orientation": "horizontal",
725
+ "aria-valuemin": 1,
726
+ "aria-valuemax": Math.max(1, points.length),
727
+ "aria-valuenow": (index ?? Math.max(0, to - 1)) + 1,
728
+ "aria-valuetext": reading ? announce : summary,
729
+ onPointerMove: (event) => {
730
+ if (event.pointerType === "mouse" || event.buttons) setActive(nearest(event.clientX));
731
+ },
732
+ onPointerDown: (event) => {
733
+ if (event.pointerType === "mouse") return;
734
+ event.currentTarget.setPointerCapture?.(event.pointerId);
735
+ setActive(nearest(event.clientX));
736
+ },
737
+ onPointerUp: (event) => {
738
+ if (event.pointerType !== "mouse") setActive(null);
739
+ },
740
+ onPointerCancel: () => setActive(null),
741
+ onPointerLeave: (event) => {
742
+ if (event.pointerType === "mouse") setActive(null);
743
+ },
744
+ onKeyDown: onPlotKey,
745
+ onBlur: () => setActive(null),
746
+ onDoubleClick: reset,
747
+ onFocus: (event) => {
748
+ if (event.currentTarget.matches(":focus-visible") && !empty) setActive((current) => current ?? windowBounds()[1] - 1);
749
+ },
750
+ className: "line-chart-plot",
751
+ style: { height },
752
+ children: [
753
+ /* @__PURE__ */ jsxs("svg", {
754
+ ref: plotSvg,
755
+ width: "100%",
756
+ height,
757
+ "aria-hidden": "true",
758
+ focusable: "false",
759
+ className: "block overflow-visible",
760
+ children: [
761
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("clipPath", {
762
+ id: `${uid}-draw`,
763
+ clipPathUnits: "userSpaceOnUse",
764
+ children: /* @__PURE__ */ jsx("rect", {
765
+ className: "line-chart-reveal",
766
+ x: -8,
767
+ y: -16,
768
+ height: height + 32,
769
+ style: { width: drawn ? plotWidth + 16 : 0 }
770
+ })
771
+ }) }),
772
+ ticks.slice(1).map((value) => /* @__PURE__ */ jsx("line", {
773
+ "data-grid": value,
774
+ className: "line-chart-grid",
775
+ x1: 0,
776
+ x2: "100%"
777
+ }, value)),
778
+ /* @__PURE__ */ jsx("line", {
779
+ className: "line-chart-baseline",
780
+ x1: 0,
781
+ x2: "100%",
782
+ y1: height - .5,
783
+ y2: height - .5
784
+ }),
785
+ events.map((item, at) => /* @__PURE__ */ jsx("line", {
786
+ "data-rule": at,
787
+ "data-active": shownEvent === item || eventAt === item || void 0,
788
+ className: "brush-chart-rule",
789
+ y1: 18,
790
+ y2: height
791
+ }, item.t)),
792
+ /* @__PURE__ */ jsxs("g", {
793
+ clipPath: `url(#${uid}-draw)`,
794
+ children: [
795
+ /* @__PURE__ */ jsx("path", {
796
+ "data-part": "band",
797
+ className: "brush-chart-band"
798
+ }),
799
+ /* @__PURE__ */ jsx("path", {
800
+ "data-part": "area",
801
+ className: "line-chart-area"
802
+ }),
803
+ /* @__PURE__ */ jsx("path", {
804
+ "data-part": "trend",
805
+ className: "brush-chart-trend"
806
+ }),
807
+ /* @__PURE__ */ jsx("path", {
808
+ "data-part": "line",
809
+ className: "line-chart-line"
810
+ })
811
+ ]
812
+ }),
813
+ /* @__PURE__ */ jsx("line", {
814
+ "data-part": "crosshair",
815
+ className: "line-chart-crosshair",
816
+ y1: 18,
817
+ y2: height
818
+ }),
819
+ /* @__PURE__ */ jsx("circle", {
820
+ "data-part": "dot",
821
+ r: 4,
822
+ className: "line-chart-dot"
823
+ })
824
+ ]
825
+ }),
826
+ events.map((item, at) => /* @__PURE__ */ jsxs("span", {
827
+ "data-event": at,
828
+ "data-active": shownEvent === item || eventAt === item || void 0,
829
+ "aria-hidden": "true",
830
+ className: "brush-chart-event",
831
+ onPointerEnter: () => setHoverEvent(at),
832
+ onPointerLeave: () => setHoverEvent(null),
833
+ onPointerDown: (event) => event.stopPropagation(),
834
+ children: [/* @__PURE__ */ jsx("span", { className: "brush-chart-event-dot" }), /* @__PURE__ */ jsx("span", {
835
+ className: "brush-chart-event-label",
836
+ children: item.label
837
+ })]
838
+ }, item.t)),
839
+ /* @__PURE__ */ jsx("div", {
840
+ ref: tip,
841
+ "aria-hidden": "true",
842
+ "data-slot": "brush-chart-tooltip",
843
+ className: "line-chart-tooltip",
844
+ children: shownEvent ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
845
+ /* @__PURE__ */ jsx("p", {
846
+ className: "m-0 mb-0.5 whitespace-nowrap text-xs tabular-nums text-text-secondary",
847
+ children: formatDate(new Date(shownEvent.t))
848
+ }),
849
+ /* @__PURE__ */ jsx("p", {
850
+ className: "m-0 text-base font-medium tabular-nums text-foreground",
851
+ children: shownEvent.label
852
+ }),
853
+ shownEvent.description ? /* @__PURE__ */ jsx("p", {
854
+ className: "m-0 text-xs text-text-secondary",
855
+ children: shownEvent.description
856
+ }) : null
857
+ ] }) : reading ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
858
+ /* @__PURE__ */ jsx("p", {
859
+ className: "m-0 mb-0.5 whitespace-nowrap text-xs tabular-nums text-text-secondary",
860
+ children: formatDate(new Date(reading.t))
861
+ }),
862
+ /* @__PURE__ */ jsx("p", {
863
+ className: "m-0 text-base font-medium tabular-nums text-foreground",
864
+ children: format(reading.value)
865
+ }),
866
+ index !== null && detailShown < 1 && smooth[index] ? /* @__PURE__ */ jsx("p", {
867
+ className: "m-0 text-xs tabular-nums text-text-secondary",
868
+ children: `${formatValue(Math.round(smooth[index].value))} seven day average`
869
+ }) : null,
870
+ change !== null ? /* @__PURE__ */ jsx("p", {
871
+ className: "m-0 text-xs tabular-nums text-text-secondary",
872
+ children: `${change >= 0 ? "+" : "−"}${Math.abs(change * 100).toFixed(1)}% vs a week earlier`
873
+ }) : null,
874
+ eventAt ? /* @__PURE__ */ jsxs("p", {
875
+ className: "m-0 mt-1 flex items-center gap-1.5 text-xs text-foreground",
876
+ children: [/* @__PURE__ */ jsx("span", {
877
+ "aria-hidden": "true",
878
+ className: "brush-chart-tip-dot"
879
+ }), eventAt.label]
880
+ }) : null
881
+ ] }) : null
882
+ }),
883
+ empty ? /* @__PURE__ */ jsx("p", {
884
+ className: "absolute inset-0 m-0 grid place-items-center text-sm text-muted-foreground pointer-events-none motion-safe:animate-fade-in",
885
+ children: emptyLabel
886
+ }) : null
887
+ ]
888
+ }),
889
+ /* @__PURE__ */ jsx("div", {
890
+ ref: gutter,
891
+ "aria-hidden": "true",
892
+ className: "relative min-w-0 [grid-area:1/2]",
893
+ children: ticks.map((value) => /* @__PURE__ */ jsx("span", {
894
+ "data-tick": value,
895
+ className: "line-chart-tick motion-safe:animate-fade-in",
896
+ children: /* @__PURE__ */ jsx("span", {
897
+ className: "block -translate-y-1/2 text-xs leading-none tabular-nums text-muted-foreground",
898
+ children: formatTick(value)
899
+ })
900
+ }, value))
901
+ }),
902
+ /* @__PURE__ */ jsx("div", {
903
+ ref: axis,
904
+ "aria-hidden": "true",
905
+ className: "relative min-w-0 [grid-area:2/1]",
906
+ children: xTicks.map((tick) => /* @__PURE__ */ jsx("span", {
907
+ "data-t": tick.t,
908
+ className: "absolute top-2 whitespace-nowrap text-xs leading-none tabular-nums text-muted-foreground",
909
+ children: tick.label
910
+ }, tick.t))
911
+ }),
912
+ /* @__PURE__ */ jsxs("div", {
913
+ ref: strip,
914
+ "data-slot": "brush-chart-overview",
915
+ className: "brush-chart-strip",
916
+ style: { height: overviewHeight },
917
+ onPointerDown: onStripDown,
918
+ onPointerMove: onStripMove,
919
+ onPointerUp: onStripUp,
920
+ onPointerCancel: () => {
921
+ drag.current = null;
922
+ },
923
+ onDoubleClick: reset,
924
+ children: [/* @__PURE__ */ jsxs("svg", {
925
+ width: "100%",
926
+ height: overviewHeight,
927
+ "aria-hidden": "true",
928
+ focusable: "false",
929
+ className: "brush-chart-overview-svg",
930
+ children: [
931
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("clipPath", {
932
+ id: `${uid}-window`,
933
+ children: /* @__PURE__ */ jsx("rect", {
934
+ "data-part": "window-clip",
935
+ y: -2,
936
+ height: overviewHeight + 4
937
+ })
938
+ }) }),
939
+ /* @__PURE__ */ jsx("path", {
940
+ d: overview.area,
941
+ className: "brush-chart-overview-area"
942
+ }),
943
+ /* @__PURE__ */ jsx("path", {
944
+ d: overview.line,
945
+ className: "brush-chart-overview-line"
946
+ }),
947
+ /* @__PURE__ */ jsxs("g", {
948
+ clipPath: `url(#${uid}-window)`,
949
+ children: [/* @__PURE__ */ jsx("path", {
950
+ d: overview.area,
951
+ className: "brush-chart-overview-area-in"
952
+ }), /* @__PURE__ */ jsx("path", {
953
+ d: overview.line,
954
+ className: "brush-chart-overview-line-in"
955
+ })]
956
+ }),
957
+ overview.marks.map((mark) => /* @__PURE__ */ jsx("circle", {
958
+ cx: mark.x,
959
+ cy: mark.y,
960
+ r: 2.5,
961
+ className: "brush-chart-overview-event"
962
+ }, mark.t))
963
+ ]
964
+ }), empty ? null : /* @__PURE__ */ jsxs(Fragment$1, { children: [
965
+ /* @__PURE__ */ jsx("div", {
966
+ "data-edge": "start",
967
+ className: "brush-chart-shade"
968
+ }),
969
+ /* @__PURE__ */ jsx("div", {
970
+ "data-edge": "end",
971
+ className: "brush-chart-shade"
972
+ }),
973
+ /* @__PURE__ */ jsx("div", {
974
+ "data-part": "window",
975
+ role: "slider",
976
+ tabIndex: 0,
977
+ "aria-label": `${label} window`,
978
+ "aria-roledescription": "range window",
979
+ onKeyDown: (event) => onHandleKey(event, "window"),
980
+ className: "brush-chart-window-key"
981
+ }),
982
+ /* @__PURE__ */ jsx("div", {
983
+ "data-part": "window-box",
984
+ "aria-hidden": "true",
985
+ className: "brush-chart-window"
986
+ }),
987
+ /* @__PURE__ */ jsx("div", {
988
+ "data-handle": "start",
989
+ role: "slider",
990
+ tabIndex: 0,
991
+ "aria-label": "Window start",
992
+ onKeyDown: (event) => onHandleKey(event, "start"),
993
+ className: "brush-chart-handle",
994
+ children: /* @__PURE__ */ jsx("span", { className: "brush-chart-grip" })
995
+ }),
996
+ /* @__PURE__ */ jsx("div", {
997
+ "data-handle": "end",
998
+ role: "slider",
999
+ tabIndex: 0,
1000
+ "aria-label": "Window end",
1001
+ onKeyDown: (event) => onHandleKey(event, "end"),
1002
+ className: "brush-chart-handle",
1003
+ children: /* @__PURE__ */ jsx("span", { className: "brush-chart-grip" })
1004
+ })
1005
+ ] })]
1006
+ })
1007
+ ]
1008
+ }),
1009
+ /* @__PURE__ */ jsx("p", {
1010
+ className: "sr-only",
1011
+ "aria-live": "polite",
1012
+ "aria-atomic": "true",
1013
+ children: announce
1014
+ }),
1015
+ /* @__PURE__ */ jsx("p", {
1016
+ className: "sr-only",
1017
+ children: summary
1018
+ }),
1019
+ empty ? null : /* @__PURE__ */ jsx("div", {
1020
+ className: "sr-only",
1021
+ children: /* @__PURE__ */ jsxs("table", { children: [
1022
+ /* @__PURE__ */ jsx("caption", { children: `${label}, ${windowText}` }),
1023
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1024
+ /* @__PURE__ */ jsx("th", {
1025
+ scope: "col",
1026
+ children: "Date"
1027
+ }),
1028
+ /* @__PURE__ */ jsx("th", {
1029
+ scope: "col",
1030
+ children: label
1031
+ }),
1032
+ /* @__PURE__ */ jsx("th", {
1033
+ scope: "col",
1034
+ children: "Event"
1035
+ })
1036
+ ] }) }),
1037
+ /* @__PURE__ */ jsx("tbody", { children: rows.map((point) => {
1038
+ const event = events.find((item) => Math.abs(item.t - point.t) < DAY / 2);
1039
+ return /* @__PURE__ */ jsxs("tr", { children: [
1040
+ /* @__PURE__ */ jsx("th", {
1041
+ scope: "row",
1042
+ children: SHORT_DATE.format(point.t)
1043
+ }),
1044
+ /* @__PURE__ */ jsx("td", { children: format(point.value) }),
1045
+ /* @__PURE__ */ jsx("td", { children: event ? `${event.label}${event.description ? `. ${event.description}` : ""}` : "" })
1046
+ ] }, point.t);
1047
+ }) })
1048
+ ] })
1049
+ })
1050
+ ]
1051
+ });
1052
+ });
1053
+ //#endregion
1054
+ export { BrushChart };
1055
+
1056
+ //# sourceMappingURL=brush-chart.js.map