@stackframe/dashboard-ui-components 2.8.84 → 2.8.85

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 (229) hide show
  1. package/dist/components/analytics-chart/analytics-chart-pie.d.ts +67 -0
  2. package/dist/components/analytics-chart/analytics-chart-pie.d.ts.map +1 -0
  3. package/dist/components/analytics-chart/analytics-chart-pie.js +253 -0
  4. package/dist/components/analytics-chart/analytics-chart-pie.js.map +1 -0
  5. package/dist/components/analytics-chart/analytics-chart.d.ts +554 -0
  6. package/dist/components/analytics-chart/analytics-chart.d.ts.map +1 -0
  7. package/dist/components/analytics-chart/analytics-chart.js +1021 -0
  8. package/dist/components/analytics-chart/analytics-chart.js.map +1 -0
  9. package/dist/components/analytics-chart/default-analytics-chart-tooltip.d.ts +66 -0
  10. package/dist/components/analytics-chart/default-analytics-chart-tooltip.d.ts.map +1 -0
  11. package/dist/components/analytics-chart/default-analytics-chart-tooltip.js +179 -0
  12. package/dist/components/analytics-chart/default-analytics-chart-tooltip.js.map +1 -0
  13. package/dist/components/analytics-chart/format.d.ts +13 -0
  14. package/dist/components/analytics-chart/format.d.ts.map +1 -0
  15. package/dist/components/analytics-chart/format.js +138 -0
  16. package/dist/components/analytics-chart/format.js.map +1 -0
  17. package/dist/components/analytics-chart/index.d.ts +8 -0
  18. package/dist/components/analytics-chart/index.js +184 -0
  19. package/dist/components/analytics-chart/palette.d.ts +15 -0
  20. package/dist/components/analytics-chart/palette.d.ts.map +1 -0
  21. package/dist/components/analytics-chart/palette.js +60 -0
  22. package/dist/components/analytics-chart/palette.js.map +1 -0
  23. package/dist/components/analytics-chart/render-data-series.d.ts +28 -0
  24. package/dist/components/analytics-chart/render-data-series.d.ts.map +1 -0
  25. package/dist/components/analytics-chart/render-data-series.js +109 -0
  26. package/dist/components/analytics-chart/render-data-series.js.map +1 -0
  27. package/dist/components/analytics-chart/state.d.ts +54 -0
  28. package/dist/components/analytics-chart/state.d.ts.map +1 -0
  29. package/dist/components/analytics-chart/state.js +142 -0
  30. package/dist/components/analytics-chart/state.js.map +1 -0
  31. package/dist/components/analytics-chart/strings.d.ts +33 -0
  32. package/dist/components/analytics-chart/strings.d.ts.map +1 -0
  33. package/dist/components/analytics-chart/strings.js +37 -0
  34. package/dist/components/analytics-chart/strings.js.map +1 -0
  35. package/dist/components/analytics-chart/types.d.ts +157 -0
  36. package/dist/components/analytics-chart/types.d.ts.map +1 -0
  37. package/dist/components/analytics-chart/types.js +21 -0
  38. package/dist/components/analytics-chart/types.js.map +1 -0
  39. package/dist/components/badge.d.ts +16 -0
  40. package/dist/components/badge.d.ts.map +1 -1
  41. package/dist/components/badge.js +16 -0
  42. package/dist/components/badge.js.map +1 -1
  43. package/dist/components/button.d.ts +15 -1
  44. package/dist/components/button.d.ts.map +1 -1
  45. package/dist/components/button.js +14 -0
  46. package/dist/components/button.js.map +1 -1
  47. package/dist/components/card.d.ts +28 -0
  48. package/dist/components/card.d.ts.map +1 -1
  49. package/dist/components/card.js +28 -0
  50. package/dist/components/card.js.map +1 -1
  51. package/dist/components/chart-card.d.ts +29 -0
  52. package/dist/components/chart-card.d.ts.map +1 -1
  53. package/dist/components/chart-card.js +29 -0
  54. package/dist/components/chart-card.js.map +1 -1
  55. package/dist/components/chart-legend.d.ts +1 -2
  56. package/dist/components/chart-legend.d.ts.map +1 -1
  57. package/dist/components/chart-legend.js +0 -4
  58. package/dist/components/chart-legend.js.map +1 -1
  59. package/dist/components/data-grid/data-grid-sizing.d.ts +11 -0
  60. package/dist/components/data-grid/data-grid-sizing.d.ts.map +1 -0
  61. package/dist/components/data-grid/data-grid-sizing.js +34 -0
  62. package/dist/components/data-grid/data-grid-sizing.js.map +1 -0
  63. package/dist/components/data-grid/data-grid-toolbar.d.ts +31 -0
  64. package/dist/components/data-grid/data-grid-toolbar.d.ts.map +1 -0
  65. package/dist/components/data-grid/data-grid-toolbar.js +226 -0
  66. package/dist/components/data-grid/data-grid-toolbar.js.map +1 -0
  67. package/dist/components/data-grid/data-grid.d.ts +233 -0
  68. package/dist/components/data-grid/data-grid.d.ts.map +1 -0
  69. package/dist/components/data-grid/data-grid.js +871 -0
  70. package/dist/components/data-grid/data-grid.js.map +1 -0
  71. package/dist/components/data-grid/index.d.ts +7 -0
  72. package/dist/components/data-grid/index.js +176 -0
  73. package/dist/components/data-grid/state.d.ts +91 -0
  74. package/dist/components/data-grid/state.d.ts.map +1 -0
  75. package/dist/components/data-grid/state.js +329 -0
  76. package/dist/components/data-grid/state.js.map +1 -0
  77. package/dist/components/data-grid/strings.d.ts +8 -0
  78. package/dist/components/data-grid/strings.d.ts.map +1 -0
  79. package/dist/components/data-grid/strings.js +42 -0
  80. package/dist/components/data-grid/strings.js.map +1 -0
  81. package/dist/components/data-grid/types.d.ts +242 -0
  82. package/dist/components/data-grid/types.d.ts.map +1 -0
  83. package/dist/components/data-grid/types.js +0 -0
  84. package/dist/components/data-grid/use-data-source.d.ts +79 -0
  85. package/dist/components/data-grid/use-data-source.d.ts.map +1 -0
  86. package/dist/components/data-grid/use-data-source.js +236 -0
  87. package/dist/components/data-grid/use-data-source.js.map +1 -0
  88. package/dist/components/empty-state.d.ts +16 -0
  89. package/dist/components/empty-state.d.ts.map +1 -1
  90. package/dist/components/empty-state.js +16 -0
  91. package/dist/components/empty-state.js.map +1 -1
  92. package/dist/components/metric-card.d.ts +24 -0
  93. package/dist/components/metric-card.d.ts.map +1 -1
  94. package/dist/components/metric-card.js +24 -0
  95. package/dist/components/metric-card.js.map +1 -1
  96. package/dist/components/progress-bar.d.ts +10 -0
  97. package/dist/components/progress-bar.d.ts.map +1 -1
  98. package/dist/components/progress-bar.js +10 -0
  99. package/dist/components/progress-bar.js.map +1 -1
  100. package/dist/components/separator.d.ts +9 -0
  101. package/dist/components/separator.d.ts.map +1 -1
  102. package/dist/components/separator.js +9 -0
  103. package/dist/components/separator.js.map +1 -1
  104. package/dist/components/skeleton.d.ts +12 -0
  105. package/dist/components/skeleton.d.ts.map +1 -1
  106. package/dist/components/skeleton.js +12 -0
  107. package/dist/components/skeleton.js.map +1 -1
  108. package/dist/components/table.d.ts +25 -0
  109. package/dist/components/table.d.ts.map +1 -1
  110. package/dist/components/table.js +25 -0
  111. package/dist/components/table.js.map +1 -1
  112. package/dist/dashboard-ui-components.global.js +8607 -2902
  113. package/dist/dashboard-ui-components.global.js.map +4 -4
  114. package/dist/esm/components/analytics-chart/analytics-chart-pie.d.ts +67 -0
  115. package/dist/esm/components/analytics-chart/analytics-chart-pie.d.ts.map +1 -0
  116. package/dist/esm/components/analytics-chart/analytics-chart-pie.js +251 -0
  117. package/dist/esm/components/analytics-chart/analytics-chart-pie.js.map +1 -0
  118. package/dist/esm/components/analytics-chart/analytics-chart.d.ts +554 -0
  119. package/dist/esm/components/analytics-chart/analytics-chart.d.ts.map +1 -0
  120. package/dist/esm/components/analytics-chart/analytics-chart.js +1019 -0
  121. package/dist/esm/components/analytics-chart/analytics-chart.js.map +1 -0
  122. package/dist/esm/components/analytics-chart/default-analytics-chart-tooltip.d.ts +66 -0
  123. package/dist/esm/components/analytics-chart/default-analytics-chart-tooltip.d.ts.map +1 -0
  124. package/dist/esm/components/analytics-chart/default-analytics-chart-tooltip.js +176 -0
  125. package/dist/esm/components/analytics-chart/default-analytics-chart-tooltip.js.map +1 -0
  126. package/dist/esm/components/analytics-chart/format.d.ts +13 -0
  127. package/dist/esm/components/analytics-chart/format.d.ts.map +1 -0
  128. package/dist/esm/components/analytics-chart/format.js +133 -0
  129. package/dist/esm/components/analytics-chart/format.js.map +1 -0
  130. package/dist/esm/components/analytics-chart/index.d.ts +8 -0
  131. package/dist/esm/components/analytics-chart/index.js +9 -0
  132. package/dist/esm/components/analytics-chart/palette.d.ts +15 -0
  133. package/dist/esm/components/analytics-chart/palette.d.ts.map +1 -0
  134. package/dist/esm/components/analytics-chart/palette.js +55 -0
  135. package/dist/esm/components/analytics-chart/palette.js.map +1 -0
  136. package/dist/esm/components/analytics-chart/render-data-series.d.ts +28 -0
  137. package/dist/esm/components/analytics-chart/render-data-series.d.ts.map +1 -0
  138. package/dist/esm/components/analytics-chart/render-data-series.js +107 -0
  139. package/dist/esm/components/analytics-chart/render-data-series.js.map +1 -0
  140. package/dist/esm/components/analytics-chart/state.d.ts +54 -0
  141. package/dist/esm/components/analytics-chart/state.d.ts.map +1 -0
  142. package/dist/esm/components/analytics-chart/state.js +126 -0
  143. package/dist/esm/components/analytics-chart/state.js.map +1 -0
  144. package/dist/esm/components/analytics-chart/strings.d.ts +33 -0
  145. package/dist/esm/components/analytics-chart/strings.d.ts.map +1 -0
  146. package/dist/esm/components/analytics-chart/strings.js +34 -0
  147. package/dist/esm/components/analytics-chart/strings.js.map +1 -0
  148. package/dist/esm/components/analytics-chart/types.d.ts +157 -0
  149. package/dist/esm/components/analytics-chart/types.d.ts.map +1 -0
  150. package/dist/esm/components/analytics-chart/types.js +18 -0
  151. package/dist/esm/components/analytics-chart/types.js.map +1 -0
  152. package/dist/esm/components/badge.d.ts +16 -0
  153. package/dist/esm/components/badge.d.ts.map +1 -1
  154. package/dist/esm/components/badge.js +16 -0
  155. package/dist/esm/components/badge.js.map +1 -1
  156. package/dist/esm/components/button.d.ts +14 -0
  157. package/dist/esm/components/button.d.ts.map +1 -1
  158. package/dist/esm/components/button.js +14 -0
  159. package/dist/esm/components/button.js.map +1 -1
  160. package/dist/esm/components/card.d.ts +28 -0
  161. package/dist/esm/components/card.d.ts.map +1 -1
  162. package/dist/esm/components/card.js +28 -0
  163. package/dist/esm/components/card.js.map +1 -1
  164. package/dist/esm/components/chart-card.d.ts +29 -0
  165. package/dist/esm/components/chart-card.d.ts.map +1 -1
  166. package/dist/esm/components/chart-card.js +29 -0
  167. package/dist/esm/components/chart-card.js.map +1 -1
  168. package/dist/esm/components/chart-legend.d.ts +1 -2
  169. package/dist/esm/components/chart-legend.d.ts.map +1 -1
  170. package/dist/esm/components/chart-legend.js +1 -3
  171. package/dist/esm/components/chart-legend.js.map +1 -1
  172. package/dist/esm/components/data-grid/data-grid-sizing.d.ts +11 -0
  173. package/dist/esm/components/data-grid/data-grid-sizing.d.ts.map +1 -0
  174. package/dist/esm/components/data-grid/data-grid-sizing.js +29 -0
  175. package/dist/esm/components/data-grid/data-grid-sizing.js.map +1 -0
  176. package/dist/esm/components/data-grid/data-grid-toolbar.d.ts +31 -0
  177. package/dist/esm/components/data-grid/data-grid-toolbar.d.ts.map +1 -0
  178. package/dist/esm/components/data-grid/data-grid-toolbar.js +223 -0
  179. package/dist/esm/components/data-grid/data-grid-toolbar.js.map +1 -0
  180. package/dist/esm/components/data-grid/data-grid.d.ts +233 -0
  181. package/dist/esm/components/data-grid/data-grid.d.ts.map +1 -0
  182. package/dist/esm/components/data-grid/data-grid.js +868 -0
  183. package/dist/esm/components/data-grid/data-grid.js.map +1 -0
  184. package/dist/esm/components/data-grid/index.d.ts +7 -0
  185. package/dist/esm/components/data-grid/index.js +7 -0
  186. package/dist/esm/components/data-grid/state.d.ts +91 -0
  187. package/dist/esm/components/data-grid/state.d.ts.map +1 -0
  188. package/dist/esm/components/data-grid/state.js +305 -0
  189. package/dist/esm/components/data-grid/state.js.map +1 -0
  190. package/dist/esm/components/data-grid/strings.d.ts +8 -0
  191. package/dist/esm/components/data-grid/strings.d.ts.map +1 -0
  192. package/dist/esm/components/data-grid/strings.js +39 -0
  193. package/dist/esm/components/data-grid/strings.js.map +1 -0
  194. package/dist/esm/components/data-grid/types.d.ts +242 -0
  195. package/dist/esm/components/data-grid/types.d.ts.map +1 -0
  196. package/dist/esm/components/data-grid/types.js +1 -0
  197. package/dist/esm/components/data-grid/use-data-source.d.ts +79 -0
  198. package/dist/esm/components/data-grid/use-data-source.d.ts.map +1 -0
  199. package/dist/esm/components/data-grid/use-data-source.js +234 -0
  200. package/dist/esm/components/data-grid/use-data-source.js.map +1 -0
  201. package/dist/esm/components/empty-state.d.ts +16 -0
  202. package/dist/esm/components/empty-state.d.ts.map +1 -1
  203. package/dist/esm/components/empty-state.js +16 -0
  204. package/dist/esm/components/empty-state.js.map +1 -1
  205. package/dist/esm/components/metric-card.d.ts +24 -0
  206. package/dist/esm/components/metric-card.d.ts.map +1 -1
  207. package/dist/esm/components/metric-card.js +24 -0
  208. package/dist/esm/components/metric-card.js.map +1 -1
  209. package/dist/esm/components/progress-bar.d.ts +10 -0
  210. package/dist/esm/components/progress-bar.d.ts.map +1 -1
  211. package/dist/esm/components/progress-bar.js +10 -0
  212. package/dist/esm/components/progress-bar.js.map +1 -1
  213. package/dist/esm/components/separator.d.ts +9 -0
  214. package/dist/esm/components/separator.d.ts.map +1 -1
  215. package/dist/esm/components/separator.js +9 -0
  216. package/dist/esm/components/separator.js.map +1 -1
  217. package/dist/esm/components/skeleton.d.ts +12 -0
  218. package/dist/esm/components/skeleton.d.ts.map +1 -1
  219. package/dist/esm/components/skeleton.js +12 -0
  220. package/dist/esm/components/skeleton.js.map +1 -1
  221. package/dist/esm/components/table.d.ts +25 -0
  222. package/dist/esm/components/table.d.ts.map +1 -1
  223. package/dist/esm/components/table.js +25 -0
  224. package/dist/esm/components/table.js.map +1 -1
  225. package/dist/esm/index.d.ts +4 -2
  226. package/dist/esm/index.js +6 -2
  227. package/dist/index.d.ts +15 -2
  228. package/dist/index.js +16 -7
  229. package/package.json +4 -3
@@ -0,0 +1,1019 @@
1
+ "use client";
2
+
3
+ import { FlagIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, XIcon } from "@phosphor-icons/react";
4
+ import { cn } from "@stackframe/stack-ui";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
+ import { CartesianGrid, ComposedChart, ReferenceArea, ReferenceLine, XAxis, YAxis } from "recharts";
8
+ import { DesignChartContainer } from "../chart-container.js";
9
+ import { DefaultAnalyticsChartTooltip } from "./default-analytics-chart-tooltip.js";
10
+ import { formatDelta, formatValue } from "./format.js";
11
+ import { cssIdent, pointValue } from "./types.js";
12
+ import { DesignButton } from "../button.js";
13
+ import { AnalyticsChartPie } from "./analytics-chart-pie.js";
14
+ import { buildRampColors, resolveAnalyticsChartPalette } from "./palette.js";
15
+ import { renderDataSeries } from "./render-data-series.js";
16
+ import { EMPTY_MATRIX, EMPTY_SERIES, STROKE_DASHARRAY, computeLocalInProgressIdx, findAnnotationsLayer, findCompareLayer, findPrimaryLayer, isAnalyticsChartDataLayer, isTimeseriesState, resolveDataLayerStyle } from "./state.js";
17
+ import { resolveAnalyticsChartStrings } from "./strings.js";
18
+
19
+ //#region src/components/analytics-chart/analytics-chart.tsx
20
+ const FALLBACK_PRIMARY_STYLE = {
21
+ color: "#2563eb",
22
+ type: "area",
23
+ strokeStyle: "solid",
24
+ fillOpacity: 0
25
+ };
26
+ const FALLBACK_COMPARE_STYLE = {
27
+ color: "#f59e0b",
28
+ type: "line",
29
+ strokeStyle: "dashed",
30
+ fillOpacity: 0
31
+ };
32
+ const FALLBACK_ANNOTATION_COLOR = "#f59e0b";
33
+ function buildTooltipLayerView(args) {
34
+ const { show, layer, color, segmented, segmentSeries, segmentRows, segmentTotals, segmentColorsLight, segmentColorsDark, activeIndex, activePoint, fallbackLabel } = args;
35
+ if (!show || !layer) return null;
36
+ const segments = segmented ? segmentSeries.map((s, sIdx) => ({
37
+ key: s.key,
38
+ label: s.label,
39
+ value: segmentRows[activeIndex]?.[sIdx] ?? 0,
40
+ color: segmentColorsLight[sIdx],
41
+ colorDark: segmentColorsDark[sIdx]
42
+ })) : [];
43
+ return {
44
+ id: layer.id,
45
+ label: layer.label || fallbackLabel || "",
46
+ color,
47
+ colorDark: color,
48
+ total: segmented ? segmentTotals[activeIndex] ?? 0 : pointValue(activePoint, layer.id),
49
+ segmented,
50
+ segments
51
+ };
52
+ }
53
+ /**
54
+ * Preferred chart for all time-series: area, line, bar, compare layers,
55
+ * segmented stacks, tooltips, zoom, and annotations. Wrap in
56
+ * `DesignChartCard` for the title/description chrome. Only fall back to
57
+ * raw Recharts for non-time-series visuals (static rankings etc.).
58
+ *
59
+ * ## Data shape
60
+ *
61
+ * `data` is `Point[]`, where `Point = { ts: number, values: Record<string, number> }`.
62
+ * `ts` is a Unix milliseconds timestamp. `values` maps layer id → numeric value
63
+ * at that bucket. Example:
64
+ *
65
+ * ```ts
66
+ * { ts: 1743465600000, values: { primary: 420 } }
67
+ * { ts: 1743465600000, values: { primary: 420, compare: 380 } } // with compare layer
68
+ * ```
69
+ *
70
+ * ## State is fully controlled — start from ANALYTICS_CHART_DEFAULT_STATE
71
+ *
72
+ * The default state ships with three pre-configured layers: `"primary"`,
73
+ * `"compare"`, and `"annotations"`. ALWAYS spread from
74
+ * `ANALYTICS_CHART_DEFAULT_STATE` and map over `layers` to override. Do NOT
75
+ * hand-build the layer array from scratch — you will miss fields and crash.
76
+ *
77
+ * ```ts
78
+ * // Default state shape (for reference — spread from the constant, don't copy):
79
+ * {
80
+ * view: "timeseries",
81
+ * layers: [
82
+ * { id: "primary", kind: "primary", label: "Current", visible: true, color: "#2563eb",
83
+ * segmented: false, type: "area", strokeStyle: "solid", fillOpacity: 0.22, inProgressFromIndex: null },
84
+ * { id: "compare", kind: "compare", label: "Previous period", visible: true, color: "#f59e0b",
85
+ * segmented: false, type: "line", strokeStyle: "dashed", inProgressFromIndex: null },
86
+ * { id: "annotations", kind: "annotations", label: "Annotations", visible: true, color: "#f59e0b" },
87
+ * ],
88
+ * xFormatKind: { type: "datetime", style: "short" },
89
+ * yFormatKind: { type: "short" },
90
+ * showGrid: true, showXAxis: true, showYAxis: true,
91
+ * zoomRange: null, pinnedIndex: null,
92
+ * }
93
+ * ```
94
+ *
95
+ * ## onChange — CRITICAL, get this right
96
+ *
97
+ * `onChange` fires with an `AnalyticsChartState` object — NOT your custom
98
+ * wrapper. If you store chart data and state together, `onChange` MUST only
99
+ * update the state part. Keep data and state in SEPARATE hooks:
100
+ *
101
+ * ```tsx
102
+ * // WRONG — overwrites your data with a bare state object, crashes on next render:
103
+ * const [combined, setCombined] = React.useState({ data: [], state: ANALYTICS_CHART_DEFAULT_STATE });
104
+ * <AnalyticsChart data={combined.data} state={combined.state} onChange={setCombined} />
105
+ *
106
+ * // RIGHT — two hooks:
107
+ * const [data, setData] = React.useState([]);
108
+ * const [chartState, setChartState] = React.useState({ ...ANALYTICS_CHART_DEFAULT_STATE });
109
+ * <AnalyticsChart data={data} state={chartState} onChange={setChartState} />
110
+ * ```
111
+ *
112
+ * NEVER pass a setter that manages a combined `{ data, state }` object directly to `onChange`.
113
+ *
114
+ * ## Common patterns
115
+ *
116
+ * ### 1. Simplest — one area layer, no compare
117
+ *
118
+ * ```tsx
119
+ * const data = rows.map(r => ({ ts: r.bucketTs, values: { primary: r.count } }));
120
+ * const [state, setState] = React.useState({
121
+ * ...ANALYTICS_CHART_DEFAULT_STATE,
122
+ * layers: ANALYTICS_CHART_DEFAULT_STATE.layers.map(l =>
123
+ * l.kind === "compare" ? { ...l, visible: false } : l
124
+ * ),
125
+ * });
126
+ * <DesignChartCard title="Signups" description="Last 30 days">
127
+ * <AnalyticsChart data={data} state={state} onChange={setState} />
128
+ * </DesignChartCard>
129
+ * ```
130
+ *
131
+ * ### 2. Current vs previous period (compare)
132
+ *
133
+ * ```tsx
134
+ * const data = rows.map(r => ({
135
+ * ts: r.bucketTs,
136
+ * values: { primary: r.thisPeriod, compare: r.lastPeriod },
137
+ * }));
138
+ * const [state, setState] = React.useState(ANALYTICS_CHART_DEFAULT_STATE);
139
+ * <AnalyticsChart data={data} state={state} onChange={setState} />
140
+ * ```
141
+ *
142
+ * ### 3. Stacked bar with breakdown (segmented)
143
+ *
144
+ * ```tsx
145
+ * const regions = [{ key: "us", label: "US" }, { key: "eu", label: "EU" }];
146
+ * const segments = rows.map(r => [r.signupsUs, r.signupsEu]); // MUST sum to primary value per row
147
+ * const [state, setState] = React.useState({
148
+ * ...ANALYTICS_CHART_DEFAULT_STATE,
149
+ * layers: ANALYTICS_CHART_DEFAULT_STATE.layers.map(l => {
150
+ * if (l.kind === "primary") return { ...l, type: "bar", segmented: true, segments, segmentSeries: regions };
151
+ * if (l.kind === "compare") return { ...l, visible: false };
152
+ * return l;
153
+ * }),
154
+ * });
155
+ * <AnalyticsChart data={data} state={state} onChange={setState} />
156
+ * ```
157
+ *
158
+ * ### 4. Two metrics on one chart (revenue bars + signups area)
159
+ *
160
+ * ```tsx
161
+ * // IMPORTANT: metrics share one y-axis, so normalize into the same range.
162
+ * const data = rows.map(r => ({
163
+ * ts: r.bucketTs,
164
+ * values: { revenue: r.revenueCents / 100, signups: r.signups },
165
+ * }));
166
+ * const [state, setState] = React.useState({
167
+ * ...ANALYTICS_CHART_DEFAULT_STATE,
168
+ * layers: ANALYTICS_CHART_DEFAULT_STATE.layers.map(l => {
169
+ * if (l.kind === "primary") return { ...l, id: "revenue", label: "Revenue", type: "bar" };
170
+ * if (l.kind === "compare") return { ...l, id: "signups", label: "Sign-ups", type: "area", visible: true };
171
+ * return l;
172
+ * }),
173
+ * });
174
+ * <AnalyticsChart data={data} state={state} onChange={setState} />
175
+ * ```
176
+ *
177
+ * ### 5. Pie view (distribution / breakdown, non-time-series)
178
+ *
179
+ * Pie needs one data point, `segments` with one row, and `segmentSeries` with labels:
180
+ *
181
+ * ```tsx
182
+ * const categories = [{ key: "verified", label: "Verified" }, { key: "unverified", label: "Unverified" }, { key: "anonymous", label: "Anonymous" }];
183
+ * const total = verified + unverified + anonymous;
184
+ * const data = [{ ts: 0, values: { primary: total } }];
185
+ * const segments = [[verified, unverified, anonymous]]; // one row; values sum to total
186
+ * const [state, setState] = React.useState({
187
+ * ...ANALYTICS_CHART_DEFAULT_STATE,
188
+ * view: "pie",
189
+ * layers: ANALYTICS_CHART_DEFAULT_STATE.layers.map(l => {
190
+ * if (l.kind === "primary") return { ...l, segmented: true, segments, segmentSeries: categories };
191
+ * if (l.kind === "compare") return { ...l, visible: false };
192
+ * return l;
193
+ * }),
194
+ * });
195
+ * <AnalyticsChart data={data} state={state} onChange={setState} />
196
+ * ```
197
+ *
198
+ * ## Segment data contract (MUST follow when segmented: true)
199
+ *
200
+ * `segments` is a 2D array: `segments[dayIndex][categoryIndex] = number`.
201
+ *
202
+ * - Outer length MUST equal `data.length` (one row per Point).
203
+ * - Inner length MUST equal `segmentSeries.length` (one value per category).
204
+ * - Each row MUST sum to `data[dayIndex].values[layerId]` (the layer's total for that day).
205
+ * - `segmentSeries` defines the category labels, in the SAME order as segment columns.
206
+ *
207
+ * Example: if `segmentSeries = [{ key: "us", label: "US" }, { key: "eu", label: "EU" }]`
208
+ * and `data[0].values.primary = 420`, then `segments[0]` must be `[usValue, euValue]`
209
+ * where `usValue + euValue === 420`. If rows don't sum to the layer total, stacked bars
210
+ * will render incorrectly (gaps or overflow).
211
+ *
212
+ * ## Palette
213
+ *
214
+ * AnalyticsChart auto-generates segment colors (blue shades for primary, amber for
215
+ * compare). You do NOT need to pass a palette prop — it just works. Segment keys
216
+ * can be any string; the component sanitizes them for CSS purposes internally.
217
+ *
218
+ * ## Layer quick reference
219
+ *
220
+ * - Layer `type` options: `"area" | "line" | "bar"`
221
+ * - Layer `kind` values: `"primary" | "compare" | "annotations"`
222
+ * - To hide a layer: `{ ...l, visible: false }`
223
+ * - To switch chart type: `{ ...l, type: "bar" }` (or `"line"`, `"area"`)
224
+ * - To rename a layer: `{ ...l, id: "myMetric", label: "My Metric" }`
225
+ *
226
+ * ## Formatting (xFormatKind / yFormatKind on state)
227
+ *
228
+ * - `{ type: "numeric" }` — plain number
229
+ * - `{ type: "short" }` — abbreviated (1.2K, 3.4M) — good default for y-axis
230
+ * - `{ type: "currency", currency: "USD", divisor: 100 }` — for cents → dollars
231
+ * - `{ type: "percent", source: "fraction" }` — for 0..1 → "45.2%"
232
+ * - `{ type: "datetime", style: "short" }` — good default for x-axis timestamps
233
+ *
234
+ * Set these on state:
235
+ * `{ ...ANALYTICS_CHART_DEFAULT_STATE, yFormatKind: { type: "currency", currency: "USD" } }`
236
+ *
237
+ * ## Scale warning
238
+ *
239
+ * All visible layers share ONE y-axis. If magnitudes differ wildly (e.g. revenue
240
+ * cents vs signup count), normalize the data BEFORE building Points. If
241
+ * normalization is impossible, use two separate `AnalyticsChart` instances stacked
242
+ * vertically.
243
+ */
244
+ function AnalyticsChart({ data: fullData, annotations: fullAnnotations = [], state, onChange, onAnnotationCreate, strings: stringsOverride, palette: paletteOverride, renderTooltip, plotMargin, yAxisWidth = 48, yDomainPadding = .1, pie, valueFormatter }) {
245
+ const resolvedPlotMargin = useMemo(() => ({
246
+ top: plotMargin?.top ?? 16,
247
+ right: plotMargin?.right ?? 24,
248
+ bottom: plotMargin?.bottom ?? 8,
249
+ left: plotMargin?.left ?? 12
250
+ }), [plotMargin]);
251
+ const fmtValue = valueFormatter ?? formatValue;
252
+ const strings = useMemo(() => resolveAnalyticsChartStrings(stringsOverride), [stringsOverride]);
253
+ const palette = useMemo(() => resolveAnalyticsChartPalette(paletteOverride), [paletteOverride]);
254
+ const renderTooltipFn = useMemo(() => renderTooltip ?? ((ctx) => /* @__PURE__ */ jsx(DefaultAnalyticsChartTooltip, { ctx })), [renderTooltip]);
255
+ const { xFormatKind, yFormatKind, layers } = state;
256
+ const timeseries = isTimeseriesState(state) ? state : null;
257
+ const showGrid = timeseries?.showGrid ?? false;
258
+ const showXAxis = timeseries?.showXAxis ?? false;
259
+ const showYAxis = timeseries?.showYAxis ?? false;
260
+ const zoomRange = timeseries?.zoomRange ?? null;
261
+ const pinnedIndex = timeseries?.pinnedIndex ?? null;
262
+ const primaryLayer = findPrimaryLayer(layers);
263
+ const compareLayer = findCompareLayer(layers);
264
+ const annotationsLayer = findAnnotationsLayer(layers);
265
+ const showPrimary = primaryLayer?.visible ?? false;
266
+ const showCompare = compareLayer?.visible ?? false;
267
+ const showAnnotationsLayer = annotationsLayer?.visible ?? false;
268
+ const primaryStyle = primaryLayer ? resolveDataLayerStyle(primaryLayer) : FALLBACK_PRIMARY_STYLE;
269
+ const compareStyle = compareLayer ? resolveDataLayerStyle(compareLayer) : FALLBACK_COMPARE_STYLE;
270
+ const primaryColor = primaryStyle.color;
271
+ const compareColor = compareStyle.color;
272
+ const annotationColor = annotationsLayer?.color ?? FALLBACK_ANNOTATION_COLOR;
273
+ const primaryStroke = STROKE_DASHARRAY[primaryStyle.strokeStyle];
274
+ const compareStroke = STROKE_DASHARRAY[compareStyle.strokeStyle];
275
+ const primaryFillOpacity = primaryStyle.fillOpacity;
276
+ const compareFillOpacity = compareStyle.fillOpacity;
277
+ const setTimeseriesField = useCallback((key, value) => {
278
+ onChange((prev) => {
279
+ if (prev.view !== "timeseries") return prev;
280
+ return {
281
+ ...prev,
282
+ [key]: value
283
+ };
284
+ });
285
+ }, [onChange]);
286
+ const wrapperRef = useRef(null);
287
+ const [hoverIndex, setHoverIndex] = useState(null);
288
+ const [committedRange, setCommittedRange] = useState(null);
289
+ const [annotationDraft, setAnnotationDraft] = useState(null);
290
+ const [dragAnchor, setDragAnchor] = useState(null);
291
+ const [brushStart, setBrushStart] = useState(null);
292
+ const [brushEnd, setBrushEnd] = useState(null);
293
+ const [pieHoverKey, setPieHoverKey] = useState(null);
294
+ const activeIndex = pinnedIndex ?? hoverIndex;
295
+ const primarySegmentSeries = useMemo(() => primaryLayer?.segmentSeries ?? EMPTY_SERIES, [primaryLayer?.segmentSeries]);
296
+ const compareSegmentSeries = useMemo(() => compareLayer?.segmentSeries ?? EMPTY_SERIES, [compareLayer?.segmentSeries]);
297
+ const primaryFullSegments = useMemo(() => primaryLayer?.segments ?? EMPTY_MATRIX, [primaryLayer?.segments]);
298
+ const compareFullSegments = useMemo(() => compareLayer?.segments ?? EMPTY_MATRIX, [compareLayer?.segments]);
299
+ const primarySegmented = (primaryLayer?.segmented ?? false) && showPrimary && primarySegmentSeries.length > 0 && primaryFullSegments.length > 0;
300
+ const compareSegmented = (compareLayer?.segmented ?? false) && showCompare && compareSegmentSeries.length > 0 && compareFullSegments.length > 0;
301
+ const visibleStart = zoomRange ? zoomRange[0] : 0;
302
+ const visibleEnd = zoomRange ? zoomRange[1] : fullData.length - 1;
303
+ const data = useMemo(() => fullData.slice(visibleStart, visibleEnd + 1), [
304
+ fullData,
305
+ visibleStart,
306
+ visibleEnd
307
+ ]);
308
+ const primarySegments = useMemo(() => primaryFullSegments.slice(visibleStart, visibleEnd + 1), [
309
+ primaryFullSegments,
310
+ visibleStart,
311
+ visibleEnd
312
+ ]);
313
+ const compareSegments = useMemo(() => compareFullSegments.slice(visibleStart, visibleEnd + 1), [
314
+ compareFullSegments,
315
+ visibleStart,
316
+ visibleEnd
317
+ ]);
318
+ const primarySegmentTotals = useMemo(() => primarySegments.map((row) => row.reduce((a, b) => a + b, 0)), [primarySegments]);
319
+ const compareSegmentTotals = useMemo(() => compareSegments.map((row) => row.reduce((a, b) => a + b, 0)), [compareSegments]);
320
+ const yDomainMax = useMemo(() => {
321
+ const layerMaxes = layers.filter(isAnalyticsChartDataLayer).map((l) => l.id).map((id) => data.reduce((m, p) => Math.max(m, pointValue(p, id)), 0));
322
+ const primaryStackMax = primarySegmentTotals.reduce((m, v) => Math.max(m, v), 0);
323
+ const compareStackMax = compareSegmentTotals.reduce((m, v) => Math.max(m, v), 0);
324
+ const rawMax = Math.max(0, ...layerMaxes, primaryStackMax, compareStackMax);
325
+ return Math.ceil(rawMax * (1 + yDomainPadding));
326
+ }, [
327
+ data,
328
+ layers,
329
+ primarySegmentTotals,
330
+ compareSegmentTotals,
331
+ yDomainPadding
332
+ ]);
333
+ const segmentColors = useMemo(() => {
334
+ return {
335
+ primary: {
336
+ light: buildRampColors(palette.primary, primarySegmentSeries.length, "light"),
337
+ dark: buildRampColors(palette.primary, primarySegmentSeries.length, "dark")
338
+ },
339
+ compare: {
340
+ light: buildRampColors(palette.compare, compareSegmentSeries.length, "light"),
341
+ dark: buildRampColors(palette.compare, compareSegmentSeries.length, "dark")
342
+ }
343
+ };
344
+ }, [
345
+ primarySegmentSeries.length,
346
+ compareSegmentSeries.length,
347
+ palette
348
+ ]);
349
+ const aggregatedPrimarySegments = useMemo(() => primarySegmentSeries.map((_, sIdx) => primarySegments.reduce((acc, row) => acc + (row[sIdx] ?? 0), 0)), [primarySegmentSeries, primarySegments]);
350
+ const aggregatedCompareSegments = useMemo(() => compareSegmentSeries.map((_, sIdx) => compareSegments.reduce((acc, row) => acc + (row[sIdx] ?? 0), 0)), [compareSegmentSeries, compareSegments]);
351
+ const aggregatedPrimaryTotal = useMemo(() => aggregatedPrimarySegments.reduce((a, b) => a + b, 0), [aggregatedPrimarySegments]);
352
+ const aggregatedCompareTotal = useMemo(() => aggregatedCompareSegments.reduce((a, b) => a + b, 0), [aggregatedCompareSegments]);
353
+ const annotations = useMemo(() => {
354
+ return fullAnnotations.filter((a) => a.index >= visibleStart && a.index <= visibleEnd).map((a) => ({
355
+ ...a,
356
+ index: a.index - visibleStart
357
+ }));
358
+ }, [
359
+ fullAnnotations,
360
+ visibleStart,
361
+ visibleEnd
362
+ ]);
363
+ const brushing = brushStart != null;
364
+ const N = data.length;
365
+ const primaryKey = primaryLayer?.id ?? "__analytics_primary";
366
+ const compareKey = compareLayer?.id ?? "__analytics_compare";
367
+ const primarySolidKey = `${primaryKey}_solid`;
368
+ const primaryDashedKey = `${primaryKey}_dashed`;
369
+ const compareSolidKey = `${compareKey}_solid`;
370
+ const compareDashedKey = `${compareKey}_dashed`;
371
+ const primarySegKey = useCallback((segKey) => `${primaryKey}_seg_${cssIdent(segKey)}`, [primaryKey]);
372
+ const compareSegKey = useCallback((segKey) => `${compareKey}_seg_${cssIdent(segKey)}`, [compareKey]);
373
+ const primaryInProgressLocalIdx = computeLocalInProgressIdx(primaryLayer?.inProgressFromIndex, visibleStart, visibleEnd);
374
+ const compareInProgressLocalIdx = computeLocalInProgressIdx(compareLayer?.inProgressFromIndex, visibleStart, visibleEnd);
375
+ const primaryHasInProgress = primaryInProgressLocalIdx != null && !primarySegmented && (primaryStyle.type === "line" || primaryStyle.type === "area");
376
+ const compareHasInProgress = compareInProgressLocalIdx != null && !compareSegmented && (compareStyle.type === "line" || compareStyle.type === "area");
377
+ const chartData = useMemo(() => {
378
+ return data.map((point, i) => {
379
+ const row = {
380
+ index: i,
381
+ ts: point.ts
382
+ };
383
+ for (const [k, v] of Object.entries(point.values)) row[k] = v;
384
+ if (primaryLayer && primaryHasInProgress) {
385
+ const k = primaryInProgressLocalIdx;
386
+ const v = pointValue(point, primaryLayer.id);
387
+ row[primarySolidKey] = i < k ? v : null;
388
+ row[primaryDashedKey] = i >= k - 1 ? v : null;
389
+ }
390
+ if (compareLayer && compareHasInProgress) {
391
+ const k = compareInProgressLocalIdx;
392
+ const v = pointValue(point, compareLayer.id);
393
+ row[compareSolidKey] = i < k ? v : null;
394
+ row[compareDashedKey] = i >= k - 1 ? v : null;
395
+ }
396
+ if (primarySegmented) primarySegmentSeries.forEach((s, sIdx) => {
397
+ row[primarySegKey(s.key)] = primarySegments[i]?.[sIdx] ?? 0;
398
+ });
399
+ if (compareSegmented) compareSegmentSeries.forEach((s, sIdx) => {
400
+ row[compareSegKey(s.key)] = compareSegments[i]?.[sIdx] ?? 0;
401
+ });
402
+ return row;
403
+ });
404
+ }, [
405
+ data,
406
+ primaryLayer,
407
+ compareLayer,
408
+ primarySolidKey,
409
+ primaryDashedKey,
410
+ compareSolidKey,
411
+ compareDashedKey,
412
+ primarySegKey,
413
+ compareSegKey,
414
+ primarySegments,
415
+ compareSegments,
416
+ primarySegmentSeries,
417
+ compareSegmentSeries,
418
+ primarySegmented,
419
+ compareSegmented,
420
+ primaryHasInProgress,
421
+ compareHasInProgress,
422
+ primaryInProgressLocalIdx,
423
+ compareInProgressLocalIdx
424
+ ]);
425
+ const chartConfig = useMemo(() => {
426
+ const primaryLabel = primaryLayer?.label ?? "";
427
+ const compareLabel = compareLayer?.label ?? "";
428
+ const config = {};
429
+ if (primaryLayer) {
430
+ config[primaryLayer.id] = {
431
+ label: primaryLabel,
432
+ color: primaryColor
433
+ };
434
+ if (primaryHasInProgress) {
435
+ config[primarySolidKey] = {
436
+ label: primaryLabel,
437
+ color: primaryColor
438
+ };
439
+ config[primaryDashedKey] = {
440
+ label: primaryLabel,
441
+ color: primaryColor
442
+ };
443
+ }
444
+ }
445
+ if (compareLayer) {
446
+ config[compareLayer.id] = {
447
+ label: compareLabel,
448
+ color: compareColor
449
+ };
450
+ if (compareHasInProgress) {
451
+ config[compareSolidKey] = {
452
+ label: compareLabel,
453
+ color: compareColor
454
+ };
455
+ config[compareDashedKey] = {
456
+ label: compareLabel,
457
+ color: compareColor
458
+ };
459
+ }
460
+ }
461
+ if (primarySegmented) primarySegmentSeries.forEach((s, i) => {
462
+ config[primarySegKey(s.key)] = {
463
+ label: s.label,
464
+ theme: {
465
+ light: segmentColors.primary.light[i],
466
+ dark: segmentColors.primary.dark[i]
467
+ }
468
+ };
469
+ });
470
+ if (compareSegmented) compareSegmentSeries.forEach((s, i) => {
471
+ config[compareSegKey(s.key)] = {
472
+ label: s.label,
473
+ theme: {
474
+ light: segmentColors.compare.light[i],
475
+ dark: segmentColors.compare.dark[i]
476
+ }
477
+ };
478
+ });
479
+ return config;
480
+ }, [
481
+ primaryLayer,
482
+ compareLayer,
483
+ primarySolidKey,
484
+ primaryDashedKey,
485
+ compareSolidKey,
486
+ compareDashedKey,
487
+ primarySegKey,
488
+ compareSegKey,
489
+ primaryColor,
490
+ compareColor,
491
+ primaryHasInProgress,
492
+ compareHasInProgress,
493
+ primarySegmented,
494
+ compareSegmented,
495
+ primarySegmentSeries,
496
+ compareSegmentSeries,
497
+ segmentColors
498
+ ]);
499
+ const handleChartMouseMove = useCallback((rechartsState) => {
500
+ const i = rechartsState.activeTooltipIndex;
501
+ if (typeof i !== "number") return;
502
+ setHoverIndex(i);
503
+ if (dragAnchor != null && (brushStart != null || i !== dragAnchor)) {
504
+ if (brushStart == null) setBrushStart(dragAnchor);
505
+ setBrushEnd(i);
506
+ }
507
+ }, [dragAnchor, brushStart]);
508
+ const handleChartMouseDown = useCallback((rechartsState, e) => {
509
+ if (e.button !== 0) return;
510
+ const i = rechartsState.activeTooltipIndex;
511
+ if (typeof i !== "number") return;
512
+ e.preventDefault();
513
+ setDragAnchor(i);
514
+ setAnnotationDraft(null);
515
+ }, []);
516
+ const handleChartMouseUp = useCallback((_, e) => {
517
+ e.stopPropagation();
518
+ if (brushStart != null && brushEnd != null) {
519
+ const lo = Math.min(brushStart, brushEnd);
520
+ const hi = Math.max(brushStart, brushEnd);
521
+ setBrushStart(null);
522
+ setBrushEnd(null);
523
+ setDragAnchor(null);
524
+ if (hi - lo >= 1) setCommittedRange([lo, hi]);
525
+ return;
526
+ }
527
+ setDragAnchor(null);
528
+ if (pinnedIndex != null) setTimeseriesField("pinnedIndex", null);
529
+ else if (hoverIndex != null) setTimeseriesField("pinnedIndex", hoverIndex);
530
+ }, [
531
+ brushStart,
532
+ brushEnd,
533
+ hoverIndex,
534
+ pinnedIndex,
535
+ setTimeseriesField
536
+ ]);
537
+ const handleChartMouseLeave = useCallback(() => {
538
+ if (!brushing) setHoverIndex(null);
539
+ }, [brushing]);
540
+ const handleKeyDown = useCallback((e) => {
541
+ if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
542
+ e.preventDefault();
543
+ setHoverIndex((cur) => {
544
+ const base = cur ?? pinnedIndex ?? 0;
545
+ return e.key === "ArrowRight" ? Math.min(N - 1, base + 1) : Math.max(0, base - 1);
546
+ });
547
+ return;
548
+ }
549
+ if (e.key === "Home") {
550
+ e.preventDefault();
551
+ setHoverIndex(0);
552
+ return;
553
+ }
554
+ if (e.key === "End") {
555
+ e.preventDefault();
556
+ setHoverIndex(N - 1);
557
+ return;
558
+ }
559
+ if (e.key === "Enter" || e.key === " ") {
560
+ e.preventDefault();
561
+ if (pinnedIndex != null) setTimeseriesField("pinnedIndex", null);
562
+ else if (hoverIndex != null) setTimeseriesField("pinnedIndex", hoverIndex);
563
+ }
564
+ }, [
565
+ N,
566
+ hoverIndex,
567
+ pinnedIndex,
568
+ setTimeseriesField
569
+ ]);
570
+ useEffect(() => {
571
+ if (pinnedIndex == null) return;
572
+ const onKey = (e) => {
573
+ if (e.key === "Escape") setTimeseriesField("pinnedIndex", null);
574
+ };
575
+ const onDown = (e) => {
576
+ if (!wrapperRef.current) return;
577
+ if (!wrapperRef.current.contains(e.target)) setTimeseriesField("pinnedIndex", null);
578
+ };
579
+ window.addEventListener("keydown", onKey);
580
+ window.addEventListener("mousedown", onDown);
581
+ return () => {
582
+ window.removeEventListener("keydown", onKey);
583
+ window.removeEventListener("mousedown", onDown);
584
+ };
585
+ }, [pinnedIndex, setTimeseriesField]);
586
+ const plotInset = resolvedPlotMargin.left + resolvedPlotMargin.right;
587
+ const indexToCss = useCallback((i) => {
588
+ if (N <= 1) return `calc(${resolvedPlotMargin.left}px + (100% - ${plotInset}px) * 0.5)`;
589
+ const t = Math.max(0, Math.min(1, i / (N - 1)));
590
+ return `calc(${resolvedPlotMargin.left}px + (100% - ${plotInset}px) * ${t})`;
591
+ }, [
592
+ N,
593
+ resolvedPlotMargin.left,
594
+ plotInset
595
+ ]);
596
+ const shouldFlip = (activeIndex != null ? N <= 1 ? 50 : activeIndex / (N - 1) * 100 : 0) > 68;
597
+ const activePoint = activeIndex != null ? data[activeIndex] : null;
598
+ const chartAriaLabel = primaryLayer?.label || "Chart";
599
+ if (state.view === "pie") return /* @__PURE__ */ jsx(AnalyticsChartPie, {
600
+ wrapperRef,
601
+ primarySegmentSeries,
602
+ compareSegmentSeries,
603
+ aggregatedPrimarySegments,
604
+ aggregatedCompareSegments,
605
+ aggregatedPrimaryTotal,
606
+ aggregatedCompareTotal,
607
+ segmentColors,
608
+ showPrimary,
609
+ showCompare,
610
+ xFormatKind,
611
+ yFormatKind,
612
+ hoverKey: pieHoverKey,
613
+ setHoverKey: setPieHoverKey,
614
+ zoomRange,
615
+ onResetZoom: () => {
616
+ onChange((prev) => ({
617
+ ...prev,
618
+ zoomRange: null,
619
+ pinnedIndex: null
620
+ }));
621
+ setCommittedRange(null);
622
+ setAnnotationDraft(null);
623
+ setHoverIndex(null);
624
+ },
625
+ visibleStart,
626
+ visibleEnd,
627
+ fullData,
628
+ strings,
629
+ fmtValue,
630
+ pie
631
+ });
632
+ return /* @__PURE__ */ jsxs("div", {
633
+ ref: wrapperRef,
634
+ className: cn("relative w-full select-none rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/40 dark:focus-visible:ring-blue-400/40", brushing && "[&_.recharts-wrapper]:cursor-ew-resize", !brushing && "[&_.recharts-wrapper]:cursor-crosshair"),
635
+ onClick: (e) => {
636
+ e.stopPropagation();
637
+ },
638
+ onKeyDown: handleKeyDown,
639
+ onFocus: () => {
640
+ if (hoverIndex == null) setHoverIndex(pinnedIndex ?? Math.floor(N / 2));
641
+ },
642
+ tabIndex: 0,
643
+ role: "img",
644
+ "aria-label": `${chartAriaLabel} over the visible ${data.length}-day range. Use arrow keys to move the cursor, Enter to pin, Escape to release. Click and drag to select a range.`,
645
+ children: [
646
+ /* @__PURE__ */ jsx(DesignChartContainer, {
647
+ config: chartConfig,
648
+ className: "aspect-auto h-[260px] w-full sm:h-[320px]",
649
+ children: /* @__PURE__ */ jsxs(ComposedChart, {
650
+ data: chartData,
651
+ margin: resolvedPlotMargin,
652
+ onMouseMove: handleChartMouseMove,
653
+ onMouseDown: handleChartMouseDown,
654
+ onMouseUp: handleChartMouseUp,
655
+ onMouseLeave: handleChartMouseLeave,
656
+ children: [
657
+ showGrid && /* @__PURE__ */ jsx(CartesianGrid, {
658
+ vertical: false,
659
+ strokeDasharray: "3 3"
660
+ }),
661
+ showXAxis && /* @__PURE__ */ jsx(XAxis, {
662
+ dataKey: "index",
663
+ tickLine: false,
664
+ axisLine: false,
665
+ tickMargin: 10,
666
+ minTickGap: 32,
667
+ tickFormatter: (value) => {
668
+ const idx = Number(value);
669
+ if (idx < 0 || idx >= data.length) return "";
670
+ return fmtValue(data[idx].ts, xFormatKind);
671
+ }
672
+ }),
673
+ showYAxis && /* @__PURE__ */ jsx(YAxis, {
674
+ tickLine: false,
675
+ axisLine: false,
676
+ tickMargin: 8,
677
+ width: yAxisWidth,
678
+ domain: [0, yDomainMax],
679
+ allowDataOverflow: false,
680
+ tickFormatter: (value) => fmtValue(Math.round(Number(value)), yFormatKind)
681
+ }),
682
+ showAnnotationsLayer && annotations.map((a) => /* @__PURE__ */ jsx(ReferenceLine, {
683
+ x: a.index,
684
+ stroke: annotationColor,
685
+ strokeDasharray: "3 4",
686
+ strokeOpacity: .55,
687
+ ifOverflow: "visible"
688
+ }, a.index)),
689
+ committedRange && /* @__PURE__ */ jsx(ReferenceArea, {
690
+ x1: committedRange[0],
691
+ x2: committedRange[1],
692
+ fill: primaryColor,
693
+ fillOpacity: .07,
694
+ stroke: primaryColor,
695
+ strokeOpacity: .45,
696
+ strokeDasharray: "3 3",
697
+ ifOverflow: "visible"
698
+ }),
699
+ brushStart != null && brushEnd != null && /* @__PURE__ */ jsx(ReferenceArea, {
700
+ x1: Math.min(brushStart, brushEnd),
701
+ x2: Math.max(brushStart, brushEnd),
702
+ fill: primaryColor,
703
+ fillOpacity: .15,
704
+ stroke: primaryColor,
705
+ strokeOpacity: .55,
706
+ ifOverflow: "visible"
707
+ }),
708
+ showPrimary && primaryLayer && renderDataSeries({
709
+ layer: primaryLayer,
710
+ segmented: primarySegmented,
711
+ segmentSeries: primarySegmentSeries,
712
+ segKey: primarySegKey,
713
+ stackId: "primary-segments",
714
+ strokeDasharray: primaryStroke,
715
+ segmentedStrokeDasharray: void 0,
716
+ fillOpacity: primaryFillOpacity,
717
+ segmentedFillOpacity: .78,
718
+ strokeWidth: 2,
719
+ segmentedStrokeWidth: .75,
720
+ inProgressKeys: primaryHasInProgress ? {
721
+ solid: primarySolidKey,
722
+ dashed: primaryDashedKey
723
+ } : null
724
+ }),
725
+ showCompare && compareLayer && renderDataSeries({
726
+ layer: compareLayer,
727
+ segmented: compareSegmented,
728
+ segmentSeries: compareSegmentSeries,
729
+ segKey: compareSegKey,
730
+ stackId: "compare-segments",
731
+ strokeDasharray: compareStroke,
732
+ segmentedStrokeDasharray: compareStroke,
733
+ fillOpacity: compareFillOpacity,
734
+ segmentedFillOpacity: .6,
735
+ baseOpacity: compareSegmented ? .9 : 1,
736
+ strokeWidth: 1.5,
737
+ segmentedStrokeWidth: .75,
738
+ inProgressKeys: compareHasInProgress ? {
739
+ solid: compareSolidKey,
740
+ dashed: compareDashedKey
741
+ } : null
742
+ })
743
+ ]
744
+ })
745
+ }),
746
+ activeIndex != null && activePoint && !brushing && /* @__PURE__ */ jsx("div", {
747
+ className: "pointer-events-none absolute inset-y-4 z-10 w-px border-l border-dashed border-foreground/30",
748
+ style: { left: indexToCss(activeIndex) }
749
+ }),
750
+ brushStart != null && brushEnd != null && (() => {
751
+ const lo = Math.min(brushStart, brushEnd);
752
+ const hi = Math.max(brushStart, brushEnd);
753
+ const days = hi - lo + 1;
754
+ return /* @__PURE__ */ jsx("div", {
755
+ className: "pointer-events-none absolute -top-1 z-30 -translate-x-1/2 -translate-y-full rounded-lg border border-blue-500/30 bg-background/95 px-3 py-1.5 text-[11px] shadow-lg backdrop-blur-xl dark:border-blue-400/30",
756
+ style: { left: indexToCss((lo + hi) / 2) },
757
+ children: /* @__PURE__ */ jsxs("div", {
758
+ className: "flex items-center gap-2",
759
+ children: [
760
+ /* @__PURE__ */ jsx("span", {
761
+ className: "font-mono text-[10px] uppercase tracking-wider text-muted-foreground",
762
+ children: strings.rangeLabel
763
+ }),
764
+ /* @__PURE__ */ jsxs("span", {
765
+ className: "font-mono tabular-nums text-foreground",
766
+ children: [
767
+ fmtValue(data[lo].ts, xFormatKind),
768
+ " – ",
769
+ fmtValue(data[hi].ts, xFormatKind)
770
+ ]
771
+ }),
772
+ /* @__PURE__ */ jsxs("span", {
773
+ className: "font-mono text-[10px] tabular-nums text-muted-foreground",
774
+ children: ["· ", strings.daysShort(days)]
775
+ })
776
+ ]
777
+ })
778
+ });
779
+ })(),
780
+ zoomRange && /* @__PURE__ */ jsx("div", {
781
+ className: "absolute right-2 top-2 z-20",
782
+ children: /* @__PURE__ */ jsxs("button", {
783
+ type: "button",
784
+ onClick: () => {
785
+ onChange((prev) => ({
786
+ ...prev,
787
+ zoomRange: null,
788
+ pinnedIndex: null
789
+ }));
790
+ setCommittedRange(null);
791
+ setAnnotationDraft(null);
792
+ setHoverIndex(null);
793
+ },
794
+ className: "inline-flex items-center gap-1.5 rounded-full bg-blue-500/10 px-2.5 py-1 font-mono text-[10px] font-semibold uppercase tracking-wider text-blue-600 ring-1 ring-blue-500/30 transition-colors duration-150 hover:bg-blue-500/15 hover:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 dark:text-blue-300 dark:ring-blue-400/30",
795
+ children: [/* @__PURE__ */ jsx(MagnifyingGlassMinusIcon, {
796
+ weight: "bold",
797
+ className: "size-3",
798
+ "aria-hidden": "true"
799
+ }), /* @__PURE__ */ jsx("span", { children: strings.resetZoom })]
800
+ })
801
+ }),
802
+ committedRange && !brushing && (() => {
803
+ const [lo, hi] = committedRange;
804
+ const center = (lo + hi) / 2;
805
+ const centerPct = N <= 1 ? 50 : center / (N - 1) * 100;
806
+ const anchorStyle = centerPct < 22 ? { left: "8px" } : centerPct > 78 ? { right: "8px" } : {
807
+ left: indexToCss(center),
808
+ transform: "translateX(-50%)"
809
+ };
810
+ const days = hi - lo + 1;
811
+ const draft = annotationDraft;
812
+ return /* @__PURE__ */ jsx("div", {
813
+ className: "absolute top-2 z-30",
814
+ style: anchorStyle,
815
+ children: /* @__PURE__ */ jsx("div", {
816
+ className: "flex items-center gap-1 rounded-full border border-foreground/10 bg-background/95 py-1 pl-2 pr-1 shadow-[0_10px_28px_rgba(15,23,42,0.18)] backdrop-blur-xl dark:shadow-[0_10px_28px_rgba(0,0,0,0.55)]",
817
+ children: draft == null ? /* @__PURE__ */ jsxs(Fragment, { children: [
818
+ /* @__PURE__ */ jsxs("span", {
819
+ className: "flex items-center gap-1.5 whitespace-nowrap px-1 font-mono text-[10px] tabular-nums text-muted-foreground",
820
+ children: [
821
+ /* @__PURE__ */ jsxs("span", {
822
+ className: "text-foreground",
823
+ children: [
824
+ fmtValue(data[lo].ts, xFormatKind),
825
+ " – ",
826
+ fmtValue(data[hi].ts, xFormatKind)
827
+ ]
828
+ }),
829
+ /* @__PURE__ */ jsx("span", {
830
+ className: "text-foreground/30",
831
+ children: "·"
832
+ }),
833
+ /* @__PURE__ */ jsx("span", { children: strings.daysShort(days) })
834
+ ]
835
+ }),
836
+ /* @__PURE__ */ jsx("div", {
837
+ className: "h-4 w-px bg-foreground/10",
838
+ "aria-hidden": "true"
839
+ }),
840
+ /* @__PURE__ */ jsxs(DesignButton, {
841
+ size: "sm",
842
+ variant: "ghost",
843
+ className: "h-7 gap-1.5 px-2 text-[11px]",
844
+ onClick: () => {
845
+ onChange((prev) => ({
846
+ ...prev,
847
+ zoomRange: [visibleStart + lo, visibleStart + hi],
848
+ pinnedIndex: null
849
+ }));
850
+ setCommittedRange(null);
851
+ setAnnotationDraft(null);
852
+ setHoverIndex(null);
853
+ },
854
+ children: [/* @__PURE__ */ jsx(MagnifyingGlassPlusIcon, {
855
+ weight: "bold",
856
+ className: "size-3.5",
857
+ "aria-hidden": "true"
858
+ }), strings.zoomIn]
859
+ }),
860
+ /* @__PURE__ */ jsxs(DesignButton, {
861
+ size: "sm",
862
+ variant: "ghost",
863
+ className: "h-7 gap-1.5 px-2 text-[11px]",
864
+ onClick: () => setAnnotationDraft(""),
865
+ children: [/* @__PURE__ */ jsx(FlagIcon, {
866
+ weight: "bold",
867
+ className: "size-3.5",
868
+ "aria-hidden": "true"
869
+ }), strings.annotate]
870
+ }),
871
+ /* @__PURE__ */ jsx(DesignButton, {
872
+ size: "sm",
873
+ variant: "ghost",
874
+ className: "h-7 w-7 p-0",
875
+ "aria-label": strings.clearSelection,
876
+ onClick: () => setCommittedRange(null),
877
+ children: /* @__PURE__ */ jsx(XIcon, {
878
+ weight: "bold",
879
+ className: "size-3.5",
880
+ "aria-hidden": "true"
881
+ })
882
+ })
883
+ ] }) : /* @__PURE__ */ jsxs("form", {
884
+ className: "flex items-center gap-1 px-1",
885
+ onSubmit: (e) => {
886
+ e.preventDefault();
887
+ const label = draft.trim();
888
+ if (label.length === 0) return;
889
+ const mid = Math.round((lo + hi) / 2);
890
+ onAnnotationCreate?.({
891
+ index: visibleStart + mid,
892
+ label,
893
+ description: label
894
+ });
895
+ setCommittedRange(null);
896
+ setAnnotationDraft(null);
897
+ },
898
+ children: [
899
+ /* @__PURE__ */ jsx(FlagIcon, {
900
+ weight: "bold",
901
+ className: "size-3.5 text-amber-500 dark:text-amber-400",
902
+ "aria-hidden": "true"
903
+ }),
904
+ /* @__PURE__ */ jsx("input", {
905
+ autoFocus: true,
906
+ type: "text",
907
+ value: draft,
908
+ onChange: (e) => setAnnotationDraft(e.target.value),
909
+ onKeyDown: (e) => {
910
+ if (e.key === "Escape") {
911
+ e.preventDefault();
912
+ setAnnotationDraft(null);
913
+ }
914
+ },
915
+ maxLength: 40,
916
+ placeholder: strings.annotationPlaceholder,
917
+ "aria-label": strings.annotationLabelAria,
918
+ className: "w-44 bg-transparent px-1 py-0.5 font-mono text-[11px] text-foreground placeholder:text-muted-foreground/60 focus-visible:outline-none"
919
+ }),
920
+ /* @__PURE__ */ jsx(DesignButton, {
921
+ type: "submit",
922
+ size: "sm",
923
+ variant: "default",
924
+ className: "h-7 px-2.5 text-[11px]",
925
+ disabled: draft.trim().length === 0,
926
+ children: strings.save
927
+ }),
928
+ /* @__PURE__ */ jsx(DesignButton, {
929
+ type: "button",
930
+ size: "sm",
931
+ variant: "ghost",
932
+ className: "h-7 px-2 text-[11px]",
933
+ onClick: () => setAnnotationDraft(null),
934
+ children: strings.cancel
935
+ })
936
+ ]
937
+ })
938
+ })
939
+ });
940
+ })(),
941
+ showAnnotationsLayer && /* @__PURE__ */ jsx("div", {
942
+ className: "pointer-events-none absolute inset-x-0 top-1 h-0",
943
+ children: annotations.map((a) => {
944
+ return /* @__PURE__ */ jsxs("div", {
945
+ className: "absolute -translate-x-1/2",
946
+ style: { left: indexToCss(a.index) },
947
+ children: [/* @__PURE__ */ jsxs("button", {
948
+ type: "button",
949
+ "aria-label": a.description,
950
+ className: "peer pointer-events-auto inline-flex items-center gap-1 rounded-full bg-amber-500/15 px-1.5 py-[1px] font-mono text-[9px] font-semibold uppercase tracking-wider text-amber-700 ring-1 ring-amber-500/30 dark:text-amber-300 dark:bg-amber-500/20 dark:ring-amber-500/40 transition-colors duration-150 hover:bg-amber-500/25 hover:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/50",
951
+ children: [/* @__PURE__ */ jsx(FlagIcon, {
952
+ weight: "fill",
953
+ className: "size-2.5",
954
+ "aria-hidden": "true"
955
+ }), a.label]
956
+ }), /* @__PURE__ */ jsx("span", {
957
+ role: "tooltip",
958
+ className: "pointer-events-none absolute left-1/2 top-full z-30 mt-1 hidden -translate-x-1/2 whitespace-nowrap rounded-md bg-foreground px-2 py-1 text-[10px] font-normal text-background shadow-md peer-hover:block peer-focus-visible:block",
959
+ children: a.description
960
+ })]
961
+ }, a.index);
962
+ })
963
+ }),
964
+ activeIndex != null && activePoint && (() => {
965
+ const primaryView = buildTooltipLayerView({
966
+ show: showPrimary,
967
+ layer: primaryLayer,
968
+ color: primaryColor,
969
+ segmented: primarySegmented,
970
+ segmentSeries: primarySegmentSeries,
971
+ segmentRows: primarySegments,
972
+ segmentTotals: primarySegmentTotals,
973
+ segmentColorsLight: segmentColors.primary.light,
974
+ segmentColorsDark: segmentColors.primary.dark,
975
+ activeIndex,
976
+ activePoint,
977
+ fallbackLabel: "Chart"
978
+ });
979
+ const compareView = buildTooltipLayerView({
980
+ show: showCompare,
981
+ layer: compareLayer,
982
+ color: compareColor,
983
+ segmented: compareSegmented,
984
+ segmentSeries: compareSegmentSeries,
985
+ segmentRows: compareSegments,
986
+ segmentTotals: compareSegmentTotals,
987
+ segmentColorsLight: segmentColors.compare.light,
988
+ segmentColorsDark: segmentColors.compare.dark,
989
+ activeIndex,
990
+ activePoint
991
+ });
992
+ const delta = primaryView && compareView ? formatDelta(primaryView.total, compareView.total) : null;
993
+ const ctx = {
994
+ activeIndex,
995
+ point: activePoint,
996
+ isPinned: pinnedIndex != null,
997
+ primary: primaryView,
998
+ compare: compareView,
999
+ delta,
1000
+ formatValue: (v) => fmtValue(v, yFormatKind),
1001
+ formatDate: (ts) => fmtValue(ts, xFormatKind),
1002
+ strings
1003
+ };
1004
+ return /* @__PURE__ */ jsx("div", {
1005
+ className: cn("pointer-events-none absolute top-10 z-20", "transition-[transform,opacity] duration-150"),
1006
+ style: {
1007
+ left: indexToCss(activeIndex),
1008
+ transform: shouldFlip ? "translateX(calc(-100% - 16px))" : "translateX(16px)"
1009
+ },
1010
+ children: renderTooltipFn(ctx)
1011
+ });
1012
+ })()
1013
+ ]
1014
+ });
1015
+ }
1016
+
1017
+ //#endregion
1018
+ export { AnalyticsChart };
1019
+ //# sourceMappingURL=analytics-chart.js.map