@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,875 @@
1
+ import { formatDate, formatDateRange, formatDelta, formatNumber, parseArcNumberFormat } from "../../lib/vega/formatters.js";
2
+ //#region src/components/HighlightCard/HighlightCard.model.ts
3
+ const DEFAULT_SIGMA_THRESHOLDS = {
4
+ notable: 1,
5
+ unusual: 2,
6
+ extreme: 3
7
+ };
8
+ const DEFAULT_PERCENTILE_THRESHOLDS = {
9
+ notable: 40,
10
+ unusual: 45,
11
+ extreme: 49
12
+ };
13
+ const DEFAULT_HIGHLIGHT_LABELS = {
14
+ severityNames: {
15
+ typical: "Typical",
16
+ notable: "Notable",
17
+ unusual: "Unusual",
18
+ extreme: "Extreme"
19
+ },
20
+ increase: "Up",
21
+ decrease: "Down",
22
+ unchanged: "No change",
23
+ percentagePoints: " pp",
24
+ contextTitle: "In context",
25
+ peersTitle: "How unusual",
26
+ driversTitle: "What drove it",
27
+ noData: "No data",
28
+ noMatch: "No matching data",
29
+ clearFilters: "Clear filters",
30
+ error: "Couldn't load this highlight",
31
+ retry: "Try again",
32
+ unresolved: "The highlighted period has no two readings to compare",
33
+ insufficientPeers: "Not enough comparable periods to say how unusual this is",
34
+ stale: "Stale",
35
+ asOf: "As of",
36
+ tablePeriod: "Period",
37
+ tableValue: "Value",
38
+ tableStep: "Step",
39
+ tableChange: "Change",
40
+ tableShare: "Share",
41
+ tableDriver: "Driver",
42
+ tableWindow: "Window",
43
+ windowStart: "Start",
44
+ windowEnd: "End",
45
+ windowChange: "Change",
46
+ windowChangePercent: "Change %",
47
+ windowPeriod: "Period",
48
+ windowPeak: "Peak",
49
+ windowTrough: "Trough",
50
+ windowSteepest: "Steepest step",
51
+ highlighted: "highlighted",
52
+ peerMean: "Typical change",
53
+ peerSd: "Standard deviation",
54
+ peerTypical: "Typical range",
55
+ peerRank: "Rank",
56
+ peerPercentile: "Percentile",
57
+ peerZ: "Distance from typical",
58
+ peerThis: "This period",
59
+ peerCount: "Compared periods",
60
+ driverTotal: "Total change",
61
+ driverExplained: "Explained",
62
+ driverResidual: "Unexplained",
63
+ driverOther: "Other",
64
+ driverContribution: "Contribution",
65
+ insightMark: "Generated insight",
66
+ about: (name) => `About ${name}`,
67
+ loading: (name) => `Loading ${name}`,
68
+ showTable: (name) => `Show the data behind ${name}`,
69
+ hideTable: (name) => `Hide the data behind ${name}`,
70
+ windowDetails: (name) => `Details of the ${name} highlight`,
71
+ driversGroup: (name) => `What drove the ${name} highlight`,
72
+ selectDriver: (label) => `Drill into ${label}`,
73
+ driverSelected: ({ label, contribution, share }) => `${label}: ${contribution}${share ? `, ${share} of the change` : ""}.`,
74
+ tableCaption: (name) => `Data behind the ${name} highlight`,
75
+ peersCaption: ({ count, readings, mode }) => `Compared with ${count} ${mode === "rolling" ? "overlapping" : "non-overlapping"} ${readings}-reading period${count === 1 ? "" : "s"} in the same series.`,
76
+ driversCaption: ({ total, explained, residual, reconciles }) => reconciles ? `Contributions sum to ${explained} of the ${total} change.` : `Contributions sum to ${explained} of the ${total} change; ${residual} is unexplained.`,
77
+ foldedCaption: (folded, label) => `${folded} smaller contributors folded into ${label}.`,
78
+ windowLine: ({ range, label }) => label ? `${label} · ${range}` : range,
79
+ windowExplanation: ({ periodText, startText, endText, changeText, secondaryText, renderedMode, direction, peakText, troughText, steepestText }) => {
80
+ const rows = [
81
+ ["Period", periodText],
82
+ ["Start", startText],
83
+ ["End", endText]
84
+ ];
85
+ rows.push([direction === "up" ? "Increase" : direction === "down" ? "Decrease" : "No change", changeText]);
86
+ if (secondaryText) rows.push([renderedMode === "percent" ? "Change" : "Change %", secondaryText]);
87
+ if (peakText) rows.push(["Peak", peakText]);
88
+ if (troughText) rows.push(["Trough", troughText]);
89
+ if (steepestText) rows.push(["Steepest step", steepestText]);
90
+ return rows;
91
+ },
92
+ peerExplanation: (stats) => {
93
+ const rows = [
94
+ ["This period", stats.changeText],
95
+ ["Rank", stats.rankText],
96
+ ["Typical range", stats.typicalText]
97
+ ];
98
+ if (stats.baseline === "sigma") {
99
+ rows.push(["Typical change", stats.meanText]);
100
+ if (stats.z != null) rows.push(["Distance from typical", stats.zText]);
101
+ } else rows.push(["Percentile", stats.percentileText]);
102
+ rows.push(["Compared periods", String(stats.count)]);
103
+ return rows;
104
+ },
105
+ peerVerdict: ({ severity, rank, total, typical, change, inside }) => {
106
+ const place = rank === "1st" ? "the largest" : `the ${rank} largest`;
107
+ const scale = `${total} period${total === 1 ? "" : "s"} this length`;
108
+ if (severity === "typical" || inside) return `${change} is ${place} of ${scale} — inside the typical range of ${typical}.`;
109
+ return `${change} is ${place} of ${scale}, outside the typical range of ${typical}.`;
110
+ },
111
+ contextDescription: ({ mark, name, count, first, last, min, max, range, label, direction, change }) => `${mark} chart of ${name}: ${count} readings from ${first} to ${last}, ranging ${min} to ${max}. The highlighted period ${label ? `${label} (${range})` : range} is painted in the polarity hue and ${direction === "flat" ? "did not change" : `moved ${change}`}.`,
112
+ peersDescription: ({ name, count, readings, mode, change, rank, typical, severity }) => `Strip plot of the change in ${name} across ${count} ${mode === "rolling" ? "overlapping" : "non-overlapping"} ${readings}-reading periods. The highlighted period changed ${change}, the ${rank} largest, against a typical range of ${typical} — ${severity}.`,
113
+ driversDescription: ({ name, count, total, top, topContribution, counter, counterContribution, residual }) => `Bar chart of ${count} contributor${count === 1 ? "" : "s"} to the ${total} change in ${name}.` + (top && topContribution ? ` Largest: ${top} at ${topContribution}.` : "") + (counter && counterContribution ? ` Largest against it: ${counter} at ${counterContribution}.` : "") + (residual ? ` ${residual} is unexplained.` : ""),
114
+ cardDescription: (input) => {
115
+ const { name, state, range, label, direction, change, secondary, startText, endText, severity, rank, count, driverCount, topDriver, topContribution, asOf } = input;
116
+ if (state === "loading") return `Loading the ${name} highlight.`;
117
+ if (state === "error") return `The ${name} highlight could not be loaded.`;
118
+ if (state === "empty") return `No data for the ${name} highlight.`;
119
+ if (state === "no-match") return `No ${name} readings match the current filters.`;
120
+ let text = `Highlight analysis of ${name} over ${label ? `${label} (${range})` : range}: ${direction === "up" ? "rose" : direction === "down" ? "fell" : "held flat"}${change ? ` ${change}` : ""}${secondary ? ` (${secondary})` : ""}, from ${startText} to ${endText}.`;
121
+ if (severity && rank && count != null) text += ` That is the ${rank} of ${count + 1} comparable periods — ${severity}.`;
122
+ if (driverCount > 0 && topDriver && topContribution) text += ` Largest contributor: ${topDriver} at ${topContribution}.`;
123
+ if (asOf) text += ` As of ${asOf}.`;
124
+ return text;
125
+ },
126
+ insight: (input) => {
127
+ const { name, direction, change, secondary, range, label, severity, rank, count, topDriver, topContribution, topShare } = input;
128
+ const period = label ? `${label} (${range})` : range;
129
+ const segments = [`${name} ${direction === "up" ? "rose" : direction === "down" ? "fell" : "held flat"}`];
130
+ if (change) {
131
+ segments.push(" ");
132
+ segments.push({ value: change });
133
+ if (secondary) segments.push(` (${secondary})`);
134
+ }
135
+ segments.push(` over ${period}`);
136
+ if (rank && count != null) {
137
+ segments.push(", the ");
138
+ segments.push({ value: rank });
139
+ segments.push(` of ${count + 1} comparable periods (${severity.toLowerCase()})`);
140
+ }
141
+ if (topDriver && topContribution) {
142
+ segments.push(". ");
143
+ segments.push({ value: topDriver });
144
+ segments.push(" contributed ");
145
+ segments.push({ value: topContribution });
146
+ if (topShare) segments.push(` (${topShare} of the move)`);
147
+ }
148
+ segments.push(".");
149
+ return segments;
150
+ },
151
+ markNames: {
152
+ line: "Line",
153
+ area: "Area",
154
+ bar: "Bar"
155
+ },
156
+ peerModeNames: {
157
+ rolling: "overlapping",
158
+ adjacent: "non-overlapping"
159
+ }
160
+ };
161
+ function resolveHighlightLabels(overrides) {
162
+ return overrides ? {
163
+ ...DEFAULT_HIGHLIGHT_LABELS,
164
+ ...overrides
165
+ } : DEFAULT_HIGHLIGHT_LABELS;
166
+ }
167
+ /**
168
+ * The state the panel renders, from the transport's status and the data.
169
+ * `loading` with data already present is a refresh: the previous render stays
170
+ * under the veil rather than collapsing to a skeleton.
171
+ */
172
+ function deriveHighlightState({ status, hasData, filtered }) {
173
+ if (status === "error") return "error";
174
+ if (status === "loading") return hasData ? "refreshing" : "loading";
175
+ if (!hasData) return filtered ? "no-match" : "empty";
176
+ return "ready";
177
+ }
178
+ const isFinite = (value) => typeof value === "number" && Number.isFinite(value);
179
+ const DAY_MS = 864e5;
180
+ const ISO_DATE = /^\d{4}-\d{2}(-\d{2})?([T ].*)?$/;
181
+ function inferXType(points) {
182
+ const sample = points.find((point) => point.x != null);
183
+ if (!sample) return "ordinal";
184
+ const { x } = sample;
185
+ if (x instanceof Date) return "temporal";
186
+ if (typeof x === "number") return "quantitative";
187
+ return ISO_DATE.test(x) ? "temporal" : "ordinal";
188
+ }
189
+ function inferGranularity(rows) {
190
+ const xs = rows.map((row) => row.x).filter((x) => typeof x === "number");
191
+ if (xs.length < 2) return "day";
192
+ const gaps = [];
193
+ for (let index = 1; index < xs.length; index += 1) gaps.push(Math.abs(xs[index] - xs[index - 1]));
194
+ gaps.sort((a, b) => a - b);
195
+ const median = gaps[Math.floor(gaps.length / 2)];
196
+ if (median < DAY_MS) return "time";
197
+ if (median >= 360 * DAY_MS) return "year";
198
+ if (median >= 27 * DAY_MS) return "month";
199
+ return "day";
200
+ }
201
+ /**
202
+ * Normalises `series` for Vega and the tables: temporal `x` becomes epoch
203
+ * milliseconds (a bare number is quantitative — pass a `Date` or an ISO string
204
+ * for time), invalid dates are dropped, `null` readings stay as gaps, and the
205
+ * range is summarised for the text equivalents. Rows are sorted along the axis
206
+ * for a temporal or quantitative series, so a window can be snapped and peer
207
+ * windows can be walked; an ordinal series keeps the consumer's order, which
208
+ * IS its axis. Returns `null` without a series at all.
209
+ */
210
+ function normalizeHighlightSeries(points, granularity) {
211
+ if (!points) return null;
212
+ const xType = inferXType(points);
213
+ const rows = [];
214
+ for (const point of points) {
215
+ const y = isFinite(point.y) ? point.y : null;
216
+ if (xType === "temporal") {
217
+ const time = point.x instanceof Date ? point.x.getTime() : new Date(point.x).getTime();
218
+ if (Number.isNaN(time)) continue;
219
+ rows.push({
220
+ x: time,
221
+ y
222
+ });
223
+ } else if (xType === "quantitative") {
224
+ if (!isFinite(point.x)) continue;
225
+ rows.push({
226
+ x: point.x,
227
+ y
228
+ });
229
+ } else rows.push({
230
+ x: String(point.x),
231
+ y
232
+ });
233
+ }
234
+ if (xType !== "ordinal") rows.sort((a, b) => a.x - b.x);
235
+ const valid = rows.filter((row) => row.y != null);
236
+ const values = valid.map((row) => row.y);
237
+ return {
238
+ rows,
239
+ xType,
240
+ count: valid.length,
241
+ first: rows[0] ?? null,
242
+ last: rows[rows.length - 1] ?? null,
243
+ latest: valid.length > 0 ? valid[valid.length - 1] : null,
244
+ min: values.length > 0 ? Math.min(...values) : null,
245
+ max: values.length > 0 ? Math.max(...values) : null,
246
+ granularity: granularity ?? (xType === "temporal" ? inferGranularity(rows) : "day")
247
+ };
248
+ }
249
+ /** The formatters every text surface shares — headline, badges, strip, bars, tables, tooltips, descriptions. */
250
+ function createHighlightFormatters({ format, locale, timeZone, formatValue, xType, granularity, deltaMode, percentagePoints }) {
251
+ const signed = { signDisplay: "exceptZero" };
252
+ const spec = parseArcNumberFormat(format);
253
+ const formatX = (x) => {
254
+ if (xType === "temporal" && typeof x === "number") return formatDate(x, {
255
+ granularity,
256
+ locale,
257
+ timeZone
258
+ });
259
+ if (xType === "quantitative" && typeof x === "number") return formatNumber(x, "number", locale);
260
+ return String(x);
261
+ };
262
+ const formatAmountChange = (delta) => spec.kind === "percent" ? `${formatNumber(delta * 100, `number:${spec.digits ?? 1}`, locale, signed)}${percentagePoints}` : formatNumber(delta, format, locale, signed);
263
+ return {
264
+ formatY: (value) => isFinite(value) ? formatValue ? formatValue(value) : formatNumber(value, format, locale) : "—",
265
+ formatX,
266
+ formatXRange: (start, end) => xType === "temporal" && typeof start === "number" && typeof end === "number" ? formatDateRange(start, end, {
267
+ granularity,
268
+ locale,
269
+ timeZone
270
+ }) : `${formatX(start)} – ${formatX(end)}`,
271
+ formatAmountChange,
272
+ formatPeerChange: (change) => deltaMode === "percent" ? formatNumber(change, "percent:1", locale, signed) : formatAmountChange(change),
273
+ formatShare: (share) => share == null ? "—" : formatNumber(share, "percent:0", locale)
274
+ };
275
+ }
276
+ /** The change between two readings, with polarity resolved through `goodDirection`. */
277
+ function deriveHighlightDelta(value, previousValue, { deltaMode, goodDirection, format, locale, percentagePoints }) {
278
+ if (!isFinite(value) || !isFinite(previousValue)) return null;
279
+ const options = {
280
+ format,
281
+ locale,
282
+ pointsSuffix: percentagePoints
283
+ };
284
+ const delta = formatDelta(value, previousValue, {
285
+ ...options,
286
+ mode: deltaMode
287
+ });
288
+ const other = formatDelta(value, previousValue, {
289
+ ...options,
290
+ mode: delta.renderedMode === "percent" ? "absolute" : "percent"
291
+ });
292
+ let polarity = "neutral";
293
+ if (delta.direction !== "flat" && goodDirection !== "none") polarity = delta.direction === "up" === (goodDirection === "up") ? "positive" : "negative";
294
+ return {
295
+ direction: delta.direction,
296
+ polarity,
297
+ text: delta.text,
298
+ renderedMode: delta.renderedMode,
299
+ secondaryText: other.renderedMode === delta.renderedMode ? null : other.text
300
+ };
301
+ }
302
+ const stripSign = (text) => text.replace(/^[+\-−]/, "");
303
+ function normalizeBound(value, xType) {
304
+ if (xType === "temporal") {
305
+ const time = value instanceof Date ? value.getTime() : new Date(value).getTime();
306
+ return Number.isNaN(time) ? null : time;
307
+ }
308
+ if (xType === "quantitative") {
309
+ const numeric = typeof value === "number" ? value : Number(value);
310
+ return Number.isFinite(numeric) ? numeric : null;
311
+ }
312
+ return String(value);
313
+ }
314
+ /**
315
+ * Snaps the window onto the series and measures it. Each bound moves inward to
316
+ * the nearest finite reading (a temporal or quantitative bound need not hit a
317
+ * reading exactly; an ordinal bound is a category label), the bounds may come
318
+ * in either order, and the result is `null` when the window does not hold two
319
+ * distinct finite readings — the panel then says so and stamps
320
+ * `data-window="unresolved"` rather than inventing a change.
321
+ *
322
+ * Beyond the change it records the window's ANATOMY, which is what makes this
323
+ * a highlight-analysis panel rather than a tile: every reading inside it, the
324
+ * step between each consecutive pair, the peak, the trough and the steepest
325
+ * single step.
326
+ */
327
+ function resolveHighlightWindow(summary, window, { deltaMode, goodDirection, format, locale, percentagePoints, formatters }) {
328
+ if (!summary || !window) return null;
329
+ const { rows, xType } = summary;
330
+ const lo = normalizeBound(window.start, xType);
331
+ const hi = normalizeBound(window.end, xType);
332
+ if (lo == null || hi == null) return null;
333
+ let startIndex = -1;
334
+ let endIndex = -1;
335
+ if (xType === "ordinal") {
336
+ const first = rows.findIndex((row) => row.x === lo);
337
+ const second = rows.findIndex((row) => row.x === hi);
338
+ if (first < 0 || second < 0) return null;
339
+ const [from, to] = first <= second ? [first, second] : [second, first];
340
+ for (let index = from; index <= to; index += 1) if (rows[index].y != null) {
341
+ startIndex = index;
342
+ break;
343
+ }
344
+ for (let index = to; index >= from; index -= 1) if (rows[index].y != null) {
345
+ endIndex = index;
346
+ break;
347
+ }
348
+ } else {
349
+ const from = Math.min(lo, hi);
350
+ const to = Math.max(lo, hi);
351
+ for (let index = 0; index < rows.length; index += 1) {
352
+ const row = rows[index];
353
+ const at = row.x;
354
+ if (at < from || at > to || row.y == null) continue;
355
+ if (startIndex < 0) startIndex = index;
356
+ endIndex = index;
357
+ }
358
+ }
359
+ if (startIndex < 0 || endIndex < 0 || startIndex === endIndex) return null;
360
+ const startRow = rows[startIndex];
361
+ const endRow = rows[endIndex];
362
+ const start = {
363
+ x: startRow.x,
364
+ y: startRow.y,
365
+ index: startIndex
366
+ };
367
+ const end = {
368
+ x: endRow.x,
369
+ y: endRow.y,
370
+ index: endIndex
371
+ };
372
+ const delta = deriveHighlightDelta(end.y, start.y, {
373
+ deltaMode,
374
+ goodDirection,
375
+ format,
376
+ locale,
377
+ percentagePoints
378
+ });
379
+ if (!delta) return null;
380
+ const absoluteDelta = formatDelta(end.y, start.y, {
381
+ mode: "absolute",
382
+ format,
383
+ locale,
384
+ pointsSuffix: percentagePoints
385
+ });
386
+ const percentDelta = formatDelta(end.y, start.y, {
387
+ mode: "percent",
388
+ format,
389
+ locale,
390
+ pointsSuffix: percentagePoints
391
+ });
392
+ const readings = [];
393
+ for (let index = startIndex; index <= endIndex; index += 1) {
394
+ const row = rows[index];
395
+ if (row.y == null) continue;
396
+ readings.push({
397
+ x: row.x,
398
+ y: row.y,
399
+ index
400
+ });
401
+ }
402
+ const legs = [];
403
+ for (let at = 1; at < readings.length; at += 1) {
404
+ const from = readings[at - 1];
405
+ const to = readings[at];
406
+ const step = deriveHighlightDelta(to.y, from.y, {
407
+ deltaMode,
408
+ goodDirection,
409
+ format,
410
+ locale,
411
+ percentagePoints
412
+ });
413
+ if (!step) continue;
414
+ legs.push({
415
+ from,
416
+ to,
417
+ change: to.y - from.y,
418
+ changeText: step.text,
419
+ rangeText: formatters.formatXRange(from.x, to.x),
420
+ direction: step.direction,
421
+ polarity: step.polarity
422
+ });
423
+ }
424
+ let peak = null;
425
+ let trough = null;
426
+ for (const reading of readings) {
427
+ if (!peak || reading.y > peak.y) peak = reading;
428
+ if (!trough || reading.y < trough.y) trough = reading;
429
+ }
430
+ if (peak && trough && peak.y === trough.y) {
431
+ peak = null;
432
+ trough = null;
433
+ }
434
+ if (peak && (peak.index === startIndex || peak.index === endIndex)) peak = null;
435
+ if (trough && (trough.index === startIndex || trough.index === endIndex)) trough = null;
436
+ const steepest = legs.reduce((best, leg) => best == null || Math.abs(leg.change) > Math.abs(best.change) ? leg : best, null);
437
+ return {
438
+ start,
439
+ end,
440
+ label: window.label,
441
+ rangeText: formatters.formatXRange(start.x, end.x),
442
+ delta,
443
+ absolute: end.y - start.y,
444
+ absoluteText: absoluteDelta.text,
445
+ percentText: percentDelta.renderedMode === "percent" ? percentDelta.text : "—",
446
+ readings,
447
+ legs,
448
+ length: endIndex - startIndex,
449
+ peak,
450
+ trough,
451
+ steepest: steepest && steepest.change !== 0 ? steepest : null
452
+ };
453
+ }
454
+ function quantile(sorted, fraction) {
455
+ if (sorted.length === 0) return 0;
456
+ if (sorted.length === 1) return sorted[0];
457
+ const position = (sorted.length - 1) * fraction;
458
+ const low = Math.floor(position);
459
+ const high = Math.ceil(position);
460
+ if (low === high) return sorted[low];
461
+ return sorted[low] + (sorted[high] - sorted[low]) * (position - low);
462
+ }
463
+ function severityFrom(distance, { notable, unusual, extreme }) {
464
+ if (distance > extreme) return "extreme";
465
+ if (distance > unusual) return "unusual";
466
+ if (distance > notable) return "notable";
467
+ return "typical";
468
+ }
469
+ /**
470
+ * Places the window's change among the changes of every comparable window in
471
+ * the same series — the reading that ESTABLISHES the highlight, and the half
472
+ * MetricCard's tile cannot show. Windows are the same length as the
473
+ * highlight's (in rows), taken at every offset (`rolling`) or tiled
474
+ * non-overlapping from the highlight's own start (`adjacent`), and both bounds
475
+ * must carry a finite reading; a window that cannot produce a change in the
476
+ * compared mode is skipped and counted in `skipped`.
477
+ *
478
+ * Returns `null` when fewer than `minPeers` comparable windows exist — the
479
+ * panel then omits the section and says why, rather than calling one window
480
+ * out of three "extreme".
481
+ *
482
+ * The spread is a COMPARISON, not a significance test: `rolling` windows
483
+ * overlap, so their changes are autocorrelated and the standard deviation is
484
+ * narrower than independent sampling would give. `adjacent` is the honest set.
485
+ * The generated text says "the 2nd largest of 48 comparable periods"; nothing
486
+ * here claims a p-value.
487
+ */
488
+ function derivePeers(summary, info, { mode, baseline, deltaMode, goodDirection, format, locale, percentagePoints, formatters, minPeers = 4, thresholds }) {
489
+ if (!summary || !info) return null;
490
+ const { rows } = summary;
491
+ const lag = info.length;
492
+ if (lag < 1) return null;
493
+ const limits = thresholds ?? (baseline === "sigma" ? DEFAULT_SIGMA_THRESHOLDS : DEFAULT_PERCENTILE_THRESHOLDS);
494
+ const offsets = [];
495
+ if (mode === "adjacent") for (let index = info.start.index % lag; index + lag < rows.length; index += lag) offsets.push(index);
496
+ else for (let index = 0; index + lag < rows.length; index += 1) offsets.push(index);
497
+ const peers = [];
498
+ let skipped = 0;
499
+ let highlightChange = null;
500
+ for (const index of offsets) {
501
+ const from = rows[index];
502
+ const to = rows[index + lag];
503
+ if (from.y == null || to.y == null) {
504
+ skipped += 1;
505
+ continue;
506
+ }
507
+ const delta = formatDelta(to.y, from.y, {
508
+ mode: deltaMode,
509
+ format,
510
+ locale,
511
+ pointsSuffix: percentagePoints
512
+ });
513
+ if (deltaMode === "percent" && delta.renderedMode !== "percent") {
514
+ skipped += 1;
515
+ continue;
516
+ }
517
+ const change = deltaMode === "percent" ? delta.percent : delta.absolute;
518
+ const isHighlight = index === info.start.index;
519
+ if (isHighlight) highlightChange = change;
520
+ let polarity = "neutral";
521
+ if (delta.direction !== "flat" && goodDirection !== "none") polarity = delta.direction === "up" === (goodDirection === "up") ? "positive" : "negative";
522
+ peers.push({
523
+ start: from.x,
524
+ end: to.x,
525
+ change,
526
+ changeText: formatters.formatPeerChange(change),
527
+ rangeText: formatters.formatXRange(from.x, to.x),
528
+ isHighlight,
529
+ polarity
530
+ });
531
+ }
532
+ if (highlightChange == null) return null;
533
+ const others = peers.filter((peer) => !peer.isHighlight).map((peer) => peer.change);
534
+ if (others.length < minPeers) return null;
535
+ const mean = others.reduce((sum, value) => sum + value, 0) / others.length;
536
+ const variance = others.reduce((sum, value) => sum + (value - mean) ** 2, 0) / others.length;
537
+ const sd = Math.sqrt(variance);
538
+ const z = sd > 0 ? (highlightChange - mean) / sd : null;
539
+ const sorted = [...others].sort((a, b) => a - b);
540
+ const percentile = sorted.filter((value) => value <= highlightChange).length / sorted.length * 100;
541
+ const [typicalLow, typicalHigh] = baseline === "sigma" ? [mean - sd, mean + sd] : [quantile(sorted, .1), quantile(sorted, .9)];
542
+ const typical = highlightChange >= typicalLow && highlightChange <= typicalHigh;
543
+ const rank = [...peers].sort((a, b) => Math.abs(b.change) - Math.abs(a.change)).findIndex((peer) => peer.isHighlight) + 1;
544
+ const severity = baseline === "sigma" ? z == null ? highlightChange === mean ? "typical" : "extreme" : severityFrom(Math.abs(z), limits) : severityFrom(Math.abs(percentile - 50), limits);
545
+ const ordinal = (value) => {
546
+ const tens = value % 100;
547
+ if (tens >= 11 && tens <= 13) return `${value}th`;
548
+ switch (value % 10) {
549
+ case 1: return `${value}st`;
550
+ case 2: return `${value}nd`;
551
+ case 3: return `${value}rd`;
552
+ default: return `${value}th`;
553
+ }
554
+ };
555
+ return {
556
+ mode,
557
+ baseline,
558
+ peers,
559
+ count: others.length,
560
+ total: others.length + 1,
561
+ skipped,
562
+ length: lag,
563
+ change: highlightChange,
564
+ changeText: formatters.formatPeerChange(highlightChange),
565
+ mean,
566
+ sd,
567
+ meanText: formatters.formatPeerChange(mean),
568
+ sdText: formatters.formatPeerChange(sd),
569
+ z,
570
+ zText: z == null ? "—" : `${formatNumber(z, "number:1", locale, { signDisplay: "exceptZero" })} sd`,
571
+ percentile,
572
+ percentileText: formatNumber(percentile / 100, "percent:0", locale),
573
+ rank,
574
+ rankText: ordinal(rank),
575
+ typicalLow,
576
+ typicalHigh,
577
+ typicalText: `${formatters.formatPeerChange(typicalLow)} to ${formatters.formatPeerChange(typicalHigh)}`,
578
+ typical,
579
+ severity
580
+ };
581
+ }
582
+ /**
583
+ * Decomposes the window's change into its contributors — the half that
584
+ * EXPLAINS the highlight. Each driver's contribution is the amount it added to
585
+ * the total, either passed directly or derived from its own `from`/`to`
586
+ * readings; rows sort by absolute contribution so the biggest mover reads
587
+ * first, the tail folds into `Other`, and whatever the parts do not account
588
+ * for becomes an explicit `Unexplained` row.
589
+ *
590
+ * Totals reconcile with parts or the panel says they do not: a decomposition
591
+ * that silently misses a third of the move is worse than none.
592
+ */
593
+ function normalizeDrivers(drivers, info, { goodDirection, formatters, limit, otherLabel, residualLabel, tolerance = .005 }) {
594
+ if (!drivers || drivers.length === 0 || !info) return null;
595
+ const total = info.absolute;
596
+ const polarityOf = (contribution) => {
597
+ if (contribution === 0 || goodDirection === "none") return "neutral";
598
+ return contribution > 0 === (goodDirection === "up") ? "positive" : "negative";
599
+ };
600
+ const seen = /* @__PURE__ */ new Map();
601
+ const parsed = drivers.flatMap((driver) => {
602
+ const from = isFinite(driver.from) ? driver.from : null;
603
+ const to = isFinite(driver.to) ? driver.to : null;
604
+ const contribution = isFinite(driver.contribution) ? driver.contribution : from != null && to != null ? to - from : null;
605
+ if (contribution == null) return [];
606
+ const base = driver.key ?? driver.label;
607
+ const repeats = seen.get(base) ?? 0;
608
+ seen.set(base, repeats + 1);
609
+ const share = total === 0 ? null : contribution / total;
610
+ return [{
611
+ key: repeats === 0 ? base : `${base}·${repeats + 1}`,
612
+ label: driver.label,
613
+ contribution,
614
+ contributionText: formatters.formatAmountChange(contribution),
615
+ share,
616
+ shareText: formatters.formatShare(share),
617
+ from,
618
+ to,
619
+ fromText: formatters.formatY(from),
620
+ toText: formatters.formatY(to),
621
+ direction: contribution > 0 ? "up" : contribution < 0 ? "down" : "flat",
622
+ polarity: polarityOf(contribution),
623
+ role: "driver"
624
+ }];
625
+ });
626
+ if (parsed.length === 0) return null;
627
+ const explained = parsed.reduce((sum, row) => sum + row.contribution, 0);
628
+ const residual = total - explained;
629
+ const ordered = [...parsed].sort((a, b) => Math.abs(b.contribution) - Math.abs(a.contribution));
630
+ const rows = [];
631
+ let folded = 0;
632
+ if (limit != null && limit > 0 && ordered.length > limit) {
633
+ rows.push(...ordered.slice(0, limit));
634
+ const tail = ordered.slice(limit);
635
+ folded = tail.length;
636
+ const contribution = tail.reduce((sum, row) => sum + row.contribution, 0);
637
+ const share = total === 0 ? null : contribution / total;
638
+ rows.push({
639
+ key: "__other__",
640
+ label: otherLabel,
641
+ contribution,
642
+ contributionText: formatters.formatAmountChange(contribution),
643
+ share,
644
+ shareText: formatters.formatShare(share),
645
+ from: null,
646
+ to: null,
647
+ fromText: "—",
648
+ toText: "—",
649
+ direction: contribution > 0 ? "up" : contribution < 0 ? "down" : "flat",
650
+ polarity: polarityOf(contribution),
651
+ role: "other"
652
+ });
653
+ } else rows.push(...ordered);
654
+ const reconciles = total === 0 ? Math.abs(residual) === 0 : Math.abs(residual / total) <= tolerance;
655
+ if (!reconciles) {
656
+ const share = total === 0 ? null : residual / total;
657
+ rows.push({
658
+ key: "__residual__",
659
+ label: residualLabel,
660
+ contribution: residual,
661
+ contributionText: formatters.formatAmountChange(residual),
662
+ share,
663
+ shareText: formatters.formatShare(share),
664
+ from: null,
665
+ to: null,
666
+ fromText: "—",
667
+ toText: "—",
668
+ direction: residual > 0 ? "up" : residual < 0 ? "down" : "flat",
669
+ polarity: polarityOf(residual),
670
+ role: "residual"
671
+ });
672
+ }
673
+ const withTotal = total === 0 ? 1 : Math.sign(total);
674
+ const contributors = rows.filter((row) => row.role !== "residual");
675
+ const top = contributors.filter((row) => Math.sign(row.contribution) === withTotal).reduce((best, row) => best == null || Math.abs(row.contribution) > Math.abs(best.contribution) ? row : best, null) ?? null;
676
+ const counter = contributors.filter((row) => Math.sign(row.contribution) === -withTotal).reduce((best, row) => best == null || Math.abs(row.contribution) > Math.abs(best.contribution) ? row : best, null) ?? null;
677
+ return {
678
+ rows,
679
+ total,
680
+ totalText: formatters.formatAmountChange(total),
681
+ explained,
682
+ explainedText: formatters.formatAmountChange(explained),
683
+ residual,
684
+ residualText: formatters.formatAmountChange(residual),
685
+ reconciles,
686
+ folded,
687
+ top,
688
+ counter
689
+ };
690
+ }
691
+ /**
692
+ * Splits the series around the window for the context plot: every row carries
693
+ * `hl` and the window's `tone`, the readings outside it are split into a
694
+ * `before` and an `after` run so the gray path never runs under the coloured
695
+ * one, and the window's extent rides one band row.
696
+ */
697
+ function buildHighlightContextDatasets(summary, info) {
698
+ const tone = info.delta.polarity;
699
+ const table = [];
700
+ const outside = [];
701
+ const inside = [];
702
+ for (let index = 0; index < summary.rows.length; index += 1) {
703
+ const row = summary.rows[index];
704
+ const hl = index >= info.start.index && index <= info.end.index;
705
+ const entry = {
706
+ ...row,
707
+ hl,
708
+ tone: hl ? tone : null
709
+ };
710
+ table.push(entry);
711
+ if (hl) inside.push(entry);
712
+ if (!hl || index === info.start.index || index === info.end.index) outside.push({
713
+ ...row,
714
+ seg: index <= info.start.index ? "before" : "after"
715
+ });
716
+ }
717
+ return {
718
+ table,
719
+ outside,
720
+ inside,
721
+ endpoints: [{
722
+ x: info.start.x,
723
+ y: info.start.y,
724
+ role: "start",
725
+ tone
726
+ }, {
727
+ x: info.end.x,
728
+ y: info.end.y,
729
+ role: "end",
730
+ tone
731
+ }],
732
+ band: [{
733
+ start: info.start.x,
734
+ end: info.end.x,
735
+ tone
736
+ }]
737
+ };
738
+ }
739
+ /** One row `{ low, high }` — the typical-range band behind the strip. */
740
+ function buildTypicalRows(stats) {
741
+ return [{
742
+ low: stats.typicalLow,
743
+ high: stats.typicalHigh
744
+ }];
745
+ }
746
+ /** The driver bars, in row order, with the fields the spec encodes. */
747
+ function buildDriverRows(info) {
748
+ return info.rows.map((row) => ({
749
+ key: row.key,
750
+ label: row.label,
751
+ value: row.contribution,
752
+ share: row.share,
753
+ tone: row.polarity,
754
+ role: row.role
755
+ }));
756
+ }
757
+ function highlightSegmentsToText(segments) {
758
+ return segments.map((segment) => typeof segment === "string" ? segment : segment.value).join("");
759
+ }
760
+ /** The takeaway: what moved, how it places among its peers, which driver carried it. */
761
+ function buildHighlightAnalysisInsight({ name, info, peers, drivers, labels }) {
762
+ const { direction } = info.delta;
763
+ return labels.insight({
764
+ name,
765
+ direction,
766
+ change: direction === "flat" ? "" : stripSign(info.delta.text),
767
+ secondary: info.delta.secondaryText ? stripSign(info.delta.secondaryText) : null,
768
+ range: info.rangeText,
769
+ label: info.label,
770
+ severity: peers?.severity ?? "typical",
771
+ rank: peers?.rankText ?? null,
772
+ count: peers?.count ?? null,
773
+ topDriver: drivers?.top?.label ?? null,
774
+ topContribution: drivers?.top ? stripSign(drivers.top.contributionText) : null,
775
+ topShare: drivers?.top?.share != null ? drivers.top.shareText : null
776
+ });
777
+ }
778
+ /**
779
+ * The panel's text equivalent (Amy Cesal's alt-text formula: what it is, what
780
+ * it shows, why it is here, when it was true) — the figure's accessible
781
+ * description, and the summary a screen reader gets before the parts.
782
+ */
783
+ function buildHighlightDescription({ name, state, info, peers, drivers, formatters, asOfText, labels }) {
784
+ if (!info || state !== "ready") return labels.cardDescription({
785
+ name,
786
+ state,
787
+ range: "",
788
+ direction: "flat",
789
+ change: "",
790
+ secondary: null,
791
+ startText: "",
792
+ endText: "",
793
+ severity: null,
794
+ rank: null,
795
+ count: null,
796
+ driverCount: 0,
797
+ topDriver: null,
798
+ topContribution: null,
799
+ asOf: asOfText ?? null
800
+ });
801
+ return labels.cardDescription({
802
+ name,
803
+ state,
804
+ range: info.rangeText,
805
+ label: info.label,
806
+ direction: info.delta.direction,
807
+ change: info.delta.direction === "flat" ? "" : stripSign(info.delta.text),
808
+ secondary: info.delta.secondaryText ? stripSign(info.delta.secondaryText) : null,
809
+ startText: formatters.formatY(info.start.y),
810
+ endText: formatters.formatY(info.end.y),
811
+ severity: peers ? labels.severityNames[peers.severity].toLowerCase() : null,
812
+ rank: peers?.rankText ?? null,
813
+ count: peers?.count ?? null,
814
+ driverCount: drivers?.rows.length ?? 0,
815
+ topDriver: drivers?.top?.label ?? null,
816
+ topContribution: drivers?.top ? stripSign(drivers.top.contributionText) : null,
817
+ asOf: asOfText ?? null
818
+ });
819
+ }
820
+ /** Accessible description of the context plot — set live through `view.description`. */
821
+ function buildContextDescription({ name, mark, summary, info, formatters, labels }) {
822
+ return labels.contextDescription({
823
+ mark: labels.markNames[mark],
824
+ name,
825
+ count: summary.count,
826
+ first: summary.first ? formatters.formatX(summary.first.x) : "—",
827
+ last: summary.last ? formatters.formatX(summary.last.x) : "—",
828
+ min: formatters.formatY(summary.min),
829
+ max: formatters.formatY(summary.max),
830
+ range: info.rangeText,
831
+ label: info.label,
832
+ direction: info.delta.direction,
833
+ change: stripSign(info.delta.text)
834
+ });
835
+ }
836
+ /** Accessible description of the peer strip. */
837
+ function buildPeersDescription({ name, stats, labels }) {
838
+ return labels.peersDescription({
839
+ name,
840
+ count: stats.count,
841
+ readings: stats.length + 1,
842
+ mode: stats.mode,
843
+ change: stats.changeText,
844
+ rank: stats.rankText,
845
+ typical: stats.typicalText,
846
+ severity: labels.severityNames[stats.severity].toLowerCase()
847
+ });
848
+ }
849
+ /** Accessible description of the drivers bars. */
850
+ function buildDriversDescription({ name, info, labels }) {
851
+ return labels.driversDescription({
852
+ name,
853
+ count: info.rows.filter((row) => row.role !== "residual").length,
854
+ total: info.totalText,
855
+ top: info.top?.label ?? null,
856
+ topContribution: info.top?.contributionText ?? null,
857
+ counter: info.counter?.label ?? null,
858
+ counterContribution: info.counter?.contributionText ?? null,
859
+ residual: info.reconciles ? null : info.residualText
860
+ });
861
+ }
862
+ /** The verdict sentence under the strip — visible text, never colour alone. */
863
+ function buildPeerVerdict(stats, labels) {
864
+ return labels.peerVerdict({
865
+ severity: stats.severity,
866
+ rank: stats.rankText,
867
+ total: stats.total,
868
+ readings: stats.length + 1,
869
+ typical: stats.typicalText,
870
+ change: stats.changeText,
871
+ inside: stats.typical
872
+ });
873
+ }
874
+ //#endregion
875
+ export { DEFAULT_HIGHLIGHT_LABELS, DEFAULT_PERCENTILE_THRESHOLDS, DEFAULT_SIGMA_THRESHOLDS, buildContextDescription, buildDriverRows, buildDriversDescription, buildHighlightAnalysisInsight, buildHighlightContextDatasets, buildHighlightDescription, buildPeerVerdict, buildPeersDescription, buildTypicalRows, createHighlightFormatters, deriveHighlightDelta, deriveHighlightState, derivePeers, highlightSegmentsToText, normalizeDrivers, normalizeHighlightSeries, resolveHighlightLabels, resolveHighlightWindow };