@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,553 @@
1
+ import { formatDate, formatDateRange, formatDelta, formatNumber } from "../../lib/vega/formatters.js";
2
+ //#region src/components/AnomalyCard/AnomalyCard.model.ts
3
+ const DEFAULT_ANOMALY_LABELS = {
4
+ high: "Above range",
5
+ low: "Below range",
6
+ mixed: "Both directions",
7
+ inRange: "In range",
8
+ percentagePoints: " pp",
9
+ noData: "No data",
10
+ noMatch: "No matching data",
11
+ clearFilters: "Clear filters",
12
+ error: "Couldn't load this metric",
13
+ retry: "Try again",
14
+ insufficient: "Not enough data to judge",
15
+ unjudged: "No expected range supplied — readings are plotted but not judged",
16
+ stale: "Stale",
17
+ asOf: "As of",
18
+ verdicts: {
19
+ clear: "In range",
20
+ watch: "Recovered",
21
+ breach: "Out of range"
22
+ },
23
+ seriesKey: "Actual",
24
+ bandKey: "Expected range",
25
+ expectedKey: "Expected",
26
+ flagKey: "Anomaly",
27
+ tablePeriod: "Period",
28
+ tableValue: "Value",
29
+ tableExpected: "Expected",
30
+ tableRange: "Range",
31
+ tableDeviation: "Deviation",
32
+ tableFlag: "Flag",
33
+ score: "Score",
34
+ episodes: "Excursions",
35
+ episodeGroup: (name) => `Excursions in ${name}`,
36
+ openEpisode: ({ range, direction, periods, deviation, score, ongoing }) => `Open the ${range} excursion — ${direction}, ${periods}` + (deviation ? `, ${deviation}` : "") + (score ? `, score ${score}` : "") + (ongoing ? ", ongoing" : ""),
37
+ ongoing: "ongoing",
38
+ periodCount: (count) => `${count} ${count === 1 ? "period" : "periods"}`,
39
+ moreEpisodes: (count) => `${count} more`,
40
+ noEpisodes: "No excursions in this window",
41
+ showTable: (name) => `Show the data behind ${name}`,
42
+ hideTable: (name) => `Hide the data behind ${name}`,
43
+ about: (name) => `About ${name}`,
44
+ loading: (name) => `Loading ${name}`,
45
+ tableCaption: (name) => `Data behind ${name}`,
46
+ standingGroup: (name) => `${name} against its expected range`,
47
+ explanation: ({ periodText, valueText, expectedText, rangeText, deviationText, secondaryText, scoreText, direction }) => {
48
+ const rows = [["Period", periodText], ["Value", valueText]];
49
+ if (expectedText) rows.push(["Expected", expectedText]);
50
+ rows.push(["Range", rangeText]);
51
+ rows.push([direction === "high" ? "Above range" : direction === "low" ? "Below range" : "Deviation", deviationText]);
52
+ if (secondaryText) rows.push(["Deviation %", secondaryText]);
53
+ if (scoreText) rows.push(["Score", scoreText]);
54
+ return rows;
55
+ },
56
+ expectedRange: (range) => `Expected ${range}`,
57
+ ofJudged: (count) => `of ${count} periods judged`,
58
+ verdictSentence: ({ verdict, name, count, range, latest }) => {
59
+ if (verdict === "clear") return `${name} stayed inside its expected range across ${range}.`;
60
+ if (verdict === "breach") return count === 1 ? `${name} is outside its expected range now, at ${latest}.` : `${name} is outside its expected range now, at ${latest}, after ${count} excursions across ${range}.`;
61
+ return count === 1 ? `${name} left its expected range once across ${range} and is back inside, at ${latest}.` : `${name} left its expected range ${count} times across ${range} and is back inside, at ${latest}.`;
62
+ },
63
+ worstSentence: ({ period, value, deviation, bound, direction }) => `Furthest out on ${period}: ${value}, ${deviation} ${direction.toLowerCase()} the expected ${bound}.`,
64
+ scoreSentence: (score) => `Peak score ${score}.`,
65
+ markNames: {
66
+ line: "Line",
67
+ area: "Area"
68
+ },
69
+ chartDescription: ({ mark, name, periodCount, range, min, max, flagged, band }) => `${mark} chart of ${name} against its expected range: ${periodCount} periods, ${range}, ranging ${min} to ${max}.` + (band ? ` ${band}.` : "") + (flagged === 0 ? " No periods outside the range." : ` ${flagged} ${flagged === 1 ? "period" : "periods"} marked outside the range.`),
70
+ insightMark: "Insight",
71
+ insight: ({ name, verdict, count, range, worstAt, worstValue, worstDeviation, worstBound, direction, ongoing }) => {
72
+ if (verdict === "clear") return [`${name} held inside its expected range across ${range}.`];
73
+ const segments = [
74
+ `${name} left its expected range `,
75
+ { value: count === 1 ? "once" : `${count} times` },
76
+ ` across ${range}`
77
+ ];
78
+ if (worstAt && worstValue && worstDeviation && worstBound && direction) segments.push(`, furthest on ${worstAt} at `, { value: worstValue }, ` — `, { value: worstDeviation }, ` ${direction.toLowerCase()} the expected ${worstBound}`);
79
+ segments.push(ongoing ? ". It is still outside the range." : ". It is back inside the range.");
80
+ return segments;
81
+ }
82
+ };
83
+ function resolveAnomalyLabels(overrides) {
84
+ return overrides ? {
85
+ ...DEFAULT_ANOMALY_LABELS,
86
+ ...overrides
87
+ } : DEFAULT_ANOMALY_LABELS;
88
+ }
89
+ /**
90
+ * The state machine. Error wins; loading over existing readings is a refresh
91
+ * (the previous render stays under the veil); no readings is `no-match` when
92
+ * the consumer's filter caused it and `empty` otherwise.
93
+ */
94
+ function deriveAnomalyState({ status, hasValue, hasData, filtered }) {
95
+ if (status === "error") return "error";
96
+ if (status === "loading") return hasData ? "refreshing" : "loading";
97
+ if (hasValue) return "ready";
98
+ return filtered ? "no-match" : "empty";
99
+ }
100
+ const isFinite = (value) => typeof value === "number" && Number.isFinite(value);
101
+ const DAY_MS = 864e5;
102
+ const ISO_DATE = /^\d{4}-\d{2}(-\d{2})?([T ].*)?$/;
103
+ function inferXType(points) {
104
+ const sample = points.find((point) => point.x != null);
105
+ if (!sample) return "ordinal";
106
+ const { x } = sample;
107
+ if (x instanceof Date) return "temporal";
108
+ if (typeof x === "number") return "quantitative";
109
+ return ISO_DATE.test(x) ? "temporal" : "ordinal";
110
+ }
111
+ function inferGranularity(periods) {
112
+ const xs = periods.filter((x) => typeof x === "number");
113
+ if (xs.length < 2) return "day";
114
+ const gaps = [];
115
+ for (let index = 1; index < xs.length; index += 1) gaps.push(Math.abs(xs[index] - xs[index - 1]));
116
+ gaps.sort((a, b) => a - b);
117
+ const median = gaps[Math.floor(gaps.length / 2)];
118
+ if (median < DAY_MS) return "time";
119
+ if (median >= 360 * DAY_MS) return "year";
120
+ if (median >= 27 * DAY_MS) return "month";
121
+ return "day";
122
+ }
123
+ /**
124
+ * The granularity an x axis labels its ticks at: the tooltip and the table
125
+ * keep the full form (`Sep 8, 2026`); an axis has room for `Sep 8`. Duplicated
126
+ * per component rather than shared, so a card's text grammar is readable in
127
+ * one file.
128
+ */
129
+ function anomalyAxisGranularity(granularity) {
130
+ switch (granularity) {
131
+ case "day": return "day-short";
132
+ case "month": return "month-short";
133
+ case "datetime": return "time";
134
+ default: return granularity;
135
+ }
136
+ }
137
+ function normalizeX(x, xType) {
138
+ if (xType === "temporal") {
139
+ const time = x instanceof Date ? x.getTime() : new Date(x).getTime();
140
+ return Number.isNaN(time) ? null : time;
141
+ }
142
+ if (xType === "quantitative") return isFinite(x) ? x : null;
143
+ return String(x);
144
+ }
145
+ /**
146
+ * How an excursion reads once the product says which way is good.
147
+ *
148
+ * `none` — the default — is the honest answer for a detector: leaving the
149
+ * expected range is UNEXPECTED, which is not the same as bad. A product that
150
+ * knows (revenue up, latency down) gets the polarity, and with it the
151
+ * diverging hues; a product that does not gets the attention hue for both
152
+ * directions, and the direction is read from the mark's shape and its word.
153
+ */
154
+ function anomalyPolarityFor(direction, goodDirection) {
155
+ if (direction == null || goodDirection === "none") return "neutral";
156
+ return direction === "high" === (goodDirection === "up") ? "positive" : "negative";
157
+ }
158
+ /**
159
+ * The colour one excursion paints with — colour's ONE job here is polarity.
160
+ * A neutral excursion takes the attention hue rather than a diverging end,
161
+ * because saying "good" or "bad" about a move nobody has assigned a direction
162
+ * to is exactly the lie rule 6 exists to prevent. This is one of the two
163
+ * places the reserved status hues belong on a mark: the encoded value IS
164
+ * "this needs a look".
165
+ */
166
+ function anomalyColorFor(polarity, tokens) {
167
+ if (polarity === "positive") return tokens.divergingPositive;
168
+ if (polarity === "negative") return tokens.divergingNegative;
169
+ return tokens.status.degraded;
170
+ }
171
+ /** Direction as SHAPE, so a flag never depends on hue alone (WCAG 1.4.1). */
172
+ function anomalyShapeFor(direction) {
173
+ if (direction === "high") return "triangle-up";
174
+ if (direction === "low") return "triangle-down";
175
+ return "circle";
176
+ }
177
+ /**
178
+ * Normalises the readings onto one period domain, resolves each period's
179
+ * expected range (its own bounds, else the flat `bounds`), decides the flag
180
+ * (the detector's `anomaly` first, the bounds second), measures how far past
181
+ * the bound each excursion went, and groups consecutive excursions into
182
+ * episodes.
183
+ *
184
+ * The plot's extent spans the readings AND the band, so a cropped `auto`
185
+ * scale can never hide the range the readings are being judged against.
186
+ *
187
+ * Returns `null` when there are no readings to speak of.
188
+ */
189
+ function normalizeAnomalies(points, { granularity, bounds, goodDirection, tokens }) {
190
+ if (!points) return null;
191
+ const xType = inferXType(points);
192
+ const flatLower = isFinite(bounds?.lower) ? bounds.lower : null;
193
+ const flatUpper = isFinite(bounds?.upper) ? bounds.upper : null;
194
+ const byPeriod = /* @__PURE__ */ new Map();
195
+ const order = [];
196
+ for (const point of points) {
197
+ const x = normalizeX(point.x, xType);
198
+ if (x == null) continue;
199
+ const id = String(x);
200
+ if (!byPeriod.has(id)) order.push(id);
201
+ byPeriod.set(id, {
202
+ x,
203
+ point
204
+ });
205
+ }
206
+ if (xType !== "ordinal") order.sort((a, b) => byPeriod.get(a).x - byPeriod.get(b).x);
207
+ const periods = order.map((id) => byPeriod.get(id).x);
208
+ const rows = order.map((id) => {
209
+ const { x, point } = byPeriod.get(id);
210
+ const y = isFinite(point.y) ? point.y : null;
211
+ const lo = isFinite(point.lower) ? point.lower : flatLower;
212
+ const hi = isFinite(point.upper) ? point.upper : flatUpper;
213
+ const ex = isFinite(point.expected) ? point.expected : null;
214
+ const sc = isFinite(point.score) ? point.score : null;
215
+ let dir = null;
216
+ let d = null;
217
+ if (y != null) {
218
+ if (hi != null && y > hi) {
219
+ dir = "high";
220
+ d = y - hi;
221
+ } else if (lo != null && y < lo) {
222
+ dir = "low";
223
+ d = y - lo;
224
+ }
225
+ }
226
+ let a = point.anomaly ?? dir != null;
227
+ if (y == null) a = false;
228
+ if (!a) {
229
+ dir = null;
230
+ d = null;
231
+ }
232
+ const polarity = anomalyPolarityFor(dir, goodDirection);
233
+ return {
234
+ x,
235
+ y,
236
+ lo,
237
+ hi,
238
+ ex,
239
+ d,
240
+ a,
241
+ dir,
242
+ c: anomalyColorFor(polarity, tokens),
243
+ sh: anomalyShapeFor(dir),
244
+ e: -1,
245
+ sc
246
+ };
247
+ });
248
+ const episodes = [];
249
+ let run = [];
250
+ const closeRun = () => {
251
+ if (run.length === 0) return;
252
+ const index = episodes.length;
253
+ const members = run.map((at) => rows[at]);
254
+ for (const at of run) rows[at].e = index;
255
+ let peak = null;
256
+ let sawHigh = false;
257
+ let sawLow = false;
258
+ for (const row of members) {
259
+ if (row.dir === "high") sawHigh = true;
260
+ if (row.dir === "low") sawLow = true;
261
+ if (row.d == null || row.y == null || row.dir == null) continue;
262
+ const bound = row.dir === "high" ? row.hi : row.lo;
263
+ if (bound == null) continue;
264
+ if ((bound === 0 ? Math.abs(row.d) : Math.abs(row.d / bound)) > (peak == null ? -1 : peak.bound === 0 ? Math.abs(peak.deviation) : Math.abs(peak.deviation / peak.bound))) peak = {
265
+ x: row.x,
266
+ y: row.y,
267
+ deviation: row.d,
268
+ bound,
269
+ direction: row.dir,
270
+ score: row.sc
271
+ };
272
+ }
273
+ const direction = peak?.direction ?? (sawHigh ? "high" : sawLow ? "low" : null);
274
+ const polarity = anomalyPolarityFor(direction, goodDirection);
275
+ episodes.push({
276
+ key: String(members[0].x),
277
+ index,
278
+ start: members[0].x,
279
+ end: members[members.length - 1].x,
280
+ count: members.length,
281
+ direction,
282
+ mixed: sawHigh && sawLow,
283
+ polarity,
284
+ color: anomalyColorFor(polarity, tokens),
285
+ peak,
286
+ ongoing: run[run.length - 1] === rows.length - 1
287
+ });
288
+ run = [];
289
+ };
290
+ rows.forEach((row, at) => {
291
+ if (row.a) run.push(at);
292
+ else closeRun();
293
+ });
294
+ closeRun();
295
+ const readings = rows.map((row) => row.y).filter((y) => y != null);
296
+ const extents = [
297
+ ...readings,
298
+ ...rows.map((row) => row.lo).filter((value) => value != null),
299
+ ...rows.map((row) => row.hi).filter((value) => value != null),
300
+ ...rows.map((row) => row.ex).filter((value) => value != null)
301
+ ];
302
+ const finiteRows = rows.filter((row) => row.y != null);
303
+ const latest = finiteRows[finiteRows.length - 1] ?? null;
304
+ const current = latest && latest.a && latest.e >= 0 ? episodes[latest.e] : null;
305
+ let worst = null;
306
+ for (const episode of episodes) {
307
+ if (!episode.peak) continue;
308
+ if ((episode.peak.bound === 0 ? Math.abs(episode.peak.deviation) : Math.abs(episode.peak.deviation / episode.peak.bound)) > (worst?.peak == null ? -1 : worst.peak.bound === 0 ? Math.abs(worst.peak.deviation) : Math.abs(worst.peak.deviation / worst.peak.bound))) worst = episode;
309
+ }
310
+ if (!worst && episodes.length > 0) worst = episodes[0];
311
+ return {
312
+ xType,
313
+ granularity: granularity ?? (xType === "temporal" ? inferGranularity(periods) : "day"),
314
+ periods,
315
+ rows,
316
+ count: readings.length,
317
+ min: extents.length > 0 ? Math.min(...extents) : null,
318
+ max: extents.length > 0 ? Math.max(...extents) : null,
319
+ hasBand: rows.some((row) => row.lo != null || row.hi != null),
320
+ hasExpected: rows.some((row) => row.ex != null),
321
+ hasScore: rows.some((row) => row.sc != null),
322
+ judged: rows.filter((row) => row.y != null && (row.lo != null || row.hi != null)).length,
323
+ flagged: rows.filter((row) => row.a).length,
324
+ episodes,
325
+ latest,
326
+ current,
327
+ worst
328
+ };
329
+ }
330
+ /** The periods the crosshair reads — one row each, so the hover lands on the period. */
331
+ function buildAnomalyPeriodRows(summary) {
332
+ return summary.periods.map((x) => ({ x }));
333
+ }
334
+ /**
335
+ * The washed span of the selected episode. A wash in the brush role means
336
+ * SELECTION here, which is exactly what this is: the reader picked an
337
+ * excursion and the plot says which one.
338
+ *
339
+ * Always one row while the window has periods — a selection turns `on`, it
340
+ * does not add a row. An empty dataset would contribute an empty extent to
341
+ * the shared `x` scale (`Infinite extent for field "start"`), and gating the
342
+ * layer on the selection instead would re-embed the view every time the
343
+ * reader picked a different excursion.
344
+ */
345
+ function buildAnomalySpanRows(summary, selectedKey) {
346
+ if (!summary || summary.periods.length === 0) return [];
347
+ const episode = selectedKey == null ? void 0 : summary.episodes.find((entry) => entry.key === selectedKey);
348
+ if (!episode) return [{
349
+ start: summary.periods[0],
350
+ end: summary.periods[0],
351
+ on: false
352
+ }];
353
+ return [{
354
+ start: episode.start,
355
+ end: episode.end,
356
+ on: true
357
+ }];
358
+ }
359
+ /**
360
+ * `breach` when the latest reading is outside its range, `watch` when the
361
+ * window holds excursions but the latest reading is inside, `clear` when
362
+ * nothing left the range. A window with nothing to judge against is `clear`
363
+ * only in the sense that nothing was flagged, and the card says so in its
364
+ * caption rather than in the verdict.
365
+ */
366
+ function deriveAnomalyStanding(summary) {
367
+ if (!summary || summary.episodes.length === 0) return {
368
+ verdict: "clear",
369
+ polarity: "neutral"
370
+ };
371
+ if (summary.current) return {
372
+ verdict: "breach",
373
+ polarity: summary.current.polarity
374
+ };
375
+ return {
376
+ verdict: "watch",
377
+ polarity: "neutral"
378
+ };
379
+ }
380
+ /** The formatters every text surface shares — headline, rows, table, tooltip, descriptions. */
381
+ function createAnomalyFormatters({ format, locale, timeZone, formatValue, xType, granularity, deltaMode = "absolute", percentagePoints = " pp", scoreFormat = "number:1" }) {
382
+ const formatX = (x) => {
383
+ if (xType === "temporal" && typeof x === "number") return formatDate(x, {
384
+ granularity,
385
+ locale,
386
+ timeZone
387
+ });
388
+ if (xType === "quantitative" && typeof x === "number") return formatNumber(x, "number", locale);
389
+ return String(x);
390
+ };
391
+ const value = (input) => isFinite(input) ? formatValue ? formatValue(input) : formatNumber(input, format, locale) : "—";
392
+ const deviationIn = (mode) => (deviation, bound) => {
393
+ if (!isFinite(deviation) || !isFinite(bound)) return null;
394
+ return formatDelta(bound + deviation, bound, {
395
+ mode,
396
+ format,
397
+ locale,
398
+ pointsSuffix: percentagePoints
399
+ });
400
+ };
401
+ return {
402
+ formatValue: value,
403
+ formatDeviation: (deviation, bound, mode = deltaMode) => deviationIn(mode)(deviation, bound)?.text ?? null,
404
+ formatDeviationSecondary: (deviation, bound, mode = deltaMode) => {
405
+ const primary = deviationIn(mode)(deviation, bound);
406
+ if (!primary) return null;
407
+ const other = deviationIn(primary.renderedMode === "percent" ? "absolute" : "percent")(deviation, bound);
408
+ return !other || other.renderedMode === primary.renderedMode ? null : other.text;
409
+ },
410
+ formatRange: (lo, hi) => {
411
+ const hasLo = isFinite(lo);
412
+ const hasHi = isFinite(hi);
413
+ if (hasLo && hasHi) return `${value(lo)} – ${value(hi)}`;
414
+ if (hasHi) return `≤ ${value(hi)}`;
415
+ if (hasLo) return `≥ ${value(lo)}`;
416
+ return "—";
417
+ },
418
+ formatScore: (score) => isFinite(score) ? formatNumber(score, scoreFormat, locale) : null,
419
+ formatX,
420
+ formatXRange: (start, end) => {
421
+ if (String(start) === String(end)) return formatX(start);
422
+ return xType === "temporal" && typeof start === "number" && typeof end === "number" ? formatDateRange(start, end, {
423
+ granularity,
424
+ locale,
425
+ timeZone
426
+ }) : `${formatX(start)} – ${formatX(end)}`;
427
+ }
428
+ };
429
+ }
430
+ const stripSign = (text) => text.replace(/^[+\-−]/, "");
431
+ /** The label/value rows one excursion explains itself with — shared by the flag tooltip, the headline tooltip and the episode rows. */
432
+ function buildAnomalyExplanation({ row, formatters, labels }) {
433
+ const bound = row.dir === "high" ? row.hi : row.dir === "low" ? row.lo : null;
434
+ const deviation = formatters.formatDeviation(row.d, bound);
435
+ return {
436
+ periodText: formatters.formatX(row.x),
437
+ valueText: formatters.formatValue(row.y),
438
+ expectedText: row.ex != null ? formatters.formatValue(row.ex) : null,
439
+ rangeText: formatters.formatRange(row.lo, row.hi),
440
+ deviationText: deviation ?? labels.inRange,
441
+ secondaryText: formatters.formatDeviationSecondary(row.d, bound),
442
+ scoreText: formatters.formatScore(row.sc),
443
+ direction: row.dir
444
+ };
445
+ }
446
+ /**
447
+ * The generated insight — what the card shows beside the AI mark when the
448
+ * consumer passes no `insight`. Values come back as `{ value }` segments so
449
+ * the card can set them in bold; the words come from `labels.insight`.
450
+ */
451
+ function buildAnomalyInsight({ name, summary, standing, formatters, labels }) {
452
+ const first = summary.periods[0];
453
+ const last = summary.periods[summary.periods.length - 1];
454
+ const peak = summary.worst?.peak ?? null;
455
+ return labels.insight({
456
+ name,
457
+ verdict: standing.verdict,
458
+ count: summary.episodes.length,
459
+ range: first != null && last != null ? formatters.formatXRange(first, last) : "—",
460
+ worstAt: peak ? formatters.formatX(peak.x) : null,
461
+ worstValue: peak ? formatters.formatValue(peak.y) : null,
462
+ worstDeviation: peak ? stripSign(formatters.formatDeviation(peak.deviation, peak.bound) ?? "") || null : null,
463
+ worstBound: peak ? formatters.formatValue(peak.bound) : null,
464
+ direction: peak ? peak.direction === "high" ? labels.high : labels.low : null,
465
+ ongoing: standing.verdict === "breach"
466
+ });
467
+ }
468
+ /**
469
+ * The hover chip's contents for one period — pure, so the reading is
470
+ * unit-tested rather than only reachable through a pointer event (Vega's own
471
+ * hit-testing needs a visible document, which a hidden frame does not give).
472
+ * Returns `null` for a period the card does not have.
473
+ */
474
+ function buildAnomalyTooltip({ summary, x, formatters, labels }) {
475
+ if (x == null || !summary) return null;
476
+ const row = summary.rows.find((entry) => String(entry.x) === String(x));
477
+ if (!row) return null;
478
+ return {
479
+ explanation: buildAnomalyExplanation({
480
+ row,
481
+ formatters,
482
+ labels
483
+ }),
484
+ flagged: row.a,
485
+ directionText: row.dir === "high" ? labels.high : row.dir === "low" ? labels.low : null,
486
+ color: row.c,
487
+ shape: row.sh
488
+ };
489
+ }
490
+ /**
491
+ * The card's text equivalent (Amy Cesal's alt-text formula: what it is, what
492
+ * it shows, why it is here, when it was true) — the figure's accessible
493
+ * description, and the summary a screen reader gets before the parts. Every
494
+ * excursion is named with its span, so nothing lives only in the marks.
495
+ */
496
+ function buildAnomalyDescription({ name, state, summary, standing, formatters, asOfText, labels }) {
497
+ if (state === "loading") return `${labels.loading(name)}.`;
498
+ if (state === "error") return `${name}: ${labels.error}.`;
499
+ if (state === "empty") return `${name}: ${labels.noData}.`;
500
+ if (state === "no-match") return `${name}: ${labels.noMatch}.`;
501
+ const sentences = [];
502
+ const first = summary?.periods[0];
503
+ const last = summary?.periods[summary.periods.length - 1];
504
+ const range = summary && first != null && last != null ? formatters.formatXRange(first, last) : "—";
505
+ sentences.push(`${name}: ${labels.verdicts[standing.verdict]}.`);
506
+ if (summary) {
507
+ sentences.push(labels.verdictSentence({
508
+ verdict: standing.verdict,
509
+ name,
510
+ count: summary.episodes.length,
511
+ range,
512
+ latest: formatters.formatValue(summary.latest?.y)
513
+ }));
514
+ if (summary.latest && (summary.latest.lo != null || summary.latest.hi != null)) sentences.push(`${labels.expectedRange(formatters.formatRange(summary.latest.lo, summary.latest.hi))}.`);
515
+ const peak = summary.worst?.peak;
516
+ if (peak) {
517
+ sentences.push(labels.worstSentence({
518
+ period: formatters.formatX(peak.x),
519
+ value: formatters.formatValue(peak.y),
520
+ deviation: stripSign(formatters.formatDeviation(peak.deviation, peak.bound) ?? ""),
521
+ bound: formatters.formatValue(peak.bound),
522
+ direction: peak.direction === "high" ? labels.high : labels.low
523
+ }));
524
+ const score = formatters.formatScore(peak.score);
525
+ if (score) sentences.push(labels.scoreSentence(score));
526
+ }
527
+ for (const episode of summary.episodes) {
528
+ const span = formatters.formatXRange(episode.start, episode.end);
529
+ const direction = episode.mixed ? labels.mixed : episode.direction === "high" ? labels.high : episode.direction === "low" ? labels.low : labels.flagKey;
530
+ sentences.push(`${span}: ${direction}, ${labels.periodCount(episode.count)}${episode.ongoing ? `, ${labels.ongoing}` : ""}.`);
531
+ }
532
+ if (!summary.hasBand) sentences.push(`${labels.unjudged}.`);
533
+ }
534
+ if (asOfText) sentences.push(`${labels.asOf} ${asOfText}.`);
535
+ return sentences.join(" ");
536
+ }
537
+ /** The plot's own `description` — the `aria-label` Vega stamps on the `graphics-document`. */
538
+ function buildAnomalyChartDescription({ name, mark, summary, formatters, labels }) {
539
+ const first = summary.periods[0];
540
+ const last = summary.periods[summary.periods.length - 1];
541
+ return labels.chartDescription({
542
+ mark: labels.markNames[mark],
543
+ name,
544
+ periodCount: summary.periods.length,
545
+ range: first != null && last != null ? formatters.formatXRange(first, last) : "—",
546
+ min: formatters.formatValue(summary.min),
547
+ max: formatters.formatValue(summary.max),
548
+ flagged: summary.flagged,
549
+ band: summary.hasBand ? labels.bandKey : null
550
+ });
551
+ }
552
+ //#endregion
553
+ export { DEFAULT_ANOMALY_LABELS, anomalyAxisGranularity, anomalyColorFor, anomalyPolarityFor, anomalyShapeFor, buildAnomalyChartDescription, buildAnomalyDescription, buildAnomalyExplanation, buildAnomalyInsight, buildAnomalyPeriodRows, buildAnomalySpanRows, buildAnomalyTooltip, createAnomalyFormatters, deriveAnomalyStanding, deriveAnomalyState, normalizeAnomalies, resolveAnomalyLabels };
@@ -0,0 +1,4 @@
1
+ import { AnomalyBounds, AnomalyCardLabels, AnomalyCardSize, AnomalyCardState, AnomalyCardStatus, AnomalyDeltaMode, AnomalyDirection, AnomalyEpisode, AnomalyExplanation, AnomalyFlagShape, AnomalyGoodDirection, AnomalyHeadline, AnomalyInsightSegment, AnomalyMark, AnomalyPeak, AnomalyPeriodRow, AnomalyPoint, AnomalyPolarity, AnomalyRow, AnomalyScale, AnomalySpanRow, AnomalyStanding, AnomalySummary, AnomalyTextFormatters, AnomalyTooltipModel, AnomalyVerdict, AnomalyXType, BuildAnomalyDescriptionInput, DEFAULT_ANOMALY_LABELS, DeriveAnomalyStateInput, NormalizeAnomalyOptions, anomalyAxisGranularity, anomalyColorFor, anomalyPolarityFor, anomalyShapeFor, buildAnomalyChartDescription, buildAnomalyDescription, buildAnomalyExplanation, buildAnomalyInsight, buildAnomalyPeriodRows, buildAnomalySpanRows, buildAnomalyTooltip, createAnomalyFormatters, deriveAnomalyStanding, deriveAnomalyState, normalizeAnomalies, resolveAnomalyLabels } from "./AnomalyCard.model.js";
2
+ import { AnomalyCard, AnomalyCardProps } from "./AnomalyCard.js";
3
+ import { ANOMALY_HOVER_PARAM, ANOMALY_PERIODS_DATASET, ANOMALY_SPAN_DATASET, ANOMALY_TABLE_DATASET, ANOMALY_X_TICKS_PARAM, AnomalyPlotSpecOptions, buildAnomalyPlotSpec } from "./AnomalyCard.chart.js";
4
+ export { ANOMALY_HOVER_PARAM, ANOMALY_PERIODS_DATASET, ANOMALY_SPAN_DATASET, ANOMALY_TABLE_DATASET, ANOMALY_X_TICKS_PARAM, type AnomalyBounds, AnomalyCard, type AnomalyCardLabels, type AnomalyCardProps, type AnomalyCardSize, type AnomalyCardState, type AnomalyCardStatus, type AnomalyDeltaMode, type AnomalyDirection, type AnomalyEpisode, type AnomalyExplanation, type AnomalyFlagShape, type AnomalyGoodDirection, type AnomalyHeadline, type AnomalyInsightSegment, type AnomalyMark, type AnomalyPeak, type AnomalyPeriodRow, type AnomalyPlotSpecOptions, type AnomalyPoint, type AnomalyPolarity, type AnomalyRow, type AnomalyScale, type AnomalySpanRow, type AnomalyStanding, type AnomalySummary, type AnomalyTextFormatters, type AnomalyTooltipModel, type AnomalyVerdict, type AnomalyXType, type BuildAnomalyDescriptionInput, DEFAULT_ANOMALY_LABELS, type DeriveAnomalyStateInput, type NormalizeAnomalyOptions, anomalyAxisGranularity, anomalyColorFor, anomalyPolarityFor, anomalyShapeFor, buildAnomalyChartDescription, buildAnomalyDescription, buildAnomalyExplanation, buildAnomalyInsight, buildAnomalyPeriodRows, buildAnomalyPlotSpec, buildAnomalySpanRows, buildAnomalyTooltip, createAnomalyFormatters, deriveAnomalyStanding, deriveAnomalyState, normalizeAnomalies, resolveAnomalyLabels };
@@ -0,0 +1,4 @@
1
+ import { ANOMALY_HOVER_PARAM, ANOMALY_PERIODS_DATASET, ANOMALY_SPAN_DATASET, ANOMALY_TABLE_DATASET, ANOMALY_X_TICKS_PARAM, buildAnomalyPlotSpec } from "./AnomalyCard.chart.js";
2
+ import { DEFAULT_ANOMALY_LABELS, anomalyAxisGranularity, anomalyColorFor, anomalyPolarityFor, anomalyShapeFor, buildAnomalyChartDescription, buildAnomalyDescription, buildAnomalyExplanation, buildAnomalyInsight, buildAnomalyPeriodRows, buildAnomalySpanRows, buildAnomalyTooltip, createAnomalyFormatters, deriveAnomalyStanding, deriveAnomalyState, normalizeAnomalies, resolveAnomalyLabels } from "./AnomalyCard.model.js";
3
+ import { AnomalyCard } from "./AnomalyCard.js";
4
+ export { ANOMALY_HOVER_PARAM, ANOMALY_PERIODS_DATASET, ANOMALY_SPAN_DATASET, ANOMALY_TABLE_DATASET, ANOMALY_X_TICKS_PARAM, AnomalyCard, DEFAULT_ANOMALY_LABELS, anomalyAxisGranularity, anomalyColorFor, anomalyPolarityFor, anomalyShapeFor, buildAnomalyChartDescription, buildAnomalyDescription, buildAnomalyExplanation, buildAnomalyInsight, buildAnomalyPeriodRows, buildAnomalyPlotSpec, buildAnomalySpanRows, buildAnomalyTooltip, createAnomalyFormatters, deriveAnomalyStanding, deriveAnomalyState, normalizeAnomalies, resolveAnomalyLabels };
@@ -0,0 +1,72 @@
1
+ import { ChartTokens } from "../../lib/vega/tokens.js";
2
+ import { ChartAggregate, ChartExtent, ChartFieldType, ChartType } from "./ChartCard.model.js";
3
+ import { TopLevelSpec } from "vega-lite";
4
+ //#region src/components/ChartCard/ChartCard.chart.d.ts
5
+ /** Long-form rows, one per reading — `ChartRow` from the model. */
6
+ declare const CHART_TABLE_DATASET = "table";
7
+ /** Distinct axis categories `{ x }` — the radar's spokes and labels. */
8
+ declare const CHART_AXES_DATASET = "axes";
9
+ /** The hover selection: nearest point, so a pointer anywhere near a mark reads it. */
10
+ declare const CHART_HOVER_PARAM = "hover";
11
+ /** Responsive x tick values, recomputed by Vega when the domain or the width changes. */
12
+ declare const CHART_X_TICKS_PARAM = "chart_x_ticks";
13
+ /** Series labels, pushed as a signal so a violin's category axis needs no spec rebuild. */
14
+ declare const CHART_SERIES_NAMES_PARAM = "chart_series_names";
15
+ interface ChartSpecOptions {
16
+ type: ChartType;
17
+ /** Resolved field types for the two positions. */
18
+ xType: ChartFieldType;
19
+ yType: ChartFieldType;
20
+ /** Plot height in pixels, axes included. */
21
+ height: number;
22
+ /** Draw the axes. `false` strips the plot to its marks — the table twin still carries every value. */
23
+ axes: boolean;
24
+ /** Draw grid lines behind the marks. */
25
+ grid: boolean;
26
+ /** Draw axis titles at all. `false` (the card's default) suppresses even the derived ones a counting or distribution form would otherwise name itself. */
27
+ axisTitles: boolean;
28
+ /** Axis titles, when `axisTitles` is on. `null` falls back to the form's own derived name. */
29
+ xTitle?: string | null;
30
+ yTitle?: string | null;
31
+ /** Plain-text names used in tooltips. */
32
+ valueTitle: string;
33
+ seriesTitle: string;
34
+ sizeTitle?: string;
35
+ /** `arcNumber` grammar for the measure axis and the tooltip (`compact@en-US`). */
36
+ valueFormat: string;
37
+ /** `arcTime` grammar for temporal values in tooltips (`day@en-US@Asia/Bangkok`). */
38
+ timeFormat: string;
39
+ /** `arcTime` grammar for tick labels — the axis-length form (`day-short@en-US`). */
40
+ axisTimeFormat: string;
41
+ /** Aggregation applied to the measure. Only the forms that let Vega aggregate read it. */
42
+ aggregate?: ChartAggregate;
43
+ /** Bin count ceiling for the binning forms. */
44
+ maxBins: number;
45
+ /** Whisker or band extent for `boxplot`, `errorbar` and `errorband`. */
46
+ extent?: ChartExtent;
47
+ /** Wire the hover selection and the tooltip channel. */
48
+ tooltip: boolean;
49
+ /**
50
+ * The consumer named an `x` field. Structural, not data: a strip plot with
51
+ * no grouping field draws one lane rather than a category axis with a single
52
+ * undefined band on it.
53
+ */
54
+ hasX: boolean;
55
+ /** Hole size of a `donut`, as a fraction of the outer radius. */
56
+ innerRadius?: number;
57
+ /** Number of ticks on the measure axis — the caller scales it to the plot's height. */
58
+ yTicks: number;
59
+ locale?: string;
60
+ timeZone?: string;
61
+ }
62
+ /**
63
+ * Builds the plot for one form. Every spec is a single or layered view — never
64
+ * a facet, concat or repeat — so `autosize: "fit"` applies and the width and
65
+ * height can come from the View API rather than from a rebuild.
66
+ *
67
+ * The Vega legend is always `null`: the legend is real Arc controls, because
68
+ * Vega offers no keyboard navigation of its own marks.
69
+ */
70
+ declare function buildChartSpec(options: ChartSpecOptions, tokens: ChartTokens): TopLevelSpec;
71
+ //#endregion
72
+ export { CHART_AXES_DATASET, CHART_HOVER_PARAM, CHART_SERIES_NAMES_PARAM, CHART_TABLE_DATASET, CHART_X_TICKS_PARAM, ChartSpecOptions, buildChartSpec };