@arcfusionz/arc-analytic-ui 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/dist/_virtual/_rolldown/runtime.js +13 -0
  2. package/dist/components/AnomalyCard/AnomalyCard.chart.d.ts +64 -0
  3. package/dist/components/AnomalyCard/AnomalyCard.chart.js +341 -0
  4. package/dist/components/AnomalyCard/AnomalyCard.d.ts +169 -0
  5. package/dist/components/AnomalyCard/AnomalyCard.js +1019 -0
  6. package/dist/components/AnomalyCard/AnomalyCard.model.d.ts +508 -0
  7. package/dist/components/AnomalyCard/AnomalyCard.model.js +553 -0
  8. package/dist/components/AnomalyCard/index.d.ts +4 -0
  9. package/dist/components/AnomalyCard/index.js +4 -0
  10. package/dist/components/ChartCard/ChartCard.chart.d.ts +72 -0
  11. package/dist/components/ChartCard/ChartCard.chart.js +1333 -0
  12. package/dist/components/ChartCard/ChartCard.d.ts +206 -0
  13. package/dist/components/ChartCard/ChartCard.js +718 -0
  14. package/dist/components/ChartCard/ChartCard.model.d.ts +452 -0
  15. package/dist/components/ChartCard/ChartCard.model.js +1223 -0
  16. package/dist/components/ChartCard/index.d.ts +4 -0
  17. package/dist/components/ChartCard/index.js +4 -0
  18. package/dist/components/ComparisonCard/ComparisonCard.chart.d.ts +80 -0
  19. package/dist/components/ComparisonCard/ComparisonCard.chart.js +337 -0
  20. package/dist/components/ComparisonCard/ComparisonCard.d.ts +200 -0
  21. package/dist/components/ComparisonCard/ComparisonCard.js +1325 -0
  22. package/dist/components/ComparisonCard/ComparisonCard.model.d.ts +309 -0
  23. package/dist/components/ComparisonCard/ComparisonCard.model.js +263 -0
  24. package/dist/components/ComparisonCard/index.d.ts +4 -0
  25. package/dist/components/ComparisonCard/index.js +4 -0
  26. package/dist/components/ForecastCard/ForecastCard.chart.d.ts +82 -0
  27. package/dist/components/ForecastCard/ForecastCard.chart.js +416 -0
  28. package/dist/components/ForecastCard/ForecastCard.d.ts +148 -0
  29. package/dist/components/ForecastCard/ForecastCard.js +1029 -0
  30. package/dist/components/ForecastCard/ForecastCard.model.d.ts +497 -0
  31. package/dist/components/ForecastCard/ForecastCard.model.js +554 -0
  32. package/dist/components/ForecastCard/index.d.ts +4 -0
  33. package/dist/components/ForecastCard/index.js +4 -0
  34. package/dist/components/HighlightCard/HighlightCard.chart.d.ts +122 -0
  35. package/dist/components/HighlightCard/HighlightCard.chart.js +660 -0
  36. package/dist/components/HighlightCard/HighlightCard.d.ts +202 -0
  37. package/dist/components/HighlightCard/HighlightCard.js +1286 -0
  38. package/dist/components/HighlightCard/HighlightCard.model.d.ts +692 -0
  39. package/dist/components/HighlightCard/HighlightCard.model.js +875 -0
  40. package/dist/components/HighlightCard/index.d.ts +4 -0
  41. package/dist/components/HighlightCard/index.js +4 -0
  42. package/dist/components/InsightCard/InsightCard.chart.d.ts +91 -0
  43. package/dist/components/InsightCard/InsightCard.chart.js +471 -0
  44. package/dist/components/InsightCard/InsightCard.d.ts +161 -0
  45. package/dist/components/InsightCard/InsightCard.js +961 -0
  46. package/dist/components/InsightCard/InsightCard.model.d.ts +405 -0
  47. package/dist/components/InsightCard/InsightCard.model.js +421 -0
  48. package/dist/components/InsightCard/index.d.ts +4 -0
  49. package/dist/components/InsightCard/index.js +4 -0
  50. package/dist/components/MetricCard/MetricCard.chart.d.ts +79 -0
  51. package/dist/components/MetricCard/MetricCard.chart.js +435 -0
  52. package/dist/components/MetricCard/MetricCard.d.ts +227 -0
  53. package/dist/components/MetricCard/MetricCard.js +1293 -0
  54. package/dist/components/MetricCard/MetricCard.model.d.ts +521 -0
  55. package/dist/components/MetricCard/MetricCard.model.js +627 -0
  56. package/dist/components/MetricCard/index.d.ts +4 -0
  57. package/dist/components/MetricCard/index.js +4 -0
  58. package/dist/components/ProfileCard/ProfileCard.chart.d.ts +64 -0
  59. package/dist/components/ProfileCard/ProfileCard.chart.js +427 -0
  60. package/dist/components/ProfileCard/ProfileCard.d.ts +140 -0
  61. package/dist/components/ProfileCard/ProfileCard.js +735 -0
  62. package/dist/components/ProfileCard/ProfileCard.model.d.ts +279 -0
  63. package/dist/components/ProfileCard/ProfileCard.model.js +244 -0
  64. package/dist/components/ProfileCard/index.d.ts +4 -0
  65. package/dist/components/ProfileCard/index.js +4 -0
  66. package/dist/components/ProgressCard/ProgressCard.chart.d.ts +73 -0
  67. package/dist/components/ProgressCard/ProgressCard.chart.js +373 -0
  68. package/dist/components/ProgressCard/ProgressCard.d.ts +137 -0
  69. package/dist/components/ProgressCard/ProgressCard.js +908 -0
  70. package/dist/components/ProgressCard/ProgressCard.model.d.ts +385 -0
  71. package/dist/components/ProgressCard/ProgressCard.model.js +389 -0
  72. package/dist/components/ProgressCard/index.d.ts +4 -0
  73. package/dist/components/ProgressCard/index.js +4 -0
  74. package/dist/components/ReportCard/ReportCard.chart.d.ts +45 -0
  75. package/dist/components/ReportCard/ReportCard.chart.js +231 -0
  76. package/dist/components/ReportCard/ReportCard.d.ts +153 -0
  77. package/dist/components/ReportCard/ReportCard.js +1118 -0
  78. package/dist/components/ReportCard/ReportCard.model.d.ts +392 -0
  79. package/dist/components/ReportCard/ReportCard.model.js +399 -0
  80. package/dist/components/ReportCard/index.d.ts +4 -0
  81. package/dist/components/ReportCard/index.js +4 -0
  82. package/dist/components/SankeyCard/SankeyCard.chart.d.ts +75 -0
  83. package/dist/components/SankeyCard/SankeyCard.chart.js +249 -0
  84. package/dist/components/SankeyCard/SankeyCard.d.ts +185 -0
  85. package/dist/components/SankeyCard/SankeyCard.js +788 -0
  86. package/dist/components/SankeyCard/SankeyCard.model.d.ts +501 -0
  87. package/dist/components/SankeyCard/SankeyCard.model.js +789 -0
  88. package/dist/components/SankeyCard/index.d.ts +4 -0
  89. package/dist/components/SankeyCard/index.js +4 -0
  90. package/dist/components/StatusCard/StatusCard.chart.d.ts +60 -0
  91. package/dist/components/StatusCard/StatusCard.chart.js +188 -0
  92. package/dist/components/StatusCard/StatusCard.d.ts +176 -0
  93. package/dist/components/StatusCard/StatusCard.js +989 -0
  94. package/dist/components/StatusCard/StatusCard.model.d.ts +555 -0
  95. package/dist/components/StatusCard/StatusCard.model.js +776 -0
  96. package/dist/components/StatusCard/index.d.ts +4 -0
  97. package/dist/components/StatusCard/index.js +4 -0
  98. package/dist/components/TableCard/TableCard.chart.d.ts +53 -0
  99. package/dist/components/TableCard/TableCard.chart.js +192 -0
  100. package/dist/components/TableCard/TableCard.d.ts +140 -0
  101. package/dist/components/TableCard/TableCard.js +811 -0
  102. package/dist/components/TableCard/TableCard.model.d.ts +381 -0
  103. package/dist/components/TableCard/TableCard.model.js +475 -0
  104. package/dist/components/TableCard/index.d.ts +4 -0
  105. package/dist/components/TableCard/index.js +4 -0
  106. package/dist/components/TargetCard/TargetCard.chart.d.ts +52 -0
  107. package/dist/components/TargetCard/TargetCard.chart.js +273 -0
  108. package/dist/components/TargetCard/TargetCard.d.ts +143 -0
  109. package/dist/components/TargetCard/TargetCard.js +910 -0
  110. package/dist/components/TargetCard/TargetCard.model.d.ts +468 -0
  111. package/dist/components/TargetCard/TargetCard.model.js +581 -0
  112. package/dist/components/TargetCard/index.d.ts +4 -0
  113. package/dist/components/TargetCard/index.js +4 -0
  114. package/dist/components/TreeCard/TreeCard.chart.d.ts +80 -0
  115. package/dist/components/TreeCard/TreeCard.chart.js +178 -0
  116. package/dist/components/TreeCard/TreeCard.d.ts +182 -0
  117. package/dist/components/TreeCard/TreeCard.js +1187 -0
  118. package/dist/components/TreeCard/TreeCard.model.d.ts +330 -0
  119. package/dist/components/TreeCard/TreeCard.model.js +401 -0
  120. package/dist/components/TreeCard/index.d.ts +4 -0
  121. package/dist/components/TreeCard/index.js +4 -0
  122. package/dist/components/TrendCard/TrendCard.chart.d.ts +75 -0
  123. package/dist/components/TrendCard/TrendCard.chart.js +393 -0
  124. package/dist/components/TrendCard/TrendCard.d.ts +224 -0
  125. package/dist/components/TrendCard/TrendCard.js +1053 -0
  126. package/dist/components/TrendCard/TrendCard.model.d.ts +545 -0
  127. package/dist/components/TrendCard/TrendCard.model.js +623 -0
  128. package/dist/components/TrendCard/index.d.ts +4 -0
  129. package/dist/components/TrendCard/index.js +4 -0
  130. package/dist/index.d.ts +76 -0
  131. package/dist/index.js +57 -0
  132. package/dist/lib/vega/ArcVegaChart.d.ts +24 -0
  133. package/dist/lib/vega/ArcVegaChart.js +42 -0
  134. package/dist/lib/vega/ArcVegaSurface.d.ts +13 -0
  135. package/dist/lib/vega/ArcVegaSurface.js +38 -0
  136. package/dist/lib/vega/axisTicks.js +156 -0
  137. package/dist/lib/vega/config.d.ts +29 -0
  138. package/dist/lib/vega/config.js +113 -0
  139. package/dist/lib/vega/embed.d.ts +29 -0
  140. package/dist/lib/vega/embed.js +29 -0
  141. package/dist/lib/vega/engine.d.ts +10 -0
  142. package/dist/lib/vega/engine.js +6 -0
  143. package/dist/lib/vega/formatters.d.ts +125 -0
  144. package/dist/lib/vega/formatters.js +277 -0
  145. package/dist/lib/vega/index.d.ts +11 -0
  146. package/dist/lib/vega/load.d.ts +13 -0
  147. package/dist/lib/vega/load.js +21 -0
  148. package/dist/lib/vega/time.d.ts +45 -0
  149. package/dist/lib/vega/time.js +105 -0
  150. package/dist/lib/vega/tokens.d.ts +73 -0
  151. package/dist/lib/vega/tokens.js +137 -0
  152. package/dist/lib/vega/tooltip.d.ts +45 -0
  153. package/dist/lib/vega/tooltip.js +124 -0
  154. package/dist/lib/vega/useArcVega.d.ts +43 -0
  155. package/dist/lib/vega/useArcVega.js +121 -0
  156. package/package.json +60 -0
@@ -0,0 +1,788 @@
1
+ import { ArcVegaChart } from "../../lib/vega/ArcVegaChart.js";
2
+ import { arcEmbedOptions } from "../../lib/vega/embed.js";
3
+ import { formatDate } from "../../lib/vega/formatters.js";
4
+ import { useChartTokens } from "../../lib/vega/tokens.js";
5
+ import { ArcTooltipRows, useArcTooltipHandler } from "../../lib/vega/tooltip.js";
6
+ import { SANKEY_NODES_DATASET, SANKEY_RIBBONS_DATASET, SANKEY_SELECTED_PARAM, buildSankeyPlotSpec } from "./SankeyCard.chart.js";
7
+ import { buildSankeyChartDescription, buildSankeyDescription, buildSankeyInsight, createSankeyFormatters, deriveSankeyState, layoutSankey, normalizeSankey, resolveSankeyLabels } from "./SankeyCard.model.js";
8
+ import { Fragment, forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
9
+ import { AILoader, Alert, Badge, Box, Button, Popover, PopoverPopup, PopoverTitle, PopoverTrigger, Skeleton, SkeletonGroup, SystemState, Table, TableBody, TableCaption, TableCell, TableContainer, TableHead, TableHeader, TableRow, Tooltip, TooltipPopup, TooltipTrigger, cn } from "@arcfusionz/arc-primitive-ui";
10
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
11
+ //#region src/components/SankeyCard/SankeyCard.tsx
12
+ const sizeStyles = {
13
+ sm: {
14
+ padding: "sm",
15
+ gap: "gap-2",
16
+ gapPx: 8,
17
+ label: "text-xs",
18
+ body: "text-xs",
19
+ plot: 180,
20
+ nodeRow: 26,
21
+ nodeWidth: 10,
22
+ nodeGap: 8,
23
+ labelSpace: 72,
24
+ headingPx: 16,
25
+ insightLinePx: 18
26
+ },
27
+ md: {
28
+ padding: "md",
29
+ gap: "gap-3",
30
+ gapPx: 12,
31
+ label: "text-sm",
32
+ body: "text-sm",
33
+ plot: 240,
34
+ nodeRow: 30,
35
+ nodeWidth: 12,
36
+ nodeGap: 10,
37
+ labelSpace: 92,
38
+ headingPx: 18,
39
+ insightLinePx: 20
40
+ },
41
+ lg: {
42
+ padding: "lg",
43
+ gap: "gap-3",
44
+ gapPx: 12,
45
+ label: "text-sm",
46
+ body: "text-sm",
47
+ plot: 320,
48
+ nodeRow: 34,
49
+ nodeWidth: 14,
50
+ nodeGap: 12,
51
+ labelSpace: 108,
52
+ headingPx: 20,
53
+ insightLinePx: 20
54
+ }
55
+ };
56
+ const PLOT_PAD_Y = 2;
57
+ const labelClasses = "min-w-0 font-sans font-medium text-muted-foreground";
58
+ const captionClasses = "font-sans text-xs text-muted-foreground";
59
+ const headerControlClasses = "size-6 min-w-0 text-muted-foreground hover:text-foreground";
60
+ const panelStateClasses = "w-full max-w-none gap-2 px-2 py-2 [--artwork-box:3rem]";
61
+ const MISSING_NAME = "Flow";
62
+ const EMPTY_PALETTE = [];
63
+ const EMPTY_ROWS = [];
64
+ /** A flow's or node's colour as a swatch, so identity never rests on colour alone — the name is always beside it. */
65
+ function SankeySwatch({ label, color }) {
66
+ return /* @__PURE__ */ jsxs("span", {
67
+ className: "inline-flex min-w-0 items-center gap-2",
68
+ children: [/* @__PURE__ */ jsx("svg", {
69
+ width: "14",
70
+ height: "10",
71
+ viewBox: "0 0 14 10",
72
+ fill: "none",
73
+ "aria-hidden": "true",
74
+ className: "shrink-0",
75
+ style: { color },
76
+ children: /* @__PURE__ */ jsx("rect", {
77
+ x: "0",
78
+ y: "1",
79
+ width: "14",
80
+ height: "8",
81
+ rx: "1",
82
+ fill: "currentColor",
83
+ fillOpacity: "0.7"
84
+ })
85
+ }), label]
86
+ });
87
+ }
88
+ function InfoIcon(props) {
89
+ return /* @__PURE__ */ jsxs("svg", {
90
+ viewBox: "0 0 24 24",
91
+ fill: "none",
92
+ stroke: "currentColor",
93
+ strokeWidth: 2,
94
+ strokeLinecap: "round",
95
+ strokeLinejoin: "round",
96
+ "aria-hidden": "true",
97
+ ...props,
98
+ children: [/* @__PURE__ */ jsx("circle", {
99
+ cx: "12",
100
+ cy: "12",
101
+ r: "9"
102
+ }), /* @__PURE__ */ jsx("path", { d: "M12 11v5M12 8h.01" })]
103
+ });
104
+ }
105
+ /** A generated insight: words as text, values in bold. */
106
+ function InsightText({ segments }) {
107
+ return /* @__PURE__ */ jsx(Fragment$1, { children: segments.map((segment, index) => typeof segment === "string" ? /* @__PURE__ */ jsx(Fragment, { children: segment }, index) : /* @__PURE__ */ jsx("strong", { children: segment.value }, index)) });
108
+ }
109
+ function useMergedRef(forwarded) {
110
+ const inner = useRef(null);
111
+ return [inner, useCallback((node) => {
112
+ inner.current = node;
113
+ if (typeof forwarded === "function") forwarded(node);
114
+ else if (forwarded) forwarded.current = node;
115
+ }, [forwarded])];
116
+ }
117
+ function usePlotWidth(ref, enabled) {
118
+ const [width, setWidth] = useState(0);
119
+ useEffect(() => {
120
+ if (!enabled) return;
121
+ const node = ref.current;
122
+ if (!node) return;
123
+ let frame = 0;
124
+ const measure = () => setWidth((previous) => {
125
+ const next = Math.round(node.getBoundingClientRect().width);
126
+ return previous === next ? previous : next;
127
+ });
128
+ measure();
129
+ if (typeof ResizeObserver === "undefined") return;
130
+ const observer = new ResizeObserver(() => {
131
+ cancelAnimationFrame(frame);
132
+ frame = requestAnimationFrame(measure);
133
+ });
134
+ observer.observe(node);
135
+ return () => {
136
+ cancelAnimationFrame(frame);
137
+ observer.disconnect();
138
+ };
139
+ }, [ref, enabled]);
140
+ return width;
141
+ }
142
+ /**
143
+ * A flow panel — the tenth member of the analytics card family, and the only
144
+ * one whose subject is the **edges of a graph** rather than a measure.
145
+ *
146
+ * `links` of `{ source, target, value }` become a sankey: stages as columns,
147
+ * nodes as rects, and each flow as a ribbon whose thickness is its quantity.
148
+ * It answers the one question no other card can — *what became of this
149
+ * volume, and where did it leak* — because only a flow diagram draws an edge
150
+ * as a quantity while keeping the chain intact.
151
+ *
152
+ * The card enforces what makes the form honest. A cycle is broken and
153
+ * reported rather than drawn. Volume that arrives and does not leave shows as
154
+ * unfilled node and is named, never balanced away. A graph with fewer than
155
+ * two columns is refused, because a part-to-whole belongs in a `Progress`
156
+ * bar. Colour defaults to a single emphasis hue, since thickness and position
157
+ * already carry the comparison. And every quantity a ribbon encodes is also
158
+ * text — in the tooltip, the flow table with its share of the source, and the
159
+ * node table with in, out and what did not reconcile — which is what makes
160
+ * the weakest channel in the family safe to use.
161
+ */
162
+ const SankeyCard = forwardRef(({ label, name: nameProp, hint, links, nodes, align = "justify", color = "none", nodeOrder, maxNodesPerStage = 8, nodeLabels = "all", showValues = false, stages, nodeWidth: nodeWidthProp, nodeGap: nodeGapProp, curvature = .5, iterations = 6, height: heightProp, selectedNode, defaultSelectedNode, onNodeSelect, format = "compact", shareFormat = "percent:0", formatValue, expectedStages = 3, expectedNodes = 3, insight, status = "ready", filtered = false, stale = false, updatedAt, updatedAtFormat = "day", onRetry, onClearFilters, size = "md", titleLevel = 3, locale, timeZone, labels: labelOverrides, description, action, defaultTableOpen = false, hideTableToggle = false, hideNotes = false, onEmbed, patchSpec, render, className, ...rest }, forwardedRef) => {
163
+ const labels = useMemo(() => resolveSankeyLabels(labelOverrides), [labelOverrides]);
164
+ const sizes = sizeStyles[size];
165
+ const name = nameProp ?? (typeof label === "string" ? label : MISSING_NAME);
166
+ const id = useId();
167
+ const headingId = `${id}-label`;
168
+ const descriptionId = `${id}-description`;
169
+ const hintId = `${id}-hint`;
170
+ const [rootRef, setRootRef] = useMergedRef(forwardedRef);
171
+ const plotRef = useRef(null);
172
+ const [popoverContainer, setPopoverContainer] = useState(null);
173
+ const tokens = useChartTokens(rootRef);
174
+ const [selectedState, setSelectedState] = useState(defaultSelectedNode ?? null);
175
+ const selectedKey = selectedNode !== void 0 ? selectedNode : selectedState;
176
+ const selectNode = useCallback((key) => {
177
+ if (selectedNode === void 0) setSelectedState(key);
178
+ onNodeSelect?.(key);
179
+ }, [selectedNode, onNodeSelect]);
180
+ const palette = tokens?.category ?? EMPTY_PALETTE;
181
+ const summary = useMemo(() => tokens ? normalizeSankey(nodes, links, {
182
+ align,
183
+ nodeOrder,
184
+ color,
185
+ maxNodesPerStage,
186
+ palette,
187
+ emphasis: tokens.emphasis,
188
+ context: tokens.context,
189
+ positive: tokens.divergingPositive,
190
+ negative: tokens.divergingNegative,
191
+ otherLabel: labels.other
192
+ }) : null, [
193
+ tokens,
194
+ nodes,
195
+ links,
196
+ align,
197
+ nodeOrder,
198
+ color,
199
+ maxNodesPerStage,
200
+ palette,
201
+ labels.other
202
+ ]);
203
+ const formatters = useMemo(() => createSankeyFormatters({
204
+ format,
205
+ shareFormat,
206
+ locale,
207
+ formatValue
208
+ }), [
209
+ format,
210
+ shareFormat,
211
+ locale,
212
+ formatValue
213
+ ]);
214
+ const hasData = (links?.length ?? 0) > 0;
215
+ const hasFlow = (summary?.count ?? 0) > 0;
216
+ const state = deriveSankeyState({
217
+ status,
218
+ hasFlow,
219
+ hasData,
220
+ filtered,
221
+ stages: summary?.stages ?? 0,
222
+ count: summary?.count ?? 0
223
+ });
224
+ const showContent = state === "ready" || state === "refreshing";
225
+ const hasPlot = showContent && summary != null && summary.count > 0;
226
+ const asOfText = useMemo(() => {
227
+ if (updatedAt == null) return null;
228
+ return typeof updatedAtFormat === "function" ? updatedAtFormat(new Date(updatedAt)) : formatDate(updatedAt, {
229
+ granularity: updatedAtFormat,
230
+ locale,
231
+ timeZone
232
+ });
233
+ }, [
234
+ updatedAt,
235
+ updatedAtFormat,
236
+ locale,
237
+ timeZone
238
+ ]);
239
+ const cardDescription = description ?? buildSankeyDescription({
240
+ name,
241
+ state,
242
+ summary,
243
+ formatters,
244
+ asOfText,
245
+ labels
246
+ });
247
+ const nodeWidth = nodeWidthProp ?? sizes.nodeWidth;
248
+ const nodeGap = nodeGapProp ?? sizes.nodeGap;
249
+ const columnCount = summary?.stages ?? Math.max(2, expectedStages);
250
+ const busiestColumn = Math.max(expectedNodes, ...summary?.columns.map((column) => column.length) ?? [0]);
251
+ const plotHeight = Math.max(heightProp ?? sizes.plot, busiestColumn * sizes.nodeRow);
252
+ const blockHeight = plotHeight + 4;
253
+ const labelEndsOutward = nodeLabels === "ends";
254
+ const drawLabels = nodeLabels !== "none";
255
+ const gutterLeft = nodeWidth / 2 + (drawLabels && labelEndsOutward ? sizes.labelSpace : 0);
256
+ const gutterRight = nodeWidth / 2 + (drawLabels ? sizes.labelSpace : 0);
257
+ const layout = useMemo(() => layoutSankey(summary, {
258
+ plotHeight,
259
+ nodeGap,
260
+ iterations,
261
+ curvature,
262
+ labels: nodeLabels,
263
+ formatValue: formatters.formatValue,
264
+ formatShare: formatters.formatShare,
265
+ missing: "—"
266
+ }), [
267
+ summary,
268
+ plotHeight,
269
+ nodeGap,
270
+ iterations,
271
+ curvature,
272
+ nodeLabels,
273
+ formatters
274
+ ]);
275
+ const datasets = useMemo(() => ({
276
+ [SANKEY_RIBBONS_DATASET]: layout.ribbons.length > 0 ? layout.ribbons : EMPTY_ROWS,
277
+ [SANKEY_NODES_DATASET]: layout.nodes.length > 0 ? layout.nodes : EMPTY_ROWS
278
+ }), [layout]);
279
+ const frameWidth = usePlotWidth(plotRef, hasPlot);
280
+ const chartWidth = Math.max(0, frameWidth - gutterLeft - gutterRight);
281
+ const selectable = onNodeSelect != null || selectedNode === void 0;
282
+ const spec = useMemo(() => {
283
+ if (!tokens) return null;
284
+ const interior = columnCount > 1 ? `max(32, width / ${columnCount - 1} - ${nodeWidth + 14})` : String(sizes.labelSpace);
285
+ const built = buildSankeyPlotSpec({
286
+ height: plotHeight,
287
+ nodeWidth,
288
+ padLeft: gutterLeft,
289
+ padRight: gutterRight,
290
+ padTop: PLOT_PAD_Y,
291
+ padBottom: PLOT_PAD_Y,
292
+ labelLimit: labelEndsOutward ? String(Math.max(32, sizes.labelSpace - 6)) : `datum.last ? ${Math.max(32, sizes.labelSpace - 6)} : ${interior}`,
293
+ labels: drawLabels,
294
+ labelEndsOutward,
295
+ showValues,
296
+ tooltip: true,
297
+ selectable,
298
+ valueFormat: locale ? `${format}@${locale}` : format,
299
+ fromTitle: labels.tableFrom,
300
+ toTitle: labels.tableTo,
301
+ valueTitle: labels.tableValue,
302
+ locale
303
+ }, tokens);
304
+ return patchSpec ? patchSpec(built) : built;
305
+ }, [
306
+ tokens,
307
+ plotHeight,
308
+ nodeWidth,
309
+ gutterLeft,
310
+ gutterRight,
311
+ columnCount,
312
+ sizes.labelSpace,
313
+ drawLabels,
314
+ labelEndsOutward,
315
+ showValues,
316
+ selectable,
317
+ format,
318
+ locale,
319
+ labels.tableFrom,
320
+ labels.tableTo,
321
+ labels.tableValue,
322
+ patchSpec
323
+ ]);
324
+ const signals = useMemo(() => selectable ? { [SANKEY_SELECTED_PARAM]: selectedKey ?? null } : void 0, [selectable, selectedKey]);
325
+ const chartDescription = summary && hasPlot ? buildSankeyChartDescription({
326
+ name,
327
+ summary,
328
+ formatters,
329
+ labels
330
+ }) : void 0;
331
+ const tooltipContent = useCallback(({ item }) => {
332
+ const datum = item.datum;
333
+ if (!datum) return null;
334
+ if (datum.sl != null && datum.tl != null) return /* @__PURE__ */ jsx(ArcTooltipRows, { rows: [
335
+ [/* @__PURE__ */ jsx(SankeySwatch, {
336
+ color: datum.c,
337
+ label: `${datum.sl} → ${datum.tl}`
338
+ }, "flow"), ""],
339
+ [labels.tableValue, formatters.formatValue(datum.v)],
340
+ [labels.shareTitle, datum.sh ?? "—"]
341
+ ] });
342
+ if (datum.label != null) return /* @__PURE__ */ jsx(ArcTooltipRows, { rows: [[datum.label, datum.vt ?? "—"]] });
343
+ return null;
344
+ }, [
345
+ formatters,
346
+ labels.tableValue,
347
+ labels.shareTitle
348
+ ]);
349
+ const { handler: tooltipHandler, tooltip } = useArcTooltipHandler({ content: tooltipContent });
350
+ const embedOptions = useMemo(() => arcEmbedOptions({ tooltip: tooltipHandler }), [tooltipHandler]);
351
+ const generatedInsight = useMemo(() => insight === void 0 ? buildSankeyInsight({
352
+ name,
353
+ summary,
354
+ formatters,
355
+ labels
356
+ }) : null, [
357
+ insight,
358
+ name,
359
+ summary,
360
+ formatters,
361
+ labels
362
+ ]);
363
+ const showInsight = insight === void 0 ? generatedInsight != null : insight != null && insight !== false;
364
+ const selectedInfo = summary?.nodes.find((node) => node.key === selectedKey) ?? null;
365
+ const announcement = selectedInfo ? labels.selectedNode(selectedInfo.label) : "";
366
+ const notes = [];
367
+ if (summary) {
368
+ if (summary.cycles > 0) notes.push(labels.cyclesNote(summary.cycles));
369
+ if (summary.imbalanced.length > 0) {
370
+ const amount = summary.imbalanced.reduce((sum, node) => sum + Math.abs(node.imbalance), 0);
371
+ notes.push(labels.imbalanceNote(summary.imbalanced.length, formatters.formatValue(amount)));
372
+ }
373
+ if (summary.folded) notes.push(labels.foldedNote(labels.other));
374
+ if (layout.hairlines > 0) notes.push(labels.hairlineNote(layout.hairlines));
375
+ if (summary.overIdentity) notes.push(labels.overIdentityNote(3));
376
+ const otherRejects = summary.rejected.filter((entry) => entry.reason !== "cycle");
377
+ if (otherRejects.length > 0) notes.push(labels.rejectedNote(otherRejects.length));
378
+ }
379
+ const showNotes = !hideNotes && showContent && notes.length > 0;
380
+ const showStages = (stages?.length ?? 0) > 0;
381
+ const bodyMinHeight = (showStages ? sizes.headingPx + sizes.gapPx : 0) + blockHeight + (showInsight ? sizes.gapPx + sizes.insightLinePx * 2 : 0);
382
+ const HeadingTag = `h${titleLevel}`;
383
+ const busy = state === "loading" || state === "refreshing";
384
+ return /* @__PURE__ */ jsxs(Box, {
385
+ ref: setRootRef,
386
+ render: render ?? /* @__PURE__ */ jsx("figure", {}),
387
+ padding: sizes.padding,
388
+ "aria-labelledby": headingId,
389
+ "aria-describedby": descriptionId,
390
+ "aria-busy": busy || void 0,
391
+ "data-status": status,
392
+ "data-state": state,
393
+ "data-size": size,
394
+ "data-align": align,
395
+ "data-color": color,
396
+ "data-labels": nodeLabels,
397
+ "data-stale": stale ? "" : void 0,
398
+ "data-empty": state === "empty" || state === "no-match" ? "" : void 0,
399
+ "data-selected": selectedInfo?.key,
400
+ "data-cycles": summary && summary.cycles > 0 ? String(summary.cycles) : void 0,
401
+ "data-imbalanced": summary && summary.imbalanced.length > 0 ? "" : void 0,
402
+ "data-folded": summary?.folded ? "" : void 0,
403
+ "data-hairlines": layout.hairlines > 0 ? String(layout.hairlines) : void 0,
404
+ "data-over-identity": summary?.overIdentity ? "" : void 0,
405
+ className: cn("relative flex min-w-0 flex-col", sizes.gap, className),
406
+ ...rest,
407
+ children: [
408
+ /* @__PURE__ */ jsx("div", {
409
+ ref: setPopoverContainer,
410
+ className: "contents"
411
+ }),
412
+ /* @__PURE__ */ jsxs("div", {
413
+ className: "flex min-w-0 items-start justify-between gap-2",
414
+ children: [/* @__PURE__ */ jsx(HeadingTag, {
415
+ id: headingId,
416
+ className: cn(labelClasses, sizes.label, "truncate"),
417
+ "data-part": "label",
418
+ children: label
419
+ }), /* @__PURE__ */ jsxs("div", {
420
+ className: "flex shrink-0 items-center gap-1",
421
+ children: [
422
+ stale && /* @__PURE__ */ jsx(Badge, {
423
+ variant: "warning",
424
+ appearance: "soft",
425
+ size: "sm",
426
+ "data-part": "stale",
427
+ children: labels.stale
428
+ }),
429
+ hint && /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
430
+ delay: 150,
431
+ render: /* @__PURE__ */ jsx(Button, {
432
+ variant: "ghost",
433
+ size: "sm",
434
+ iconOnly: true,
435
+ "aria-label": labels.about(name),
436
+ "aria-describedby": hintId,
437
+ className: headerControlClasses
438
+ }),
439
+ children: /* @__PURE__ */ jsx(InfoIcon, { className: "size-4" })
440
+ }), /* @__PURE__ */ jsx(TooltipPopup, {
441
+ side: "top",
442
+ sideOffset: 6,
443
+ container: popoverContainer,
444
+ children: /* @__PURE__ */ jsx("span", {
445
+ id: hintId,
446
+ children: hint
447
+ })
448
+ })] }),
449
+ !hideTableToggle && hasPlot && summary && /* @__PURE__ */ jsxs(Popover, {
450
+ defaultOpen: defaultTableOpen,
451
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, {
452
+ render: /* @__PURE__ */ jsx(Button, {
453
+ variant: "ghost",
454
+ size: "sm",
455
+ iconOnly: true,
456
+ "aria-label": labels.showTable(name),
457
+ className: headerControlClasses
458
+ }),
459
+ children: /* @__PURE__ */ jsx(InfoIcon, { className: "size-4" })
460
+ }), /* @__PURE__ */ jsxs(PopoverPopup, {
461
+ side: "bottom",
462
+ align: "end",
463
+ sideOffset: 8,
464
+ collisionPadding: 8,
465
+ container: popoverContainer,
466
+ className: "w-[min(38rem,var(--available-width))]",
467
+ "data-part": "popover",
468
+ children: [
469
+ /* @__PURE__ */ jsx(PopoverTitle, {
470
+ className: "sr-only",
471
+ children: labels.tableCaption(name)
472
+ }),
473
+ /* @__PURE__ */ jsxs(TableContainer, {
474
+ variant: "plain",
475
+ tabIndex: 0,
476
+ role: "region",
477
+ "aria-label": labels.tableCaption(name),
478
+ className: "max-h-[min(24rem,calc(var(--available-height)-5rem))] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
479
+ "data-part": "table",
480
+ children: [/* @__PURE__ */ jsxs(Table, {
481
+ size: "sm",
482
+ stickyHeader: true,
483
+ hoverable: false,
484
+ children: [
485
+ /* @__PURE__ */ jsx(TableCaption, {
486
+ side: "top",
487
+ className: "sr-only",
488
+ children: labels.flowsCaption(name)
489
+ }),
490
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
491
+ /* @__PURE__ */ jsx(TableHead, { children: labels.tableFrom }),
492
+ /* @__PURE__ */ jsx(TableHead, { children: labels.tableTo }),
493
+ /* @__PURE__ */ jsx(TableHead, {
494
+ className: "text-right",
495
+ children: labels.tableValue
496
+ }),
497
+ /* @__PURE__ */ jsx(TableHead, {
498
+ className: "text-right",
499
+ children: labels.tableShare
500
+ })
501
+ ] }) }),
502
+ /* @__PURE__ */ jsx(TableBody, { children: summary.links.map((link) => /* @__PURE__ */ jsxs(TableRow, {
503
+ "data-tone": link.tone,
504
+ children: [
505
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(SankeySwatch, {
506
+ color: link.color,
507
+ label: link.sourceLabel
508
+ }) }),
509
+ /* @__PURE__ */ jsx(TableCell, { children: link.targetLabel }),
510
+ /* @__PURE__ */ jsx(TableCell, {
511
+ className: "text-right tabular-nums",
512
+ children: formatters.formatValue(link.value)
513
+ }),
514
+ /* @__PURE__ */ jsx(TableCell, {
515
+ className: "text-right tabular-nums text-muted-foreground",
516
+ children: formatters.formatShare(link.share)
517
+ })
518
+ ]
519
+ }, link.id)) })
520
+ ]
521
+ }), /* @__PURE__ */ jsxs(Table, {
522
+ size: "sm",
523
+ stickyHeader: true,
524
+ hoverable: false,
525
+ className: "mt-4",
526
+ children: [
527
+ /* @__PURE__ */ jsx(TableCaption, {
528
+ side: "top",
529
+ className: "sr-only",
530
+ children: labels.nodesCaption(name)
531
+ }),
532
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
533
+ /* @__PURE__ */ jsx(TableHead, { children: labels.tableNode }),
534
+ /* @__PURE__ */ jsx(TableHead, {
535
+ className: "text-right",
536
+ children: labels.tableStage
537
+ }),
538
+ /* @__PURE__ */ jsx(TableHead, {
539
+ className: "text-right",
540
+ children: labels.tableIn
541
+ }),
542
+ /* @__PURE__ */ jsx(TableHead, {
543
+ className: "text-right",
544
+ children: labels.tableOut
545
+ }),
546
+ /* @__PURE__ */ jsx(TableHead, {
547
+ className: "text-right",
548
+ children: labels.tableUnaccounted
549
+ })
550
+ ] }) }),
551
+ /* @__PURE__ */ jsx(TableBody, { children: summary.nodes.map((node) => {
552
+ const isSelected = node.key === selectedKey;
553
+ return /* @__PURE__ */ jsxs(TableRow, {
554
+ "data-selected": isSelected ? "" : void 0,
555
+ children: [
556
+ /* @__PURE__ */ jsxs(TableCell, { children: [selectable ? /* @__PURE__ */ jsx(Button, {
557
+ variant: "ghost",
558
+ size: "sm",
559
+ "aria-pressed": isSelected,
560
+ "aria-label": labels.selectNode(node.label),
561
+ onClick: () => selectNode(isSelected ? null : node.key),
562
+ className: cn("h-auto max-w-full justify-start px-1 py-0.5", isSelected && "font-semibold text-foreground"),
563
+ "data-part": "node-button",
564
+ children: /* @__PURE__ */ jsx(SankeySwatch, {
565
+ color: node.color,
566
+ label: /* @__PURE__ */ jsx("span", {
567
+ className: "min-w-0 truncate",
568
+ children: node.label
569
+ })
570
+ })
571
+ }) : /* @__PURE__ */ jsx(SankeySwatch, {
572
+ color: node.color,
573
+ label: node.label
574
+ }), node.folded && node.members.length > 0 && /* @__PURE__ */ jsx("span", {
575
+ className: cn(captionClasses, "mt-0.5 block"),
576
+ "data-part": "fold-members",
577
+ children: labels.otherMembers(node.members.join(", "))
578
+ })] }),
579
+ /* @__PURE__ */ jsx(TableCell, {
580
+ className: "text-right tabular-nums text-muted-foreground",
581
+ children: node.depth + 1
582
+ }),
583
+ /* @__PURE__ */ jsx(TableCell, {
584
+ className: "text-right tabular-nums",
585
+ children: node.isSource ? "—" : formatters.formatValue(node.inValue)
586
+ }),
587
+ /* @__PURE__ */ jsx(TableCell, {
588
+ className: "text-right tabular-nums",
589
+ children: node.isSink ? "—" : formatters.formatValue(node.outValue)
590
+ }),
591
+ /* @__PURE__ */ jsx(TableCell, {
592
+ className: cn("text-right tabular-nums", node.imbalance !== 0 && !node.isSource && !node.isSink ? "font-medium text-chart-negative" : "text-muted-foreground"),
593
+ children: node.isSource || node.isSink || node.imbalance === 0 ? "—" : formatters.formatValue(node.imbalance)
594
+ })
595
+ ]
596
+ }, node.key);
597
+ }) })
598
+ ]
599
+ })]
600
+ }),
601
+ selectedInfo && /* @__PURE__ */ jsx(Button, {
602
+ variant: "secondary",
603
+ size: "sm",
604
+ onClick: () => selectNode(null),
605
+ className: "mt-3",
606
+ "data-part": "clear-selection",
607
+ children: labels.clearSelection
608
+ })
609
+ ]
610
+ })]
611
+ }),
612
+ action
613
+ ]
614
+ })]
615
+ }),
616
+ /* @__PURE__ */ jsx("p", {
617
+ id: descriptionId,
618
+ className: "sr-only",
619
+ children: cardDescription
620
+ }),
621
+ selectable && /* @__PURE__ */ jsx("p", {
622
+ role: "status",
623
+ "aria-live": "polite",
624
+ className: "sr-only",
625
+ "data-part": "announcement",
626
+ children: announcement
627
+ }),
628
+ /* @__PURE__ */ jsxs("div", {
629
+ className: "relative flex min-w-0 flex-col",
630
+ style: { minHeight: bodyMinHeight },
631
+ children: [
632
+ state === "loading" && /* @__PURE__ */ jsxs(SkeletonGroup, {
633
+ className: cn("flex flex-col", sizes.gap),
634
+ children: [
635
+ showStages && /* @__PURE__ */ jsx("div", {
636
+ className: "flex gap-2",
637
+ children: Array.from({ length: Math.max(2, expectedStages) }, (_, index) => /* @__PURE__ */ jsx(Skeleton, {
638
+ variant: "rounded",
639
+ className: "flex-1",
640
+ style: { height: sizes.headingPx }
641
+ }, index))
642
+ }),
643
+ /* @__PURE__ */ jsx(Skeleton, {
644
+ variant: "rounded",
645
+ className: "w-full",
646
+ style: { height: blockHeight }
647
+ }),
648
+ showInsight && /* @__PURE__ */ jsxs("div", {
649
+ className: "flex items-start gap-2",
650
+ children: [/* @__PURE__ */ jsx(Skeleton, {
651
+ variant: "rounded",
652
+ className: "size-4 shrink-0"
653
+ }), /* @__PURE__ */ jsx(Skeleton, {
654
+ variant: "rounded",
655
+ className: "w-full",
656
+ style: { height: sizes.insightLinePx }
657
+ })]
658
+ })
659
+ ]
660
+ }),
661
+ state === "error" && /* @__PURE__ */ jsx(SystemState, {
662
+ variant: "server-error",
663
+ scope: "inline",
664
+ title: labels.error,
665
+ description: false,
666
+ action: onRetry ? /* @__PURE__ */ jsx(Button, {
667
+ size: "sm",
668
+ onClick: onRetry,
669
+ children: labels.retry
670
+ }) : void 0,
671
+ className: cn(panelStateClasses, "my-auto")
672
+ }),
673
+ state === "empty" && /* @__PURE__ */ jsx(SystemState, {
674
+ variant: "empty",
675
+ scope: "inline",
676
+ title: labels.noData,
677
+ description: false,
678
+ className: cn(panelStateClasses, "my-auto")
679
+ }),
680
+ state === "no-match" && /* @__PURE__ */ jsx(SystemState, {
681
+ variant: "no-results",
682
+ scope: "inline",
683
+ title: labels.noMatch,
684
+ description: false,
685
+ action: onClearFilters ? /* @__PURE__ */ jsx(Button, {
686
+ size: "sm",
687
+ variant: "secondary",
688
+ onClick: onClearFilters,
689
+ children: labels.clearFilters
690
+ }) : void 0,
691
+ className: cn(panelStateClasses, "my-auto")
692
+ }),
693
+ state === "insufficient" && /* @__PURE__ */ jsx(SystemState, {
694
+ variant: "empty",
695
+ scope: "inline",
696
+ title: labels.insufficient,
697
+ description: false,
698
+ className: cn(panelStateClasses, "my-auto"),
699
+ "data-part": "insufficient"
700
+ }),
701
+ showContent && /* @__PURE__ */ jsxs("div", {
702
+ className: cn("flex min-w-0 flex-col", sizes.gap, state === "refreshing" && "bg-veil"),
703
+ children: [
704
+ showStages && /* @__PURE__ */ jsx("div", {
705
+ className: "relative",
706
+ style: {
707
+ height: sizes.headingPx,
708
+ marginLeft: gutterLeft,
709
+ marginRight: gutterRight
710
+ },
711
+ "data-part": "stages",
712
+ children: layout.columnCenters.map((center, index) => {
713
+ const heading = stages?.[index];
714
+ if (!heading) return null;
715
+ const first = index === 0;
716
+ const last = index === layout.columnCenters.length - 1;
717
+ return /* @__PURE__ */ jsx("span", {
718
+ className: cn(captionClasses, "absolute top-0 max-w-[9rem] truncate font-medium", first ? "text-left" : last ? "text-right" : "text-center"),
719
+ style: {
720
+ left: `${center * 100}%`,
721
+ transform: first ? "none" : last ? "translateX(-100%)" : "translateX(-50%)"
722
+ },
723
+ children: heading
724
+ }, index);
725
+ })
726
+ }),
727
+ /* @__PURE__ */ jsx("div", {
728
+ ref: plotRef,
729
+ className: "relative w-full",
730
+ style: { height: blockHeight },
731
+ "data-part": "plot-block",
732
+ children: hasPlot && chartWidth > 0 && spec && /* @__PURE__ */ jsx(ArcVegaChart, {
733
+ spec,
734
+ options: embedOptions,
735
+ datasets,
736
+ description: chartDescription,
737
+ width: chartWidth,
738
+ height: plotHeight,
739
+ signals,
740
+ onEmbed,
741
+ className: "absolute inset-0 [&_svg]:block [&_.vega-bindings:empty]:hidden",
742
+ "data-part": "plot",
743
+ fallback: /* @__PURE__ */ jsx(Skeleton, {
744
+ variant: "rounded",
745
+ className: "absolute inset-0"
746
+ })
747
+ })
748
+ }),
749
+ showNotes && /* @__PURE__ */ jsx(Alert, {
750
+ variant: "warning",
751
+ appearance: "soft",
752
+ role: "note",
753
+ "data-part": "notes",
754
+ children: notes.join(" ")
755
+ }),
756
+ showInsight && /* @__PURE__ */ jsxs("div", {
757
+ className: cn("flex items-start gap-2 font-sans text-foreground", sizes.body),
758
+ "data-part": "insight",
759
+ children: [/* @__PURE__ */ jsx(AILoader, {
760
+ size: "sm",
761
+ state: "default",
762
+ label: labels.insightMark,
763
+ className: cn("shrink-0", size === "sm" ? "" : "mt-0.5")
764
+ }), /* @__PURE__ */ jsx("p", {
765
+ className: "min-w-0 flex-1 text-xs leading-[1.8] [&_strong]:font-semibold [&_strong]:text-foreground",
766
+ children: insight === void 0 && generatedInsight ? /* @__PURE__ */ jsx(InsightText, { segments: generatedInsight }) : insight
767
+ })]
768
+ })
769
+ ]
770
+ })
771
+ ]
772
+ }),
773
+ asOfText && /* @__PURE__ */ jsxs("figcaption", {
774
+ className: cn(captionClasses, "text-right"),
775
+ "data-part": "as-of",
776
+ children: [
777
+ labels.asOf,
778
+ " ",
779
+ asOfText
780
+ ]
781
+ }),
782
+ tooltip
783
+ ]
784
+ });
785
+ });
786
+ SankeyCard.displayName = "SankeyCard";
787
+ //#endregion
788
+ export { SankeyCard };