@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.
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/components/AnomalyCard/AnomalyCard.chart.d.ts +64 -0
- package/dist/components/AnomalyCard/AnomalyCard.chart.js +341 -0
- package/dist/components/AnomalyCard/AnomalyCard.d.ts +169 -0
- package/dist/components/AnomalyCard/AnomalyCard.js +1019 -0
- package/dist/components/AnomalyCard/AnomalyCard.model.d.ts +508 -0
- package/dist/components/AnomalyCard/AnomalyCard.model.js +553 -0
- package/dist/components/AnomalyCard/index.d.ts +4 -0
- package/dist/components/AnomalyCard/index.js +4 -0
- package/dist/components/ChartCard/ChartCard.chart.d.ts +72 -0
- package/dist/components/ChartCard/ChartCard.chart.js +1333 -0
- package/dist/components/ChartCard/ChartCard.d.ts +206 -0
- package/dist/components/ChartCard/ChartCard.js +718 -0
- package/dist/components/ChartCard/ChartCard.model.d.ts +452 -0
- package/dist/components/ChartCard/ChartCard.model.js +1223 -0
- package/dist/components/ChartCard/index.d.ts +4 -0
- package/dist/components/ChartCard/index.js +4 -0
- package/dist/components/ComparisonCard/ComparisonCard.chart.d.ts +80 -0
- package/dist/components/ComparisonCard/ComparisonCard.chart.js +337 -0
- package/dist/components/ComparisonCard/ComparisonCard.d.ts +200 -0
- package/dist/components/ComparisonCard/ComparisonCard.js +1325 -0
- package/dist/components/ComparisonCard/ComparisonCard.model.d.ts +309 -0
- package/dist/components/ComparisonCard/ComparisonCard.model.js +263 -0
- package/dist/components/ComparisonCard/index.d.ts +4 -0
- package/dist/components/ComparisonCard/index.js +4 -0
- package/dist/components/ForecastCard/ForecastCard.chart.d.ts +82 -0
- package/dist/components/ForecastCard/ForecastCard.chart.js +416 -0
- package/dist/components/ForecastCard/ForecastCard.d.ts +148 -0
- package/dist/components/ForecastCard/ForecastCard.js +1029 -0
- package/dist/components/ForecastCard/ForecastCard.model.d.ts +497 -0
- package/dist/components/ForecastCard/ForecastCard.model.js +554 -0
- package/dist/components/ForecastCard/index.d.ts +4 -0
- package/dist/components/ForecastCard/index.js +4 -0
- package/dist/components/HighlightCard/HighlightCard.chart.d.ts +122 -0
- package/dist/components/HighlightCard/HighlightCard.chart.js +660 -0
- package/dist/components/HighlightCard/HighlightCard.d.ts +202 -0
- package/dist/components/HighlightCard/HighlightCard.js +1286 -0
- package/dist/components/HighlightCard/HighlightCard.model.d.ts +692 -0
- package/dist/components/HighlightCard/HighlightCard.model.js +875 -0
- package/dist/components/HighlightCard/index.d.ts +4 -0
- package/dist/components/HighlightCard/index.js +4 -0
- package/dist/components/InsightCard/InsightCard.chart.d.ts +91 -0
- package/dist/components/InsightCard/InsightCard.chart.js +471 -0
- package/dist/components/InsightCard/InsightCard.d.ts +161 -0
- package/dist/components/InsightCard/InsightCard.js +961 -0
- package/dist/components/InsightCard/InsightCard.model.d.ts +405 -0
- package/dist/components/InsightCard/InsightCard.model.js +421 -0
- package/dist/components/InsightCard/index.d.ts +4 -0
- package/dist/components/InsightCard/index.js +4 -0
- package/dist/components/MetricCard/MetricCard.chart.d.ts +79 -0
- package/dist/components/MetricCard/MetricCard.chart.js +435 -0
- package/dist/components/MetricCard/MetricCard.d.ts +227 -0
- package/dist/components/MetricCard/MetricCard.js +1293 -0
- package/dist/components/MetricCard/MetricCard.model.d.ts +521 -0
- package/dist/components/MetricCard/MetricCard.model.js +627 -0
- package/dist/components/MetricCard/index.d.ts +4 -0
- package/dist/components/MetricCard/index.js +4 -0
- package/dist/components/ProfileCard/ProfileCard.chart.d.ts +64 -0
- package/dist/components/ProfileCard/ProfileCard.chart.js +427 -0
- package/dist/components/ProfileCard/ProfileCard.d.ts +140 -0
- package/dist/components/ProfileCard/ProfileCard.js +735 -0
- package/dist/components/ProfileCard/ProfileCard.model.d.ts +279 -0
- package/dist/components/ProfileCard/ProfileCard.model.js +244 -0
- package/dist/components/ProfileCard/index.d.ts +4 -0
- package/dist/components/ProfileCard/index.js +4 -0
- package/dist/components/ProgressCard/ProgressCard.chart.d.ts +73 -0
- package/dist/components/ProgressCard/ProgressCard.chart.js +373 -0
- package/dist/components/ProgressCard/ProgressCard.d.ts +137 -0
- package/dist/components/ProgressCard/ProgressCard.js +908 -0
- package/dist/components/ProgressCard/ProgressCard.model.d.ts +385 -0
- package/dist/components/ProgressCard/ProgressCard.model.js +389 -0
- package/dist/components/ProgressCard/index.d.ts +4 -0
- package/dist/components/ProgressCard/index.js +4 -0
- package/dist/components/ReportCard/ReportCard.chart.d.ts +45 -0
- package/dist/components/ReportCard/ReportCard.chart.js +231 -0
- package/dist/components/ReportCard/ReportCard.d.ts +153 -0
- package/dist/components/ReportCard/ReportCard.js +1118 -0
- package/dist/components/ReportCard/ReportCard.model.d.ts +392 -0
- package/dist/components/ReportCard/ReportCard.model.js +399 -0
- package/dist/components/ReportCard/index.d.ts +4 -0
- package/dist/components/ReportCard/index.js +4 -0
- package/dist/components/SankeyCard/SankeyCard.chart.d.ts +75 -0
- package/dist/components/SankeyCard/SankeyCard.chart.js +249 -0
- package/dist/components/SankeyCard/SankeyCard.d.ts +185 -0
- package/dist/components/SankeyCard/SankeyCard.js +788 -0
- package/dist/components/SankeyCard/SankeyCard.model.d.ts +501 -0
- package/dist/components/SankeyCard/SankeyCard.model.js +789 -0
- package/dist/components/SankeyCard/index.d.ts +4 -0
- package/dist/components/SankeyCard/index.js +4 -0
- package/dist/components/StatusCard/StatusCard.chart.d.ts +60 -0
- package/dist/components/StatusCard/StatusCard.chart.js +188 -0
- package/dist/components/StatusCard/StatusCard.d.ts +176 -0
- package/dist/components/StatusCard/StatusCard.js +989 -0
- package/dist/components/StatusCard/StatusCard.model.d.ts +555 -0
- package/dist/components/StatusCard/StatusCard.model.js +776 -0
- package/dist/components/StatusCard/index.d.ts +4 -0
- package/dist/components/StatusCard/index.js +4 -0
- package/dist/components/TableCard/TableCard.chart.d.ts +53 -0
- package/dist/components/TableCard/TableCard.chart.js +192 -0
- package/dist/components/TableCard/TableCard.d.ts +140 -0
- package/dist/components/TableCard/TableCard.js +811 -0
- package/dist/components/TableCard/TableCard.model.d.ts +381 -0
- package/dist/components/TableCard/TableCard.model.js +475 -0
- package/dist/components/TableCard/index.d.ts +4 -0
- package/dist/components/TableCard/index.js +4 -0
- package/dist/components/TargetCard/TargetCard.chart.d.ts +52 -0
- package/dist/components/TargetCard/TargetCard.chart.js +273 -0
- package/dist/components/TargetCard/TargetCard.d.ts +143 -0
- package/dist/components/TargetCard/TargetCard.js +910 -0
- package/dist/components/TargetCard/TargetCard.model.d.ts +468 -0
- package/dist/components/TargetCard/TargetCard.model.js +581 -0
- package/dist/components/TargetCard/index.d.ts +4 -0
- package/dist/components/TargetCard/index.js +4 -0
- package/dist/components/TreeCard/TreeCard.chart.d.ts +80 -0
- package/dist/components/TreeCard/TreeCard.chart.js +178 -0
- package/dist/components/TreeCard/TreeCard.d.ts +182 -0
- package/dist/components/TreeCard/TreeCard.js +1187 -0
- package/dist/components/TreeCard/TreeCard.model.d.ts +330 -0
- package/dist/components/TreeCard/TreeCard.model.js +401 -0
- package/dist/components/TreeCard/index.d.ts +4 -0
- package/dist/components/TreeCard/index.js +4 -0
- package/dist/components/TrendCard/TrendCard.chart.d.ts +75 -0
- package/dist/components/TrendCard/TrendCard.chart.js +393 -0
- package/dist/components/TrendCard/TrendCard.d.ts +224 -0
- package/dist/components/TrendCard/TrendCard.js +1053 -0
- package/dist/components/TrendCard/TrendCard.model.d.ts +545 -0
- package/dist/components/TrendCard/TrendCard.model.js +623 -0
- package/dist/components/TrendCard/index.d.ts +4 -0
- package/dist/components/TrendCard/index.js +4 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +57 -0
- package/dist/lib/vega/ArcVegaChart.d.ts +24 -0
- package/dist/lib/vega/ArcVegaChart.js +42 -0
- package/dist/lib/vega/ArcVegaSurface.d.ts +13 -0
- package/dist/lib/vega/ArcVegaSurface.js +38 -0
- package/dist/lib/vega/axisTicks.js +156 -0
- package/dist/lib/vega/config.d.ts +29 -0
- package/dist/lib/vega/config.js +113 -0
- package/dist/lib/vega/embed.d.ts +29 -0
- package/dist/lib/vega/embed.js +29 -0
- package/dist/lib/vega/engine.d.ts +10 -0
- package/dist/lib/vega/engine.js +6 -0
- package/dist/lib/vega/formatters.d.ts +125 -0
- package/dist/lib/vega/formatters.js +277 -0
- package/dist/lib/vega/index.d.ts +11 -0
- package/dist/lib/vega/load.d.ts +13 -0
- package/dist/lib/vega/load.js +21 -0
- package/dist/lib/vega/time.d.ts +45 -0
- package/dist/lib/vega/time.js +105 -0
- package/dist/lib/vega/tokens.d.ts +73 -0
- package/dist/lib/vega/tokens.js +137 -0
- package/dist/lib/vega/tooltip.d.ts +45 -0
- package/dist/lib/vega/tooltip.js +124 -0
- package/dist/lib/vega/useArcVega.d.ts +43 -0
- package/dist/lib/vega/useArcVega.js +121 -0
- 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 };
|