@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,554 @@
1
+ import { formatDate, formatDateRange, formatDelta, formatNumber } from "../../lib/vega/formatters.js";
2
+ //#region src/components/ForecastCard/ForecastCard.model.ts
3
+ const DEFAULT_FORECAST_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 forecast",
12
+ retry: "Try again",
13
+ insufficient: "Not enough data for a forecast",
14
+ noForecast: "No forecast for this period — measured readings only",
15
+ stale: "Stale",
16
+ asOf: "As of",
17
+ actual: "Actual",
18
+ forecast: "Forecast",
19
+ interval: "Interval",
20
+ target: "Target",
21
+ boundary: "Forecast starts",
22
+ tablePeriod: "Period",
23
+ tableValue: "Value",
24
+ tableKind: "Kind",
25
+ tableInterval: "Interval",
26
+ headlineSentence: ({ name, value, caption }) => `${name}: ${value} (${caption}).`,
27
+ landingCaption: (period) => `Projected for ${period}`,
28
+ latestCaption: (period) => `Actual, ${period}`,
29
+ intervalCaption: (range, level) => level ? `${range} (${level})` : range,
30
+ intervalKey: (level) => level ? `${level} interval` : "Interval",
31
+ levelText: (level) => `${Math.round(level * 100)}%`,
32
+ outlook: {
33
+ likely: "Likely to hit target",
34
+ uncertain: "Too close to call",
35
+ unlikely: "Unlikely to hit target"
36
+ },
37
+ outlookExplanation: ({ outlook, target, landing, range, horizon }) => {
38
+ const head = `Projected ${landing} by ${horizon} against a ${target} target`;
39
+ if (outlook === "uncertain") return `${head}. The target falls inside the ${range} interval, so the forecast does not decide it.`;
40
+ if (outlook === "likely") return range ? `${head}. The whole ${range} interval reaches it.` : `${head}.`;
41
+ return range ? `${head}. No part of the ${range} interval reaches it.` : `${head}.`;
42
+ },
43
+ crossesAt: (period) => `Reaches the target in ${period}`,
44
+ noCrossing: "Does not reach the target inside the horizon",
45
+ showTable: (name) => `Show the data behind ${name}`,
46
+ hideTable: (name) => `Hide the data behind ${name}`,
47
+ about: (name) => `About ${name}`,
48
+ loading: (name) => `Loading ${name}`,
49
+ tableCaption: (name) => `Data behind the ${name} forecast`,
50
+ changeGroup: (name) => `${name} projected change`,
51
+ deltaExplanation: ({ fromLabel, fromText, toLabel, toText, changeText, secondaryText, renderedMode, direction, rangeText }) => {
52
+ const rows = [
53
+ [fromLabel, fromText],
54
+ [toLabel, toText],
55
+ [direction === "up" ? "Increase" : direction === "down" ? "Decrease" : "No change", changeText]
56
+ ];
57
+ if (secondaryText) rows.push([renderedMode === "percent" ? "Change" : "Change %", secondaryText]);
58
+ if (rangeText) rows.push(["Interval", rangeText]);
59
+ return rows;
60
+ },
61
+ projectionSentence: ({ word, primary, secondary, from, to, horizon }) => primary ? `Projected ${word.toLowerCase()} ${primary}${secondary ? ` (${secondary})` : ""} from ${from} to ${to} by ${horizon}.` : `Projected to hold at ${from} through ${horizon}.`,
62
+ intervalSentence: ({ low, high, level, horizon }) => `Between ${low} and ${high} by ${horizon}${level ? `, ${level} interval` : ""}.`,
63
+ historySentence: ({ count, range, latest }) => `Based on ${count} measured periods, ${range}, latest ${latest}.`,
64
+ partialIntervalSentence: ({ with: withInterval, total }) => `${withInterval} of ${total} projected periods carry an interval.`,
65
+ markNames: {
66
+ line: "Line",
67
+ area: "Area"
68
+ },
69
+ chartDescription: ({ mark, name, actualCount, forecastCount, range, min, max, boundary, interval, target }) => `${mark} chart of ${name}: ${actualCount} measured periods followed by ${forecastCount} projected, ${range}, ranging ${min} to ${max}.` + (boundary ? ` The forecast starts after ${boundary}.` : "") + (interval ? ` ${interval}.` : "") + (target ? ` Reference line at the ${target} target.` : ""),
70
+ insightMark: "Insight",
71
+ insight: ({ name, direction, from, to, change, horizon, low, high, level, outlook, target, crossesAt }) => {
72
+ const segments = direction === "flat" ? [
73
+ `${name} is projected to hold at `,
74
+ { value: to },
75
+ ` through ${horizon}.`
76
+ ] : [
77
+ `${name} is projected to ${direction === "up" ? "reach" : "fall to"} `,
78
+ { value: to },
79
+ ` by ${horizon}, ${direction === "up" ? "up" : "down"} `,
80
+ { value: change },
81
+ ` from `,
82
+ { value: from },
83
+ "."
84
+ ];
85
+ if (low && high) segments.push(` The ${level ? `${level} ` : ""}interval runs `, { value: low }, " to ", { value: high }, ".");
86
+ if (target && outlook !== "none") {
87
+ if (outlook === "uncertain") segments.push(` The ${target} target sits inside that range, so it is too close to call.`);
88
+ else if (crossesAt) segments.push(` It reaches the ${target} target in ${crossesAt}.`);
89
+ else if (outlook === "likely") segments.push(` That clears the ${target} target.`);
90
+ else segments.push(` That misses the ${target} target.`);
91
+ }
92
+ return segments;
93
+ }
94
+ };
95
+ function resolveForecastLabels(overrides) {
96
+ return overrides ? {
97
+ ...DEFAULT_FORECAST_LABELS,
98
+ ...overrides
99
+ } : DEFAULT_FORECAST_LABELS;
100
+ }
101
+ /**
102
+ * The state machine. Error wins; loading over existing readings is a refresh
103
+ * (the previous render stays under the veil); no readings is `no-match` when
104
+ * the consumer's filter caused it and `empty` otherwise.
105
+ */
106
+ function deriveForecastState({ status, hasValue, hasData, filtered }) {
107
+ if (status === "error") return "error";
108
+ if (status === "loading") return hasData ? "refreshing" : "loading";
109
+ if (hasValue) return "ready";
110
+ return filtered ? "no-match" : "empty";
111
+ }
112
+ const isFinite = (value) => typeof value === "number" && Number.isFinite(value);
113
+ const DAY_MS = 864e5;
114
+ const ISO_DATE = /^\d{4}-\d{2}(-\d{2})?([T ].*)?$/;
115
+ function inferXType(points) {
116
+ const sample = points.find((point) => point.x != null);
117
+ if (!sample) return "ordinal";
118
+ const { x } = sample;
119
+ if (x instanceof Date) return "temporal";
120
+ if (typeof x === "number") return "quantitative";
121
+ return ISO_DATE.test(x) ? "temporal" : "ordinal";
122
+ }
123
+ function inferGranularity(periods) {
124
+ const xs = periods.filter((x) => typeof x === "number");
125
+ if (xs.length < 2) return "day";
126
+ const gaps = [];
127
+ for (let index = 1; index < xs.length; index += 1) gaps.push(Math.abs(xs[index] - xs[index - 1]));
128
+ gaps.sort((a, b) => a - b);
129
+ const median = gaps[Math.floor(gaps.length / 2)];
130
+ if (median < DAY_MS) return "time";
131
+ if (median >= 360 * DAY_MS) return "year";
132
+ if (median >= 27 * DAY_MS) return "month";
133
+ return "day";
134
+ }
135
+ /**
136
+ * The granularity an x axis labels its ticks at: the tooltip and the table keep
137
+ * the full form (`Sep 8, 2026`); an axis has room for `Sep 8`. Duplicated per
138
+ * component rather than shared, so a card's text grammar is readable in one
139
+ * file — the same reason each card owns its own state machine.
140
+ */
141
+ function forecastAxisGranularity(granularity) {
142
+ switch (granularity) {
143
+ case "day": return "day-short";
144
+ case "month": return "month-short";
145
+ case "datetime": return "time";
146
+ default: return granularity;
147
+ }
148
+ }
149
+ function normalizeX(x, xType) {
150
+ if (xType === "temporal") {
151
+ const time = x instanceof Date ? x.getTime() : new Date(x).getTime();
152
+ return Number.isNaN(time) ? null : time;
153
+ }
154
+ if (xType === "quantitative") return isFinite(x) ? x : null;
155
+ return String(x);
156
+ }
157
+ /**
158
+ * Lays the measured and the projected readings onto one period domain.
159
+ *
160
+ * The two halves stay distinguishable at every level: each period carries its
161
+ * `kind`, the projected half is the only one with interval bounds, and the plot
162
+ * rows repeat the last measured reading as the projected path's ANCHOR so the
163
+ * solid and dashed paths meet — the interval opens from that anchor at zero
164
+ * width, which is what makes a fan read as growing uncertainty rather than as a
165
+ * band that appears from nowhere. `readings` carries the anchor once, so the
166
+ * table and the description never double-count it.
167
+ *
168
+ * A projected period that repeats a measured one is dropped: a forecast cannot
169
+ * overwrite a reading. Returns `null` when there is nothing at all to show.
170
+ */
171
+ function normalizeForecast(actual, forecast, { granularity, name, color }) {
172
+ const actualPoints = actual ?? [];
173
+ const forecastPoints = forecast ?? [];
174
+ if (actualPoints.length === 0 && forecastPoints.length === 0) return null;
175
+ const xType = inferXType([...actualPoints, ...forecastPoints]);
176
+ const measured = /* @__PURE__ */ new Map();
177
+ for (const point of actualPoints) {
178
+ const x = normalizeX(point.x, xType);
179
+ if (x == null) continue;
180
+ measured.set(String(x), {
181
+ x,
182
+ y: isFinite(point.y) ? point.y : null,
183
+ lower: null,
184
+ upper: null,
185
+ kind: "actual"
186
+ });
187
+ }
188
+ const projected = /* @__PURE__ */ new Map();
189
+ for (const point of forecastPoints) {
190
+ const x = normalizeX(point.x, xType);
191
+ if (x == null) continue;
192
+ if (measured.has(String(x))) continue;
193
+ const lower = isFinite(point.lower) ? point.lower : null;
194
+ const upper = isFinite(point.upper) ? point.upper : null;
195
+ const hasInterval = lower != null && upper != null;
196
+ projected.set(String(x), {
197
+ x,
198
+ y: isFinite(point.y) ? point.y : null,
199
+ lower: hasInterval ? Math.min(lower, upper) : null,
200
+ upper: hasInterval ? Math.max(lower, upper) : null,
201
+ kind: "forecast"
202
+ });
203
+ }
204
+ const sortReadings = (entries) => xType === "ordinal" ? entries : [...entries].sort((a, b) => a.x - b.x);
205
+ const measuredReadings = sortReadings([...measured.values()]);
206
+ const projectedReadings = sortReadings([...projected.values()]);
207
+ const readings = [...measuredReadings, ...projectedReadings];
208
+ const periods = readings.map((reading) => reading.x);
209
+ const finiteMeasured = measuredReadings.filter((reading) => reading.y != null);
210
+ const finiteProjected = projectedReadings.filter((reading) => reading.y != null);
211
+ const lastMeasured = finiteMeasured[finiteMeasured.length - 1] ?? null;
212
+ const lastProjected = finiteProjected[finiteProjected.length - 1] ?? null;
213
+ const rows = readings.map((reading) => ({
214
+ x: reading.x,
215
+ y: reading.y,
216
+ lo: reading.lower,
217
+ hi: reading.upper,
218
+ k: reading.kind,
219
+ c: color,
220
+ n: name
221
+ }));
222
+ const anchor = lastMeasured && finiteProjected.length > 0 ? lastMeasured : null;
223
+ if (anchor) {
224
+ const at = rows.findIndex((row) => String(row.x) === String(anchor.x) && row.k === "actual");
225
+ const anchorRow = {
226
+ x: anchor.x,
227
+ y: anchor.y,
228
+ lo: anchor.y,
229
+ hi: anchor.y,
230
+ k: "forecast",
231
+ c: color,
232
+ n: name
233
+ };
234
+ rows.splice(at + 1, 0, anchorRow);
235
+ }
236
+ const interval = [];
237
+ if (anchor) interval.push({
238
+ x: anchor.x,
239
+ lo: anchor.y,
240
+ hi: anchor.y
241
+ });
242
+ for (const reading of projectedReadings) {
243
+ if (reading.lower == null || reading.upper == null) continue;
244
+ interval.push({
245
+ x: reading.x,
246
+ lo: reading.lower,
247
+ hi: reading.upper
248
+ });
249
+ }
250
+ const extent = [];
251
+ for (const reading of readings) {
252
+ if (reading.y != null) extent.push(reading.y);
253
+ if (reading.lower != null) extent.push(reading.lower);
254
+ if (reading.upper != null) extent.push(reading.upper);
255
+ }
256
+ const moment = (reading) => reading && reading.y != null ? {
257
+ x: reading.x,
258
+ y: reading.y,
259
+ lower: reading.lower,
260
+ upper: reading.upper
261
+ } : null;
262
+ return {
263
+ xType,
264
+ granularity: granularity ?? (xType === "temporal" ? inferGranularity(periods) : "day"),
265
+ periods,
266
+ readings,
267
+ rows,
268
+ interval,
269
+ boundary: lastMeasured?.x ?? null,
270
+ latest: moment(lastMeasured),
271
+ landing: moment(lastProjected),
272
+ actualCount: finiteMeasured.length,
273
+ forecastCount: finiteProjected.length,
274
+ intervalCount: projectedReadings.filter((reading) => reading.lower != null && reading.upper != null).length,
275
+ min: extent.length > 0 ? Math.min(...extent) : null,
276
+ max: extent.length > 0 ? Math.max(...extent) : null,
277
+ hasNegative: extent.some((value) => value < 0)
278
+ };
279
+ }
280
+ /** The periods the crosshair reads — one row each, so it lands on the period rather than on a mark. */
281
+ function buildForecastPeriodRows(summary) {
282
+ return summary.periods.map((x) => ({ x }));
283
+ }
284
+ /**
285
+ * The projected stretch as one washed rect, from the last measured period to
286
+ * the horizon — the ground under everything the card did not measure. Empty
287
+ * when nothing is projected, so a card with history alone draws no wash.
288
+ */
289
+ function buildForecastRegionRows(summary) {
290
+ const projected = summary.readings.filter((reading) => reading.kind === "forecast");
291
+ if (projected.length === 0) return [];
292
+ return [{
293
+ start: summary.boundary ?? projected[0].x,
294
+ end: projected[projected.length - 1].x
295
+ }];
296
+ }
297
+ /** Zero or one row — the rule at the last measured period. */
298
+ function buildForecastBoundaryRows(summary) {
299
+ const hasProjection = summary.readings.some((reading) => reading.kind === "forecast");
300
+ return summary.boundary != null && hasProjection ? [{ x: summary.boundary }] : [];
301
+ }
302
+ /** Zero or one row — where the landing label sits. */
303
+ function buildForecastLandingRows(summary, text) {
304
+ if (!summary.landing) return [];
305
+ return [{
306
+ x: summary.landing.x,
307
+ y: summary.landing.y,
308
+ n: text,
309
+ c: summary.rows[0]?.c ?? ""
310
+ }];
311
+ }
312
+ /** Whether a value reaches the target, given which direction is good. `null` when reaching is undefined. */
313
+ function reachesTarget(value, target, goodDirection) {
314
+ if (goodDirection === "none") return null;
315
+ return goodDirection === "down" ? value <= target : value >= target;
316
+ }
317
+ /**
318
+ * The verdict the interval supports, and nothing more: `likely` when the WHOLE
319
+ * interval reaches the target, `unlikely` when none of it does, `uncertain`
320
+ * when the target falls inside it — the honest answer a point estimate cannot
321
+ * give. Without an interval the point estimate decides between `likely` and
322
+ * `unlikely` (the card says so in its explanation); without a target, a landing
323
+ * or a direction, there is no verdict at all.
324
+ */
325
+ function deriveForecastOutlook({ landing, target, goodDirection }) {
326
+ if (!landing || target == null || !isFinite(target)) return "none";
327
+ const point = reachesTarget(landing.y, target, goodDirection);
328
+ if (point == null) return "none";
329
+ if (landing.lower == null || landing.upper == null) return point ? "likely" : "unlikely";
330
+ const lowReaches = reachesTarget(landing.lower, target, goodDirection);
331
+ if (lowReaches === reachesTarget(landing.upper, target, goodDirection)) return lowReaches ? "likely" : "unlikely";
332
+ return "uncertain";
333
+ }
334
+ /**
335
+ * The first projected period whose reading reaches the target — the "when" a
336
+ * forecast is really asked for. `null` when the last measured reading already
337
+ * reaches it (there is nothing to wait for), when the path never gets there, or
338
+ * when reaching is undefined.
339
+ */
340
+ function deriveForecastCrossing(summary, target, goodDirection) {
341
+ if (!summary || target == null || !isFinite(target) || goodDirection === "none") return null;
342
+ if (summary.latest && reachesTarget(summary.latest.y, target, goodDirection)) return null;
343
+ for (const reading of summary.readings) {
344
+ if (reading.kind !== "forecast" || reading.y == null) continue;
345
+ if (reachesTarget(reading.y, target, goodDirection)) return {
346
+ x: reading.x,
347
+ y: reading.y,
348
+ lower: reading.lower,
349
+ upper: reading.upper
350
+ };
351
+ }
352
+ return null;
353
+ }
354
+ /** A change between two readings, with polarity resolved through `goodDirection`. */
355
+ function deriveForecastDelta(value, previous, { deltaMode, goodDirection, format, locale, percentagePoints }) {
356
+ if (!isFinite(value) || !isFinite(previous)) return null;
357
+ const delta = formatDelta(value, previous, {
358
+ mode: deltaMode,
359
+ format,
360
+ locale,
361
+ pointsSuffix: percentagePoints
362
+ });
363
+ const other = formatDelta(value, previous, {
364
+ mode: delta.renderedMode === "percent" ? "absolute" : "percent",
365
+ format,
366
+ locale,
367
+ pointsSuffix: percentagePoints
368
+ });
369
+ let polarity = "neutral";
370
+ if (delta.direction !== "flat" && goodDirection !== "none") polarity = delta.direction === "up" === (goodDirection === "up") ? "positive" : "negative";
371
+ return {
372
+ direction: delta.direction,
373
+ polarity,
374
+ text: delta.text,
375
+ renderedMode: delta.renderedMode,
376
+ secondaryText: other.renderedMode === delta.renderedMode ? null : other.text
377
+ };
378
+ }
379
+ /**
380
+ * The move from the last measured reading to the landing. `null` unless the
381
+ * card has both — a forecast with no history to leave from, or a history with
382
+ * nothing projected, has no projected change to state.
383
+ */
384
+ function deriveForecastProjection(summary, { formatters, ...deltaOptions }) {
385
+ if (!summary?.latest || !summary.landing) return null;
386
+ const delta = deriveForecastDelta(summary.landing.y, summary.latest.y, deltaOptions);
387
+ if (!delta) return null;
388
+ return {
389
+ from: summary.latest,
390
+ to: summary.landing,
391
+ rangeText: formatters.formatXRange(summary.latest.x, summary.landing.x),
392
+ delta
393
+ };
394
+ }
395
+ /** The formatters every text surface shares — headline, badge, table, tooltip, descriptions. */
396
+ function createForecastFormatters({ format, locale, timeZone, formatValue, xType, granularity }) {
397
+ const formatX = (x) => {
398
+ if (xType === "temporal" && typeof x === "number") return formatDate(x, {
399
+ granularity,
400
+ locale,
401
+ timeZone
402
+ });
403
+ if (xType === "quantitative" && typeof x === "number") return formatNumber(x, "number", locale);
404
+ return String(x);
405
+ };
406
+ const value = (input) => isFinite(input) ? formatValue ? formatValue(input) : formatNumber(input, format, locale) : "—";
407
+ return {
408
+ formatValue: value,
409
+ formatX,
410
+ formatXRange: (start, end) => xType === "temporal" && typeof start === "number" && typeof end === "number" ? formatDateRange(start, end, {
411
+ granularity,
412
+ locale,
413
+ timeZone
414
+ }) : `${formatX(start)} – ${formatX(end)}`,
415
+ formatInterval: (lower, upper) => isFinite(lower) && isFinite(upper) ? `${value(lower)} – ${value(upper)}` : null
416
+ };
417
+ }
418
+ const stripSign = (text) => text.replace(/^[+\-−]/, "");
419
+ /**
420
+ * The hover chip's contents for one period — pure, so the reading is
421
+ * unit-tested rather than only reachable through a pointer event (Vega's own
422
+ * hit-testing needs a visible document, which a hidden frame does not give).
423
+ * Returns `null` for a period the card does not have.
424
+ */
425
+ function buildForecastTooltip({ summary, x, target, formatters, labels }) {
426
+ if (x == null || !summary) return null;
427
+ const reading = summary.readings.find((entry) => String(entry.x) === String(x));
428
+ if (!reading) return null;
429
+ return {
430
+ periodText: formatters.formatX(reading.x),
431
+ kind: reading.kind,
432
+ kindText: reading.kind === "actual" ? labels.actual : labels.forecast,
433
+ valueText: formatters.formatValue(reading.y),
434
+ intervalText: formatters.formatInterval(reading.lower, reading.upper),
435
+ targetText: target != null ? formatters.formatValue(target) : null
436
+ };
437
+ }
438
+ /**
439
+ * The generated insight — what the card shows beside the AI mark when the
440
+ * consumer passes no `insight`: where the measure lands, the move that takes it
441
+ * there, the interval around it, and what that means for the target. Values
442
+ * come back as `{ value }` segments so the card can set them in bold.
443
+ */
444
+ function buildForecastInsight({ name, summary, projection, outlook, crossing, target, level, formatters, labels }) {
445
+ const landing = summary.landing;
446
+ if (!landing || !projection) return null;
447
+ return labels.insight({
448
+ name,
449
+ direction: projection.delta.direction,
450
+ from: formatters.formatValue(projection.from.y),
451
+ to: formatters.formatValue(landing.y),
452
+ change: stripSign(projection.delta.text),
453
+ horizon: formatters.formatX(landing.x),
454
+ low: landing.lower != null ? formatters.formatValue(landing.lower) : null,
455
+ high: landing.upper != null ? formatters.formatValue(landing.upper) : null,
456
+ level: level != null ? labels.levelText(level) : null,
457
+ outlook,
458
+ target: target != null ? formatters.formatValue(target) : null,
459
+ crossesAt: crossing ? formatters.formatX(crossing.x) : null
460
+ });
461
+ }
462
+ /**
463
+ * The card's text equivalent (Amy Cesal's alt-text formula: what it is, what it
464
+ * shows, why it is here, when it was true) — the figure's accessible
465
+ * description, and the summary a screen reader gets before the parts. The
466
+ * landing, its interval, the measured span behind it and the verdict are all
467
+ * here, so nothing the plot says lives only in the marks.
468
+ */
469
+ function buildForecastDescription({ name, state, headline, summary, projection, outlook, crossing, target, level, formatters, asOfText, labels }) {
470
+ if (state === "loading") return `${labels.loading(name)}.`;
471
+ if (state === "error") return `${name}: ${labels.error}.`;
472
+ if (state === "empty") return `${name}: ${labels.noData}.`;
473
+ if (state === "no-match") return `${name}: ${labels.noMatch}.`;
474
+ const sentences = [];
475
+ const landing = summary?.landing ?? null;
476
+ const latest = summary?.latest ?? null;
477
+ const levelText = level != null ? labels.levelText(level) : null;
478
+ if (headline === "landing" && landing) sentences.push(labels.headlineSentence({
479
+ name,
480
+ value: formatters.formatValue(landing.y),
481
+ caption: labels.landingCaption(formatters.formatX(landing.x))
482
+ }));
483
+ else if (headline === "latest" && latest) sentences.push(labels.headlineSentence({
484
+ name,
485
+ value: formatters.formatValue(latest.y),
486
+ caption: labels.latestCaption(formatters.formatX(latest.x))
487
+ }));
488
+ else if (headline === "change" && projection) sentences.push(`${name}: ${projection.delta.text} projected over ${projection.rangeText}.`);
489
+ else sentences.push(`${name}.`);
490
+ if (projection) {
491
+ const flat = projection.delta.direction === "flat";
492
+ const word = projection.delta.direction === "up" ? labels.increase : projection.delta.direction === "down" ? labels.decrease : labels.unchanged;
493
+ sentences.push(labels.projectionSentence({
494
+ word,
495
+ primary: flat ? "" : stripSign(projection.delta.text),
496
+ secondary: flat || !projection.delta.secondaryText ? null : stripSign(projection.delta.secondaryText),
497
+ from: formatters.formatValue(projection.from.y),
498
+ to: formatters.formatValue(projection.to.y),
499
+ horizon: formatters.formatX(projection.to.x)
500
+ }));
501
+ }
502
+ if (landing?.lower != null && landing.upper != null) sentences.push(labels.intervalSentence({
503
+ low: formatters.formatValue(landing.lower),
504
+ high: formatters.formatValue(landing.upper),
505
+ level: levelText,
506
+ horizon: formatters.formatX(landing.x)
507
+ }));
508
+ if (summary && summary.actualCount > 0) {
509
+ const measured = summary.readings.filter((reading) => reading.kind === "actual");
510
+ const first = measured[0];
511
+ const last = measured[measured.length - 1];
512
+ sentences.push(labels.historySentence({
513
+ count: summary.actualCount,
514
+ range: first && last ? formatters.formatXRange(first.x, last.x) : "—",
515
+ latest: formatters.formatValue(latest?.y)
516
+ }));
517
+ }
518
+ if (summary && summary.forecastCount > 0 && summary.intervalCount > 0 && summary.intervalCount < summary.forecastCount) sentences.push(labels.partialIntervalSentence({
519
+ with: summary.intervalCount,
520
+ total: summary.forecastCount
521
+ }));
522
+ if (target != null) {
523
+ sentences.push(`${labels.target} ${formatters.formatValue(target)}.`);
524
+ if (outlook !== "none") sentences.push(`${labels.outlook[outlook]}.` + (crossing ? ` ${labels.crossesAt(formatters.formatX(crossing.x))}.` : outlook === "unlikely" ? ` ${labels.noCrossing}.` : ""));
525
+ }
526
+ if (asOfText) sentences.push(`${labels.asOf} ${asOfText}.`);
527
+ return sentences.join(" ");
528
+ }
529
+ /** The plot's own `description` — the `aria-label` Vega stamps on the `graphics-document`. */
530
+ function buildForecastChartDescription({ name, mark, summary, level, formatters, targetText, labels }) {
531
+ const first = summary.periods[0];
532
+ const last = summary.periods[summary.periods.length - 1];
533
+ const landing = summary.landing;
534
+ const intervalText = summary.intervalCount > 0 && landing?.lower != null && landing.upper != null ? labels.intervalSentence({
535
+ low: formatters.formatValue(landing.lower),
536
+ high: formatters.formatValue(landing.upper),
537
+ level: level != null ? labels.levelText(level) : null,
538
+ horizon: formatters.formatX(landing.x)
539
+ }).replace(/\.$/, "") : null;
540
+ return labels.chartDescription({
541
+ mark: labels.markNames[mark],
542
+ name,
543
+ actualCount: summary.actualCount,
544
+ forecastCount: summary.forecastCount,
545
+ range: first != null && last != null ? formatters.formatXRange(first, last) : "—",
546
+ min: formatters.formatValue(summary.min),
547
+ max: formatters.formatValue(summary.max),
548
+ boundary: summary.boundary != null && summary.forecastCount > 0 ? formatters.formatX(summary.boundary) : null,
549
+ interval: intervalText,
550
+ target: targetText ?? null
551
+ });
552
+ }
553
+ //#endregion
554
+ export { DEFAULT_FORECAST_LABELS, buildForecastBoundaryRows, buildForecastChartDescription, buildForecastDescription, buildForecastInsight, buildForecastLandingRows, buildForecastPeriodRows, buildForecastRegionRows, buildForecastTooltip, createForecastFormatters, deriveForecastCrossing, deriveForecastDelta, deriveForecastOutlook, deriveForecastProjection, deriveForecastState, forecastAxisGranularity, normalizeForecast, reachesTarget, resolveForecastLabels };
@@ -0,0 +1,4 @@
1
+ import { BuildForecastDescriptionInput, BuildForecastInsightInput, DEFAULT_FORECAST_LABELS, DeriveForecastDeltaOptions, DeriveForecastOutlookInput, DeriveForecastProjectionOptions, DeriveForecastStateInput, ForecastCardLabels, ForecastCardSize, ForecastCardState, ForecastCardStatus, ForecastDelta, ForecastDeltaExplanation, ForecastDeltaMode, ForecastGoodDirection, ForecastHeadline, ForecastInsightSegment, ForecastKind, ForecastMark, ForecastMoment, ForecastOutlook, ForecastPeriodRow, ForecastPoint, ForecastPolarity, ForecastProjection, ForecastReading, ForecastRegionRow, ForecastRow, ForecastScale, ForecastSummary, ForecastTextFormatters, ForecastTooltipModel, ForecastXType, NormalizeForecastOptions, buildForecastBoundaryRows, buildForecastChartDescription, buildForecastDescription, buildForecastInsight, buildForecastLandingRows, buildForecastPeriodRows, buildForecastRegionRows, buildForecastTooltip, createForecastFormatters, deriveForecastCrossing, deriveForecastDelta, deriveForecastOutlook, deriveForecastProjection, deriveForecastState, forecastAxisGranularity, normalizeForecast, reachesTarget, resolveForecastLabels } from "./ForecastCard.model.js";
2
+ import { ForecastCard, ForecastCardProps } from "./ForecastCard.js";
3
+ import { FORECAST_BOUNDARY_DATASET, FORECAST_HOVER_PARAM, FORECAST_INTERVAL_DATASET, FORECAST_LANDING_DATASET, FORECAST_PERIODS_DATASET, FORECAST_REFERENCE_DATASET, FORECAST_REGION_DATASET, FORECAST_REGION_END_PARAM, FORECAST_REGION_START_PARAM, FORECAST_TABLE_DATASET, FORECAST_X_TICKS_PARAM, ForecastPlotSpecOptions, buildForecastPlotSpec } from "./ForecastCard.chart.js";
4
+ export { type BuildForecastDescriptionInput, type BuildForecastInsightInput, DEFAULT_FORECAST_LABELS, type DeriveForecastDeltaOptions, type DeriveForecastOutlookInput, type DeriveForecastProjectionOptions, type DeriveForecastStateInput, FORECAST_BOUNDARY_DATASET, FORECAST_HOVER_PARAM, FORECAST_INTERVAL_DATASET, FORECAST_LANDING_DATASET, FORECAST_PERIODS_DATASET, FORECAST_REFERENCE_DATASET, FORECAST_REGION_DATASET, FORECAST_REGION_END_PARAM, FORECAST_REGION_START_PARAM, FORECAST_TABLE_DATASET, FORECAST_X_TICKS_PARAM, ForecastCard, type ForecastCardLabels, type ForecastCardProps, type ForecastCardSize, type ForecastCardState, type ForecastCardStatus, type ForecastDelta, type ForecastDeltaExplanation, type ForecastDeltaMode, type ForecastGoodDirection, type ForecastHeadline, type ForecastInsightSegment, type ForecastKind, type ForecastMark, type ForecastMoment, type ForecastOutlook, type ForecastPeriodRow, type ForecastPlotSpecOptions, type ForecastPoint, type ForecastPolarity, type ForecastProjection, type ForecastReading, type ForecastRegionRow, type ForecastRow, type ForecastScale, type ForecastSummary, type ForecastTextFormatters, type ForecastTooltipModel, type ForecastXType, type NormalizeForecastOptions, buildForecastBoundaryRows, buildForecastChartDescription, buildForecastDescription, buildForecastInsight, buildForecastLandingRows, buildForecastPeriodRows, buildForecastPlotSpec, buildForecastRegionRows, buildForecastTooltip, createForecastFormatters, deriveForecastCrossing, deriveForecastDelta, deriveForecastOutlook, deriveForecastProjection, deriveForecastState, forecastAxisGranularity, normalizeForecast, reachesTarget, resolveForecastLabels };
@@ -0,0 +1,4 @@
1
+ import { FORECAST_BOUNDARY_DATASET, FORECAST_HOVER_PARAM, FORECAST_INTERVAL_DATASET, FORECAST_LANDING_DATASET, FORECAST_PERIODS_DATASET, FORECAST_REFERENCE_DATASET, FORECAST_REGION_DATASET, FORECAST_REGION_END_PARAM, FORECAST_REGION_START_PARAM, FORECAST_TABLE_DATASET, FORECAST_X_TICKS_PARAM, buildForecastPlotSpec } from "./ForecastCard.chart.js";
2
+ import { DEFAULT_FORECAST_LABELS, buildForecastBoundaryRows, buildForecastChartDescription, buildForecastDescription, buildForecastInsight, buildForecastLandingRows, buildForecastPeriodRows, buildForecastRegionRows, buildForecastTooltip, createForecastFormatters, deriveForecastCrossing, deriveForecastDelta, deriveForecastOutlook, deriveForecastProjection, deriveForecastState, forecastAxisGranularity, normalizeForecast, reachesTarget, resolveForecastLabels } from "./ForecastCard.model.js";
3
+ import { ForecastCard } from "./ForecastCard.js";
4
+ export { DEFAULT_FORECAST_LABELS, FORECAST_BOUNDARY_DATASET, FORECAST_HOVER_PARAM, FORECAST_INTERVAL_DATASET, FORECAST_LANDING_DATASET, FORECAST_PERIODS_DATASET, FORECAST_REFERENCE_DATASET, FORECAST_REGION_DATASET, FORECAST_REGION_END_PARAM, FORECAST_REGION_START_PARAM, FORECAST_TABLE_DATASET, FORECAST_X_TICKS_PARAM, ForecastCard, buildForecastBoundaryRows, buildForecastChartDescription, buildForecastDescription, buildForecastInsight, buildForecastLandingRows, buildForecastPeriodRows, buildForecastPlotSpec, buildForecastRegionRows, buildForecastTooltip, createForecastFormatters, deriveForecastCrossing, deriveForecastDelta, deriveForecastOutlook, deriveForecastProjection, deriveForecastState, forecastAxisGranularity, normalizeForecast, reachesTarget, resolveForecastLabels };