@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,623 @@
1
+ import { formatDate, formatDateRange, formatDelta, formatNumber } from "../../lib/vega/formatters.js";
2
+ //#region src/components/TrendCard/TrendCard.model.ts
3
+ const DEFAULT_TREND_LABELS = {
4
+ increase: "Up",
5
+ decrease: "Down",
6
+ unchanged: "No change",
7
+ percentagePoints: " pp",
8
+ noData: "No data",
9
+ noMatch: "No matching data",
10
+ clearFilters: "Clear filters",
11
+ error: "Couldn't load this trend",
12
+ retry: "Try again",
13
+ insufficient: "Not enough data for a trend",
14
+ stale: "Stale",
15
+ asOf: "As of",
16
+ tablePeriod: "Period",
17
+ tableValue: "Value",
18
+ tableActual: "actual",
19
+ target: "Target",
20
+ other: "Other",
21
+ legendGroup: (name) => `Series shown in ${name}`,
22
+ hideSeries: (series) => `Hide ${series}`,
23
+ showSeries: (series) => `Show ${series}`,
24
+ focused: "focused",
25
+ band: "highlighted period",
26
+ since: (period) => `vs ${period}`,
27
+ indexedAt: (period) => `Indexed to 100 at ${period}`,
28
+ normalized: "Share of period total",
29
+ showTable: (name) => `Show the data behind ${name}`,
30
+ hideTable: (name) => `Hide the data behind ${name}`,
31
+ about: (name) => `About ${name}`,
32
+ loading: (name) => `Loading ${name}`,
33
+ tableCaption: (name) => `Data behind the ${name} trend`,
34
+ changeGroup: (name) => `${name} change`,
35
+ deltaExplanation: ({ fromLabel, fromText, toLabel, toText, changeText, secondaryText, renderedMode, direction }) => {
36
+ const rows = [
37
+ [fromLabel, fromText],
38
+ [toLabel, toText],
39
+ [direction === "up" ? "Increase" : direction === "down" ? "Decrease" : "No change", changeText]
40
+ ];
41
+ if (secondaryText) rows.push([renderedMode === "percent" ? "Change" : "Change %", secondaryText]);
42
+ return rows;
43
+ },
44
+ spanSentence: ({ word, series, primary, secondary, range, from, to }) => primary ? `${series} ${word.toLowerCase()} ${primary}${secondary ? ` (${secondary})` : ""} over ${range}, from ${from} to ${to}.` : `${series} ${word.toLowerCase()} over ${range} (${from}).`,
45
+ extremesSentence: ({ peak, peakAt, trough, troughAt }) => `Peaked at ${peak} on ${peakAt} and bottomed at ${trough} on ${troughAt}.`,
46
+ seriesSummary: (count, list) => count === 1 ? `Latest reading: ${list}.` : `${count} series, latest readings: ${list}.`,
47
+ foldedSummary: (count) => `${count} more folded into Other.`,
48
+ bandsSummary: (list) => `Highlighted periods: ${list}.`,
49
+ markNames: {
50
+ line: "Line",
51
+ area: "Area",
52
+ bar: "Bar"
53
+ },
54
+ chartDescription: ({ mark, name, seriesCount, periodCount, range, min, max, focus, target, stacked }) => `${mark} chart of ${name}: ${seriesCount} series over ${periodCount} periods, ${range}, ranging ${min} to ${max}.` + (focus ? ` ${focus} emphasised.` : "") + (stacked ? ` ${stacked}.` : "") + (target ? ` Reference line at the ${target} target.` : ""),
55
+ insightMark: "Insight",
56
+ insight: ({ series, direction, from, to, change, secondary, range, peak, peakAt }) => {
57
+ if (direction === "flat") return [
58
+ `${series} held at `,
59
+ { value: from },
60
+ ` across ${range}.`
61
+ ];
62
+ const segments = [
63
+ `${series} ${direction === "up" ? "rose" : "fell"} from `,
64
+ { value: from },
65
+ " to ",
66
+ { value: to },
67
+ ` across ${range}, a ${direction === "up" ? "rise" : "drop"} of `,
68
+ { value: change }
69
+ ];
70
+ if (secondary) segments.push(" (", { value: secondary }, ")");
71
+ segments.push(".");
72
+ if (peak && peakAt) segments.push(` It peaked at `, { value: peak }, ` on ${peakAt}.`);
73
+ return segments;
74
+ }
75
+ };
76
+ function resolveTrendLabels(overrides) {
77
+ return overrides ? {
78
+ ...DEFAULT_TREND_LABELS,
79
+ ...overrides
80
+ } : DEFAULT_TREND_LABELS;
81
+ }
82
+ /**
83
+ * The state machine. Error wins; loading over existing series is a refresh (the
84
+ * previous render stays under the veil); no readings is `no-match` when the
85
+ * consumer's filter caused it and `empty` otherwise.
86
+ */
87
+ function deriveTrendState({ status, hasValue, hasData, filtered }) {
88
+ if (status === "error") return "error";
89
+ if (status === "loading") return hasData ? "refreshing" : "loading";
90
+ if (hasValue) return "ready";
91
+ return filtered ? "no-match" : "empty";
92
+ }
93
+ const isFinite = (value) => typeof value === "number" && Number.isFinite(value);
94
+ const DAY_MS = 864e5;
95
+ const ISO_DATE = /^\d{4}-\d{2}(-\d{2})?([T ].*)?$/;
96
+ /** The key of the series the long tail folds into. Never collides with a consumer key that is a label. */
97
+ const TREND_OTHER_KEY = "__other__";
98
+ function inferXType(series) {
99
+ for (const entry of series) {
100
+ const sample = entry.points.find((point) => point.x != null);
101
+ if (!sample) continue;
102
+ const { x } = sample;
103
+ if (x instanceof Date) return "temporal";
104
+ if (typeof x === "number") return "quantitative";
105
+ return ISO_DATE.test(x) ? "temporal" : "ordinal";
106
+ }
107
+ return "ordinal";
108
+ }
109
+ function inferGranularity(periods) {
110
+ const xs = periods.filter((x) => typeof x === "number");
111
+ if (xs.length < 2) return "day";
112
+ const gaps = [];
113
+ for (let index = 1; index < xs.length; index += 1) gaps.push(Math.abs(xs[index] - xs[index - 1]));
114
+ gaps.sort((a, b) => a - b);
115
+ const median = gaps[Math.floor(gaps.length / 2)];
116
+ if (median < DAY_MS) return "time";
117
+ if (median >= 360 * DAY_MS) return "year";
118
+ if (median >= 27 * DAY_MS) return "month";
119
+ return "day";
120
+ }
121
+ /**
122
+ * The granularity an x axis labels its ticks at: the tooltip and table keep the
123
+ * full form (`Sep 8, 2026`); an axis has room for `Sep 8`. Duplicated per
124
+ * component rather than shared, so a card's text grammar is readable in one
125
+ * file — the same reason each card owns its own state machine.
126
+ */
127
+ function trendAxisGranularity(granularity) {
128
+ switch (granularity) {
129
+ case "day": return "day-short";
130
+ case "month": return "month-short";
131
+ case "datetime": return "time";
132
+ default: return granularity;
133
+ }
134
+ }
135
+ function normalizeX(x, xType) {
136
+ if (xType === "temporal") {
137
+ const time = x instanceof Date ? x.getTime() : new Date(x).getTime();
138
+ return Number.isNaN(time) ? null : time;
139
+ }
140
+ if (xType === "quantitative") return isFinite(x) ? x : null;
141
+ return String(x);
142
+ }
143
+ /**
144
+ * The colour job, resolved against the theme's own categorical budget.
145
+ *
146
+ * The brand ships six hues and reserves three for status, so `colors.css`
147
+ * defines only a handful of `chart-N` identity slots. Repeating a slot would
148
+ * make two series look like one, so `categorical` past the budget degrades to
149
+ * `emphasis` — the focused series in the hue on a context-gray field, which is
150
+ * the FT/Few reading of a many-line chart anyway. `auto` applies the same
151
+ * budget without being asked.
152
+ */
153
+ function resolveSeriesColorMode(requested, seriesCount, slots) {
154
+ if (requested === "sequential") return "sequential";
155
+ if (requested === "emphasis") return "emphasis";
156
+ if (requested === "categorical") return seriesCount <= slots ? "categorical" : "emphasis";
157
+ if (seriesCount <= 1) return "emphasis";
158
+ return seriesCount <= slots ? "categorical" : "emphasis";
159
+ }
160
+ /**
161
+ * The colour one series paints with. Identity slots are assigned by the
162
+ * series' index in the consumer's order — never by rank or by value — so
163
+ * hiding a series cannot repaint the survivors (rule 6). A sequential ramp
164
+ * spreads the ordered series across the whole ramp so the ends are always the
165
+ * ends, however many series there are.
166
+ */
167
+ function seriesColorAt(index, seriesCount, mode, focusIndex, tokens) {
168
+ if (mode === "categorical") return tokens.category[index % tokens.category.length];
169
+ if (mode === "sequential") {
170
+ const ramp = tokens.sequential;
171
+ if (seriesCount <= 1) return ramp[ramp.length - 1];
172
+ const step = Math.round(index * (ramp.length - 1) / (seriesCount - 1));
173
+ return ramp[Math.min(ramp.length - 1, Math.max(0, step))];
174
+ }
175
+ return index === focusIndex ? tokens.emphasis : tokens.context;
176
+ }
177
+ /**
178
+ * Normalises `series` onto one shared period domain for the plot, the legend
179
+ * and the table: temporal `x` becomes epoch milliseconds, invalid dates are
180
+ * dropped, a period a series has no reading for stays a gap (never a zero),
181
+ * the long tail folds into `Other` when asked, and the visible series are laid
182
+ * out long-form with their colour and stack order baked into each row — so the
183
+ * spec stays structural and a visibility change is a data push, not a re-embed.
184
+ *
185
+ * Returns `null` when there are no series to speak of.
186
+ */
187
+ function normalizeSeries(series, { granularity, normalize = "none", focus, visible, maxSeries, otherLabel, colorMode, tokens, stack = false }) {
188
+ if (!series) return null;
189
+ const xType = inferXType(series);
190
+ const seen = /* @__PURE__ */ new Map();
191
+ let entries = series.map((entry) => {
192
+ const base = entry.key ?? entry.label;
193
+ const repeats = seen.get(base) ?? 0;
194
+ seen.set(base, repeats + 1);
195
+ return {
196
+ key: repeats === 0 ? base : `${base}·${repeats + 1}`,
197
+ label: entry.label,
198
+ points: entry.points
199
+ };
200
+ });
201
+ const focusKey = focus != null && entries.some((entry) => entry.key === focus) ? focus : entries[0]?.key ?? "";
202
+ let folded = 0;
203
+ if (maxSeries != null && maxSeries >= 1 && entries.length > maxSeries) {
204
+ const keepCount = Math.max(0, maxSeries - 1);
205
+ const kept = [];
206
+ const rest = [];
207
+ entries.forEach((entry, index) => {
208
+ if (index < keepCount || entry.key === focusKey) kept.push(entry);
209
+ else rest.push(entry);
210
+ });
211
+ if (rest.length >= 2) {
212
+ const totals = /* @__PURE__ */ new Map();
213
+ for (const entry of rest) for (const point of entry.points) {
214
+ const x = normalizeX(point.x, xType);
215
+ if (x == null || !isFinite(point.y)) continue;
216
+ totals.set(String(x), (totals.get(String(x)) ?? 0) + point.y);
217
+ }
218
+ const orderedX = [];
219
+ const seenX = /* @__PURE__ */ new Set();
220
+ for (const entry of rest) for (const point of entry.points) {
221
+ const x = normalizeX(point.x, xType);
222
+ if (x == null || seenX.has(String(x))) continue;
223
+ seenX.add(String(x));
224
+ orderedX.push(x);
225
+ }
226
+ const points = orderedX.map((x) => ({
227
+ x,
228
+ y: totals.get(String(x)) ?? null
229
+ }));
230
+ entries = [...kept, {
231
+ key: TREND_OTHER_KEY,
232
+ label: otherLabel,
233
+ points
234
+ }];
235
+ folded = rest.length;
236
+ }
237
+ }
238
+ const periodSet = /* @__PURE__ */ new Map();
239
+ for (const entry of entries) for (const point of entry.points) {
240
+ const x = normalizeX(point.x, xType);
241
+ if (x == null) continue;
242
+ periodSet.set(String(x), x);
243
+ }
244
+ const periods = [...periodSet.values()];
245
+ if (xType !== "ordinal") periods.sort((a, b) => a - b);
246
+ const focusIndex = Math.max(0, entries.findIndex((entry) => entry.key === focusKey));
247
+ const positionOf = new Map(periods.map((x, index) => [String(x), index]));
248
+ const infos = entries.map((entry, index) => {
249
+ const values = new Array(periods.length).fill(null);
250
+ const reported = new Array(periods.length).fill(false);
251
+ for (const point of entry.points) {
252
+ const x = normalizeX(point.x, xType);
253
+ if (x == null) continue;
254
+ const at = positionOf.get(String(x));
255
+ if (at == null) continue;
256
+ values[at] = isFinite(point.y) ? point.y : null;
257
+ reported[at] = true;
258
+ }
259
+ const baseIndex = values.findIndex((value) => value != null);
260
+ const base = baseIndex < 0 ? null : values[baseIndex];
261
+ const plotted = normalize === "index" && base != null && base !== 0 ? values.map((value) => value == null ? null : value / base * 100) : values;
262
+ const finite = values.map((value, at) => ({
263
+ x: periods[at],
264
+ y: value
265
+ })).filter((row) => row.y != null);
266
+ const finitePlotted = plotted.map((value, at) => ({
267
+ x: periods[at],
268
+ y: value
269
+ })).filter((row) => row.y != null);
270
+ return {
271
+ key: entry.key,
272
+ label: entry.label,
273
+ index,
274
+ color: seriesColorAt(index, entries.length, colorMode, focusIndex, tokens),
275
+ values,
276
+ plotted,
277
+ reported,
278
+ count: finite.length,
279
+ first: finite[0] ?? null,
280
+ latest: finite[finite.length - 1] ?? null,
281
+ latestPlotted: finitePlotted[finitePlotted.length - 1] ?? null,
282
+ min: finite.length > 0 ? Math.min(...finite.map((row) => row.y)) : null,
283
+ max: finite.length > 0 ? Math.max(...finite.map((row) => row.y)) : null
284
+ };
285
+ });
286
+ const shown = new Set(visible ?? infos.map((info) => info.key));
287
+ const visibleInfos = infos.filter((info) => shown.has(info.key));
288
+ const rows = [];
289
+ periods.forEach((x, at) => {
290
+ for (const info of visibleInfos) {
291
+ if (!info.reported[at]) continue;
292
+ rows.push({
293
+ x,
294
+ s: info.key,
295
+ y: info.plotted[at],
296
+ raw: info.values[at],
297
+ i: info.index,
298
+ c: info.color,
299
+ n: info.label,
300
+ f: info.key === focusKey
301
+ });
302
+ }
303
+ });
304
+ const plottedValues = rows.map((row) => row.y).filter((y) => y != null);
305
+ let min = plottedValues.length > 0 ? Math.min(...plottedValues) : null;
306
+ let max = plottedValues.length > 0 ? Math.max(...plottedValues) : null;
307
+ if (stack === "zero" && plottedValues.length > 0) {
308
+ let positive = 0;
309
+ let negative = 0;
310
+ periods.forEach((_x, at) => {
311
+ let up = 0;
312
+ let down = 0;
313
+ for (const info of visibleInfos) {
314
+ const value = info.plotted[at];
315
+ if (value == null) continue;
316
+ if (value >= 0) up += value;
317
+ else down += value;
318
+ }
319
+ positive = Math.max(positive, up);
320
+ negative = Math.min(negative, down);
321
+ });
322
+ max = Math.max(max ?? 0, positive);
323
+ min = Math.min(min ?? 0, negative);
324
+ }
325
+ return {
326
+ xType,
327
+ granularity: granularity ?? (xType === "temporal" ? inferGranularity(periods) : "day"),
328
+ periods,
329
+ series: infos,
330
+ rows,
331
+ count: plottedValues.length,
332
+ min,
333
+ max,
334
+ hasNegative: plottedValues.some((value) => value < 0),
335
+ folded,
336
+ focusKey
337
+ };
338
+ }
339
+ /** The periods the crosshair reads — one row each, so it lands on the period rather than on a series. */
340
+ function buildPeriodRows(summary) {
341
+ return summary.periods.map((x) => ({ x }));
342
+ }
343
+ /** One row per visible series at its last plotted reading — where a direct endpoint label sits. */
344
+ function buildLatestRows(summary, visibleKeys) {
345
+ const shown = new Set(visibleKeys);
346
+ return summary.series.filter((info) => shown.has(info.key) && info.latestPlotted != null).map((info) => ({
347
+ x: info.latestPlotted.x,
348
+ y: info.latestPlotted.y,
349
+ n: info.label,
350
+ c: info.color
351
+ }));
352
+ }
353
+ /**
354
+ * Snaps every band onto the period domain. Each bound moves inward to the
355
+ * nearest period the data actually has, so a band can never extend the axis;
356
+ * a band that covers no period is dropped rather than drawn as a hairline.
357
+ */
358
+ function resolveBands(bands, summary) {
359
+ if (!bands || !summary || summary.periods.length === 0) return [];
360
+ const { periods, xType } = summary;
361
+ const rows = [];
362
+ for (const band of bands) {
363
+ const lo = normalizeX(band.start, xType);
364
+ const hi = normalizeX(band.end, xType);
365
+ if (lo == null || hi == null) continue;
366
+ if (xType === "ordinal") {
367
+ let first = periods.findIndex((x) => x === lo);
368
+ let last = periods.findIndex((x) => x === hi);
369
+ if (first < 0 || last < 0) continue;
370
+ if (first > last) [first, last] = [last, first];
371
+ rows.push({
372
+ start: periods[first],
373
+ end: periods[last],
374
+ label: band.label
375
+ });
376
+ continue;
377
+ }
378
+ const [from, to] = lo <= hi ? [lo, hi] : [hi, lo];
379
+ const inside = periods.filter((x) => x >= from && x <= to);
380
+ if (inside.length === 0) continue;
381
+ rows.push({
382
+ start: inside[0],
383
+ end: inside[inside.length - 1],
384
+ label: band.label
385
+ });
386
+ }
387
+ return rows;
388
+ }
389
+ /**
390
+ * The band a period falls inside — the tooltip's and the table's band marker.
391
+ * A continuous axis compares the values; an ordinal one compares positions in
392
+ * `periods`, since a category label has no order of its own. The first
393
+ * matching band wins, so overlapping bands are read in the consumer's order.
394
+ */
395
+ function bandAt(bands, periods, x) {
396
+ if (bands.length === 0) return null;
397
+ const positionOf = (value) => typeof value === "number" ? value : periods.findIndex((period) => String(period) === String(value));
398
+ const at = positionOf(x);
399
+ if (at < 0) return null;
400
+ for (const band of bands) {
401
+ const from = positionOf(band.start);
402
+ const to = positionOf(band.end);
403
+ if (from < 0 || to < 0) continue;
404
+ if (at >= Math.min(from, to) && at <= Math.max(from, to)) return band;
405
+ }
406
+ return null;
407
+ }
408
+ /**
409
+ * The change the focused series made across the span it actually covers —
410
+ * first finite reading to last finite reading — with the extremes in between.
411
+ * `null` unless the series holds two distinct readings, so the card never
412
+ * invents a trend from one point.
413
+ */
414
+ function deriveTrendSpan(summary, focusKey, { deltaMode, goodDirection, format, locale, percentagePoints, formatters }) {
415
+ if (!summary) return null;
416
+ const info = summary.series.find((entry) => entry.key === focusKey) ?? summary.series[0];
417
+ if (!info || !info.first || !info.latest || info.count < 2) return null;
418
+ if (String(info.first.x) === String(info.latest.x)) return null;
419
+ const delta = deriveTrendDelta(info.latest.y, info.first.y, {
420
+ deltaMode,
421
+ goodDirection,
422
+ format,
423
+ locale,
424
+ percentagePoints
425
+ });
426
+ if (!delta) return null;
427
+ const finite = info.values.map((value, at) => ({
428
+ x: summary.periods[at],
429
+ y: value
430
+ })).filter((row) => row.y != null);
431
+ let peak = finite[0];
432
+ let trough = finite[0];
433
+ for (const row of finite) {
434
+ if (row.y > peak.y) peak = row;
435
+ if (row.y < trough.y) trough = row;
436
+ }
437
+ const isEnd = (row) => String(row.x) === String(info.first?.x) || String(row.x) === String(info.latest?.x);
438
+ return {
439
+ seriesKey: info.key,
440
+ seriesLabel: info.label,
441
+ from: info.first,
442
+ to: info.latest,
443
+ rangeText: formatters.formatXRange(info.first.x, info.latest.x),
444
+ delta,
445
+ peak: isEnd(peak) ? null : peak,
446
+ trough: isEnd(trough) ? null : trough
447
+ };
448
+ }
449
+ /** A change between two readings, with polarity resolved through `goodDirection`. */
450
+ function deriveTrendDelta(value, previous, { deltaMode, goodDirection, format, locale, percentagePoints }) {
451
+ if (!isFinite(value) || !isFinite(previous)) return null;
452
+ const delta = formatDelta(value, previous, {
453
+ mode: deltaMode,
454
+ format,
455
+ locale,
456
+ pointsSuffix: percentagePoints
457
+ });
458
+ const other = formatDelta(value, previous, {
459
+ mode: delta.renderedMode === "percent" ? "absolute" : "percent",
460
+ format,
461
+ locale,
462
+ pointsSuffix: percentagePoints
463
+ });
464
+ let polarity = "neutral";
465
+ if (delta.direction !== "flat" && goodDirection !== "none") polarity = delta.direction === "up" === (goodDirection === "up") ? "positive" : "negative";
466
+ return {
467
+ direction: delta.direction,
468
+ polarity,
469
+ text: delta.text,
470
+ renderedMode: delta.renderedMode,
471
+ secondaryText: other.renderedMode === delta.renderedMode ? null : other.text
472
+ };
473
+ }
474
+ /** The formatters every text surface shares — headline, badge, legend, table, tooltip, descriptions. */
475
+ function createTrendFormatters({ format, locale, timeZone, formatValue, xType, granularity, normalize = "none", stack = false, indexFormat = "number:0" }) {
476
+ const formatX = (x) => {
477
+ if (xType === "temporal" && typeof x === "number") return formatDate(x, {
478
+ granularity,
479
+ locale,
480
+ timeZone
481
+ });
482
+ if (xType === "quantitative" && typeof x === "number") return formatNumber(x, "number", locale);
483
+ return String(x);
484
+ };
485
+ const value = (input) => isFinite(input) ? formatValue ? formatValue(input) : formatNumber(input, format, locale) : "—";
486
+ return {
487
+ formatValue: value,
488
+ formatPlotted: normalize === "index" ? (input) => formatNumber(input, indexFormat, locale) : stack === "normalize" ? (input) => formatNumber(input, "percent:0", locale) : value,
489
+ formatX,
490
+ formatXRange: (start, end) => xType === "temporal" && typeof start === "number" && typeof end === "number" ? formatDateRange(start, end, {
491
+ granularity,
492
+ locale,
493
+ timeZone
494
+ }) : `${formatX(start)} – ${formatX(end)}`
495
+ };
496
+ }
497
+ const stripSign = (text) => text.replace(/^[+\-−]/, "");
498
+ /**
499
+ * The generated insight — what the card shows beside the AI mark when the
500
+ * consumer passes no `insight`. The focused series' two ends, its change (in
501
+ * the badge's form, with the other form in brackets) and its peak come back as
502
+ * `{ value }` segments so the card can set them in bold; the words come from
503
+ * `labels.insight`.
504
+ */
505
+ function buildTrendInsight({ span, formatters, labels }) {
506
+ const flat = span.delta.direction === "flat";
507
+ const secondary = span.delta.secondaryText;
508
+ return labels.insight({
509
+ series: span.seriesLabel,
510
+ direction: span.delta.direction,
511
+ from: formatters.formatValue(span.from.y),
512
+ to: formatters.formatValue(span.to.y),
513
+ change: flat ? "" : stripSign(span.delta.text),
514
+ secondary: flat || !secondary ? null : stripSign(secondary),
515
+ range: span.rangeText,
516
+ peak: span.peak ? formatters.formatValue(span.peak.y) : null,
517
+ peakAt: span.peak ? formatters.formatX(span.peak.x) : null
518
+ });
519
+ }
520
+ /**
521
+ * The hover chip's contents for one period — pure, so the shared-x reading is
522
+ * unit-tested rather than only reachable through a pointer event (Vega's own
523
+ * hit-testing needs a visible document, which a hidden frame does not give).
524
+ *
525
+ * Ranked descending by the PLOTTED value, so the reader compares by reading
526
+ * down the chip in the order the marks sit on screen. Returns `null` for a
527
+ * period the card does not have.
528
+ */
529
+ function buildTrendTooltip({ summary, visibleKeys, bands, x, target, formatters, transformed }) {
530
+ if (x == null || !summary) return null;
531
+ const at = summary.periods.findIndex((period) => String(period) === String(x));
532
+ if (at < 0) return null;
533
+ const shown = new Set(visibleKeys);
534
+ const band = bandAt(bands, summary.periods, summary.periods[at]);
535
+ const entries = summary.series.filter((info) => shown.has(info.key) && info.reported[at]).map((info) => ({
536
+ info,
537
+ plotted: info.plotted[at],
538
+ value: info.values[at]
539
+ })).sort((a, b) => (b.plotted ?? Number.NEGATIVE_INFINITY) - (a.plotted ?? Number.NEGATIVE_INFINITY)).map(({ info, plotted, value }) => ({
540
+ key: info.key,
541
+ label: info.label,
542
+ color: info.color,
543
+ isFocus: info.key === summary.focusKey,
544
+ valueText: formatters.formatValue(value),
545
+ plottedText: transformed ? formatters.formatPlotted(plotted) : null
546
+ }));
547
+ return {
548
+ periodText: formatters.formatX(summary.periods[at]),
549
+ bandText: band ? band.label ?? formatters.formatXRange(band.start, band.end) : null,
550
+ entries,
551
+ targetText: target != null ? formatters.formatValue(target) : null
552
+ };
553
+ }
554
+ /**
555
+ * The card's text equivalent (Amy Cesal's alt-text formula: what it is, what it
556
+ * shows, why it is here, when it was true) — the figure's accessible
557
+ * description, and the summary a screen reader gets before the parts. Every
558
+ * visible series' latest reading is listed, so nothing lives only in the lines.
559
+ */
560
+ function buildTrendDescription({ name, state, headline, summary, visibleKeys, span, bands, formatters, targetText, asOfText, labels }) {
561
+ if (state === "loading") return `${labels.loading(name)}.`;
562
+ if (state === "error") return `${name}: ${labels.error}.`;
563
+ if (state === "empty") return `${name}: ${labels.noData}.`;
564
+ if (state === "no-match") return `${name}: ${labels.noMatch}.`;
565
+ const shown = new Set(visibleKeys);
566
+ const visible = (summary?.series ?? []).filter((info) => shown.has(info.key));
567
+ const sentences = [];
568
+ if (headline === "latest" && span) sentences.push(`${name}: ${span.seriesLabel} ${formatters.formatValue(span.to.y)}.`);
569
+ else if (headline === "change" && span) sentences.push(`${name}: ${span.seriesLabel} ${span.delta.text} over ${span.rangeText}.`);
570
+ else sentences.push(`${name}.`);
571
+ if (span) {
572
+ const flat = span.delta.direction === "flat";
573
+ const word = span.delta.direction === "up" ? labels.increase : span.delta.direction === "down" ? labels.decrease : labels.unchanged;
574
+ sentences.push(labels.spanSentence({
575
+ word,
576
+ series: span.seriesLabel,
577
+ primary: flat ? "" : stripSign(span.delta.text),
578
+ secondary: flat || !span.delta.secondaryText ? null : stripSign(span.delta.secondaryText),
579
+ range: span.rangeText,
580
+ from: formatters.formatValue(span.from.y),
581
+ to: formatters.formatValue(span.to.y)
582
+ }));
583
+ if (span.peak && span.trough) sentences.push(labels.extremesSentence({
584
+ peak: formatters.formatValue(span.peak.y),
585
+ peakAt: formatters.formatX(span.peak.x),
586
+ trough: formatters.formatValue(span.trough.y),
587
+ troughAt: formatters.formatX(span.trough.x)
588
+ }));
589
+ }
590
+ if (visible.length > 0) {
591
+ const list = visible.map((info) => `${info.label} ${formatters.formatValue(info.latest?.y)}`).join(", ");
592
+ sentences.push(labels.seriesSummary(visible.length, list));
593
+ }
594
+ if (summary && summary.folded > 0) sentences.push(labels.foldedSummary(summary.folded));
595
+ if (bands.length > 0) {
596
+ const list = bands.map((band) => band.label ?? formatters.formatXRange(band.start, band.end)).join(", ");
597
+ sentences.push(labels.bandsSummary(list));
598
+ }
599
+ if (targetText) sentences.push(`${labels.target} ${targetText}.`);
600
+ if (asOfText) sentences.push(`${labels.asOf} ${asOfText}.`);
601
+ return sentences.join(" ");
602
+ }
603
+ /** The plot's own `description` — the `aria-label` Vega stamps on the `graphics-document`. */
604
+ function buildTrendChartDescription({ name, mark, summary, visibleKeys, focusLabel, stack, normalize, formatters, targetText, labels }) {
605
+ const shown = new Set(visibleKeys);
606
+ const visible = summary.series.filter((info) => shown.has(info.key));
607
+ const first = summary.periods[0];
608
+ const last = summary.periods[summary.periods.length - 1];
609
+ return labels.chartDescription({
610
+ mark: labels.markNames[mark],
611
+ name,
612
+ seriesCount: visible.length,
613
+ periodCount: summary.periods.length,
614
+ range: first != null && last != null ? formatters.formatXRange(first, last) : "—",
615
+ min: formatters.formatPlotted(summary.min),
616
+ max: formatters.formatPlotted(summary.max),
617
+ focus: focusLabel,
618
+ target: targetText ?? null,
619
+ stacked: stack === "normalize" ? labels.normalized : normalize === "index" && summary.periods.length > 0 ? labels.indexedAt(formatters.formatX(summary.periods[0])) : null
620
+ });
621
+ }
622
+ //#endregion
623
+ export { DEFAULT_TREND_LABELS, TREND_OTHER_KEY, bandAt, buildLatestRows, buildPeriodRows, buildTrendChartDescription, buildTrendDescription, buildTrendInsight, buildTrendTooltip, createTrendFormatters, deriveTrendDelta, deriveTrendSpan, deriveTrendState, normalizeSeries, resolveBands, resolveSeriesColorMode, resolveTrendLabels, seriesColorAt, trendAxisGranularity };
@@ -0,0 +1,4 @@
1
+ import { BuildTrendDescriptionInput, DEFAULT_TREND_LABELS, DeriveTrendDeltaOptions, DeriveTrendSpanOptions, DeriveTrendStateInput, NormalizeSeriesOptions, TREND_OTHER_KEY, TrendBand, TrendBandRow, TrendCardLabels, TrendCardSize, TrendCardState, TrendCardStatus, TrendDelta, TrendDeltaExplanation, TrendDeltaMode, TrendGoodDirection, TrendHeadline, TrendInsightSegment, TrendMark, TrendNormalize, TrendPeriodRow, TrendPoint, TrendPolarity, TrendRow, TrendScale, TrendSeries, TrendSeriesColor, TrendSeriesColorMode, TrendSeriesInfo, TrendSpan, TrendStack, TrendSummary, TrendTextFormatters, TrendTooltipEntry, TrendTooltipModel, TrendXType, bandAt, buildLatestRows, buildPeriodRows, buildTrendChartDescription, buildTrendDescription, buildTrendInsight, buildTrendTooltip, createTrendFormatters, deriveTrendDelta, deriveTrendSpan, deriveTrendState, normalizeSeries, resolveBands, resolveSeriesColorMode, resolveTrendLabels, seriesColorAt, trendAxisGranularity } from "./TrendCard.model.js";
2
+ import { TrendCard, TrendCardProps, TrendLegendMode } from "./TrendCard.js";
3
+ import { TREND_BANDS_DATASET, TREND_HOVER_PARAM, TREND_LATEST_DATASET, TREND_PERIODS_DATASET, TREND_REFERENCE_DATASET, TREND_TABLE_DATASET, TREND_X_TICKS_PARAM, TrendPlotSpecOptions, buildTrendPlotSpec } from "./TrendCard.chart.js";
4
+ export { type BuildTrendDescriptionInput, DEFAULT_TREND_LABELS, type DeriveTrendDeltaOptions, type DeriveTrendSpanOptions, type DeriveTrendStateInput, type NormalizeSeriesOptions, TREND_BANDS_DATASET, TREND_HOVER_PARAM, TREND_LATEST_DATASET, TREND_OTHER_KEY, TREND_PERIODS_DATASET, TREND_REFERENCE_DATASET, TREND_TABLE_DATASET, TREND_X_TICKS_PARAM, type TrendBand, type TrendBandRow, TrendCard, type TrendCardLabels, type TrendCardProps, type TrendCardSize, type TrendCardState, type TrendCardStatus, type TrendDelta, type TrendDeltaExplanation, type TrendDeltaMode, type TrendGoodDirection, type TrendHeadline, type TrendInsightSegment, type TrendLegendMode, type TrendMark, type TrendNormalize, type TrendPeriodRow, type TrendPlotSpecOptions, type TrendPoint, type TrendPolarity, type TrendRow, type TrendScale, type TrendSeries, type TrendSeriesColor, type TrendSeriesColorMode, type TrendSeriesInfo, type TrendSpan, type TrendStack, type TrendSummary, type TrendTextFormatters, type TrendTooltipEntry, type TrendTooltipModel, type TrendXType, bandAt, buildLatestRows, buildPeriodRows, buildTrendChartDescription, buildTrendDescription, buildTrendInsight, buildTrendPlotSpec, buildTrendTooltip, createTrendFormatters, deriveTrendDelta, deriveTrendSpan, deriveTrendState, normalizeSeries, resolveBands, resolveSeriesColorMode, resolveTrendLabels, seriesColorAt, trendAxisGranularity };
@@ -0,0 +1,4 @@
1
+ import { TREND_BANDS_DATASET, TREND_HOVER_PARAM, TREND_LATEST_DATASET, TREND_PERIODS_DATASET, TREND_REFERENCE_DATASET, TREND_TABLE_DATASET, TREND_X_TICKS_PARAM, buildTrendPlotSpec } from "./TrendCard.chart.js";
2
+ import { DEFAULT_TREND_LABELS, TREND_OTHER_KEY, bandAt, buildLatestRows, buildPeriodRows, buildTrendChartDescription, buildTrendDescription, buildTrendInsight, buildTrendTooltip, createTrendFormatters, deriveTrendDelta, deriveTrendSpan, deriveTrendState, normalizeSeries, resolveBands, resolveSeriesColorMode, resolveTrendLabels, seriesColorAt, trendAxisGranularity } from "./TrendCard.model.js";
3
+ import { TrendCard } from "./TrendCard.js";
4
+ export { DEFAULT_TREND_LABELS, TREND_BANDS_DATASET, TREND_HOVER_PARAM, TREND_LATEST_DATASET, TREND_OTHER_KEY, TREND_PERIODS_DATASET, TREND_REFERENCE_DATASET, TREND_TABLE_DATASET, TREND_X_TICKS_PARAM, TrendCard, bandAt, buildLatestRows, buildPeriodRows, buildTrendChartDescription, buildTrendDescription, buildTrendInsight, buildTrendPlotSpec, buildTrendTooltip, createTrendFormatters, deriveTrendDelta, deriveTrendSpan, deriveTrendState, normalizeSeries, resolveBands, resolveSeriesColorMode, resolveTrendLabels, seriesColorAt, trendAxisGranularity };