@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,475 @@
|
|
|
1
|
+
import { formatDate, formatDelta, formatNumber } from "../../lib/vega/formatters.js";
|
|
2
|
+
//#region src/components/TableCard/TableCard.model.ts
|
|
3
|
+
const DEFAULT_TABLE_LABELS = {
|
|
4
|
+
sortBy: (header) => `Sort by ${header}`,
|
|
5
|
+
sortDescending: (header) => `Sort by ${header}, descending`,
|
|
6
|
+
sortAscending: (header) => `Sort by ${header}, ascending`,
|
|
7
|
+
sortedBy: (header, direction) => `Sorted by ${header}, ${direction === "asc" ? "ascending" : "descending"}`,
|
|
8
|
+
ascending: "ascending",
|
|
9
|
+
descending: "descending",
|
|
10
|
+
rank: (place) => `Rank ${place}`,
|
|
11
|
+
total: "Total",
|
|
12
|
+
showingTopOf: (shown, total) => `Showing top ${shown} of ${total}`,
|
|
13
|
+
showAll: "Show all",
|
|
14
|
+
insightAboveMedian: (leader, header, value, share, rows) => `${leader} leads ${header} at ${value} — ${share} above the median of these ${rows} rows.`,
|
|
15
|
+
insightBelowMedian: (leader, header, value, share, rows) => `${leader} leads ${header} at ${value} — ${share} below the median of these ${rows} rows.`,
|
|
16
|
+
insightAtMedian: (leader, header, value, rows) => `${leader} leads ${header} at ${value}, at the median of these ${rows} rows.`,
|
|
17
|
+
trendSentence: (first, last, change) => `${first} to ${last}, ${change}`,
|
|
18
|
+
trendInsufficient: "Not enough readings for a trend",
|
|
19
|
+
shareOf: (share) => `${share} of the column`,
|
|
20
|
+
increase: "increase",
|
|
21
|
+
decrease: "decrease",
|
|
22
|
+
unchanged: "unchanged",
|
|
23
|
+
percentagePoints: " pp",
|
|
24
|
+
noData: "No data",
|
|
25
|
+
noMatch: "No rows match the filters",
|
|
26
|
+
clearFilters: "Clear filters",
|
|
27
|
+
error: "Could not load this table",
|
|
28
|
+
retry: "Retry",
|
|
29
|
+
insufficient: "Only one row — not enough to rank",
|
|
30
|
+
stale: "Stale",
|
|
31
|
+
asOf: "As of",
|
|
32
|
+
about: (name) => `About ${name}`,
|
|
33
|
+
openRow: (label) => `Open ${label}`,
|
|
34
|
+
caption: (name) => name
|
|
35
|
+
};
|
|
36
|
+
/** Merges a partial override over the defaults. */
|
|
37
|
+
function resolveTableLabels(labels) {
|
|
38
|
+
return labels ? {
|
|
39
|
+
...DEFAULT_TABLE_LABELS,
|
|
40
|
+
...labels
|
|
41
|
+
} : DEFAULT_TABLE_LABELS;
|
|
42
|
+
}
|
|
43
|
+
function isFiniteNumber(value) {
|
|
44
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
45
|
+
}
|
|
46
|
+
/** Coerces a field value to a number, treating anything unparseable as missing rather than as zero (rule 14). */
|
|
47
|
+
function toNumber(value) {
|
|
48
|
+
if (isFiniteNumber(value)) return value;
|
|
49
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
50
|
+
const parsed = Number(value);
|
|
51
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
function toText(value) {
|
|
56
|
+
if (value == null) return null;
|
|
57
|
+
if (typeof value === "string") return value;
|
|
58
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : null;
|
|
59
|
+
if (typeof value === "boolean") return String(value);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Normalises a trend field: a bare array of readings becomes indexed points,
|
|
64
|
+
* `{ x, y }` records pass through, and anything else becomes an empty series.
|
|
65
|
+
* Nulls survive as nulls — a gap is not a zero.
|
|
66
|
+
*/
|
|
67
|
+
function normalizeTrendPoints(value) {
|
|
68
|
+
if (!Array.isArray(value)) return [];
|
|
69
|
+
return value.map((entry, index) => {
|
|
70
|
+
if (entry != null && typeof entry === "object" && !Array.isArray(entry)) {
|
|
71
|
+
const record = entry;
|
|
72
|
+
return {
|
|
73
|
+
x: record.x ?? index,
|
|
74
|
+
y: toNumber(record.y)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
x: index,
|
|
79
|
+
y: toNumber(entry)
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** What a trend cell's series says: its ends, its extremes, its gaps, and the change across it. */
|
|
84
|
+
function summarizeTrend(points) {
|
|
85
|
+
const values = points.map((point) => point.y).filter(isFiniteNumber);
|
|
86
|
+
const withValue = points.filter((point) => isFiniteNumber(point.y));
|
|
87
|
+
const first = withValue.length > 0 ? withValue[0].y : null;
|
|
88
|
+
const last = withValue.length > 0 ? withValue[withValue.length - 1].y : null;
|
|
89
|
+
const change = first != null && last != null && withValue.length > 1 ? last - first : null;
|
|
90
|
+
return {
|
|
91
|
+
first,
|
|
92
|
+
last,
|
|
93
|
+
min: values.length > 0 ? Math.min(...values) : null,
|
|
94
|
+
max: values.length > 0 ? Math.max(...values) : null,
|
|
95
|
+
count: values.length,
|
|
96
|
+
gaps: points.length - values.length,
|
|
97
|
+
change,
|
|
98
|
+
direction: change == null ? null : change > 0 ? "up" : change < 0 ? "down" : "flat"
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** How a change reads once `goodDirection` is applied: up is not always good. */
|
|
102
|
+
function deltaPolarity(direction, goodDirection) {
|
|
103
|
+
if (direction === "flat" || goodDirection === "none") return "neutral";
|
|
104
|
+
return goodDirection === "up" === (direction === "up") ? "positive" : "negative";
|
|
105
|
+
}
|
|
106
|
+
const DEFAULT_ALIGN = {
|
|
107
|
+
text: "start",
|
|
108
|
+
number: "end",
|
|
109
|
+
bar: "end",
|
|
110
|
+
delta: "end",
|
|
111
|
+
trend: "start",
|
|
112
|
+
badge: "start"
|
|
113
|
+
};
|
|
114
|
+
/** A column's identity: its own `id`, else the field it reads. */
|
|
115
|
+
function columnId(column) {
|
|
116
|
+
return column.id ?? column.field;
|
|
117
|
+
}
|
|
118
|
+
function columnFormat(column) {
|
|
119
|
+
if (column.kind === "bar" || column.kind === "trend") return column.format ?? "compact";
|
|
120
|
+
if (column.kind === "number" || column.kind === "delta") return column.format ?? "number";
|
|
121
|
+
return "number";
|
|
122
|
+
}
|
|
123
|
+
function buildCell(column, row, context, domainMax) {
|
|
124
|
+
const { locale, labels } = context;
|
|
125
|
+
const raw = row[column.field];
|
|
126
|
+
const base = {
|
|
127
|
+
id: columnId(column),
|
|
128
|
+
field: column.field,
|
|
129
|
+
kind: column.kind,
|
|
130
|
+
value: null,
|
|
131
|
+
share: null,
|
|
132
|
+
delta: null,
|
|
133
|
+
points: null,
|
|
134
|
+
trend: null,
|
|
135
|
+
tone: null,
|
|
136
|
+
caption: null,
|
|
137
|
+
detail: null
|
|
138
|
+
};
|
|
139
|
+
switch (column.kind) {
|
|
140
|
+
case "text": {
|
|
141
|
+
const text = toText(raw);
|
|
142
|
+
return {
|
|
143
|
+
...base,
|
|
144
|
+
text: text ?? "—",
|
|
145
|
+
sortValue: text,
|
|
146
|
+
caption: column.captionField ? toText(row[column.captionField]) : null
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
case "badge": {
|
|
150
|
+
const text = toText(raw);
|
|
151
|
+
return {
|
|
152
|
+
...base,
|
|
153
|
+
text: text ?? "—",
|
|
154
|
+
sortValue: text,
|
|
155
|
+
tone: (text != null ? column.tones?.[text] : null) ?? "neutral"
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
case "number": {
|
|
159
|
+
const value = toNumber(raw);
|
|
160
|
+
return {
|
|
161
|
+
...base,
|
|
162
|
+
value,
|
|
163
|
+
text: formatNumber(value, columnFormat(column), locale),
|
|
164
|
+
sortValue: value
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
case "bar": {
|
|
168
|
+
const value = toNumber(raw);
|
|
169
|
+
const format = columnFormat(column);
|
|
170
|
+
const share = value != null && value >= 0 && domainMax != null && domainMax > 0 ? Math.min(1, value / domainMax) : null;
|
|
171
|
+
return {
|
|
172
|
+
...base,
|
|
173
|
+
value,
|
|
174
|
+
text: formatNumber(value, format, locale),
|
|
175
|
+
sortValue: value,
|
|
176
|
+
share,
|
|
177
|
+
detail: share != null ? labels.shareOf(formatNumber(share, "percent:0", locale)) : null
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
case "delta": {
|
|
181
|
+
const format = columnFormat(column);
|
|
182
|
+
const mode = column.mode ?? "percent";
|
|
183
|
+
const goodDirection = column.goodDirection ?? "up";
|
|
184
|
+
if (column.previousField) {
|
|
185
|
+
const current = toNumber(raw);
|
|
186
|
+
const previous = toNumber(row[column.previousField]);
|
|
187
|
+
if (current == null || previous == null) return {
|
|
188
|
+
...base,
|
|
189
|
+
text: "—",
|
|
190
|
+
sortValue: null
|
|
191
|
+
};
|
|
192
|
+
const formatted = formatDelta(current, previous, {
|
|
193
|
+
mode,
|
|
194
|
+
format,
|
|
195
|
+
locale,
|
|
196
|
+
pointsSuffix: labels.percentagePoints
|
|
197
|
+
});
|
|
198
|
+
const polarity = deltaPolarity(formatted.direction, goodDirection);
|
|
199
|
+
const sortValue = formatted.renderedMode === "percent" ? formatted.percent ?? formatted.absolute : formatted.absolute;
|
|
200
|
+
return {
|
|
201
|
+
...base,
|
|
202
|
+
value: formatted.absolute,
|
|
203
|
+
text: formatted.text,
|
|
204
|
+
sortValue,
|
|
205
|
+
delta: {
|
|
206
|
+
direction: formatted.direction,
|
|
207
|
+
polarity,
|
|
208
|
+
text: formatted.text,
|
|
209
|
+
renderedMode: formatted.renderedMode,
|
|
210
|
+
value: sortValue
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const value = toNumber(raw);
|
|
215
|
+
if (value == null) return {
|
|
216
|
+
...base,
|
|
217
|
+
text: "—",
|
|
218
|
+
sortValue: null
|
|
219
|
+
};
|
|
220
|
+
const direction = value > 0 ? "up" : value < 0 ? "down" : "flat";
|
|
221
|
+
const text = mode === "percent" ? formatNumber(value, "percent:1", locale, { signDisplay: "exceptZero" }) : formatNumber(value, format, locale, { signDisplay: "exceptZero" });
|
|
222
|
+
return {
|
|
223
|
+
...base,
|
|
224
|
+
value,
|
|
225
|
+
text,
|
|
226
|
+
sortValue: value,
|
|
227
|
+
delta: {
|
|
228
|
+
direction,
|
|
229
|
+
polarity: deltaPolarity(direction, goodDirection),
|
|
230
|
+
text,
|
|
231
|
+
renderedMode: mode,
|
|
232
|
+
value
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
case "trend": {
|
|
237
|
+
const points = normalizeTrendPoints(raw);
|
|
238
|
+
const trend = summarizeTrend(points);
|
|
239
|
+
const format = columnFormat(column);
|
|
240
|
+
const changeText = trend.change != null && trend.first != null ? formatDelta(trend.last, trend.first, {
|
|
241
|
+
mode: "percent",
|
|
242
|
+
format,
|
|
243
|
+
locale,
|
|
244
|
+
pointsSuffix: labels.percentagePoints
|
|
245
|
+
}).text : null;
|
|
246
|
+
const detail = trend.count > 1 && changeText != null ? labels.trendSentence(formatNumber(trend.first, format, locale), formatNumber(trend.last, format, locale), changeText) : labels.trendInsufficient;
|
|
247
|
+
return {
|
|
248
|
+
...base,
|
|
249
|
+
text: formatNumber(trend.last, format, locale),
|
|
250
|
+
sortValue: trend.change,
|
|
251
|
+
value: trend.last,
|
|
252
|
+
points,
|
|
253
|
+
trend,
|
|
254
|
+
detail
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function aggregate(kind, values) {
|
|
260
|
+
if (values.length === 0) return null;
|
|
261
|
+
switch (kind) {
|
|
262
|
+
case "sum": return values.reduce((total, value) => total + value, 0);
|
|
263
|
+
case "mean": return values.reduce((total, value) => total + value, 0) / values.length;
|
|
264
|
+
case "max": return Math.max(...values);
|
|
265
|
+
case "min": return Math.min(...values);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Turns the consumer's columns and records into the model the card renders:
|
|
270
|
+
* every column resolved (alignment, sortability, its own length scale and its
|
|
271
|
+
* footer aggregate), every cell formatted once and given the single number it
|
|
272
|
+
* sorts by, the rows ordered, capped and ranked.
|
|
273
|
+
*
|
|
274
|
+
* Missing readings sort last in both directions — "no reading" is not the
|
|
275
|
+
* smallest reading — and a capped tail is reported in `truncated` rather than
|
|
276
|
+
* folded into an "Other" row: a table's columns are not all additive, so
|
|
277
|
+
* summing them would invent numbers.
|
|
278
|
+
*/
|
|
279
|
+
function normalizeTable(input) {
|
|
280
|
+
const { columns, rows, rowKey, sort, maxRows, totalRows, showRank, locale, timeZone } = input;
|
|
281
|
+
const context = {
|
|
282
|
+
locale,
|
|
283
|
+
timeZone,
|
|
284
|
+
labels: input.labels ?? DEFAULT_TABLE_LABELS
|
|
285
|
+
};
|
|
286
|
+
const labelField = rowKey ?? columns.find((column) => column.kind === "text")?.field;
|
|
287
|
+
const domains = /* @__PURE__ */ new Map();
|
|
288
|
+
for (const column of columns) {
|
|
289
|
+
if (column.kind !== "bar") continue;
|
|
290
|
+
const values = rows.map((row) => toNumber(row[column.field])).filter(isFiniteNumber);
|
|
291
|
+
if (typeof column.domain === "number") domains.set(columnId(column), column.domain);
|
|
292
|
+
else if (column.domain === "total") {
|
|
293
|
+
const sum = values.reduce((total, value) => total + value, 0);
|
|
294
|
+
domains.set(columnId(column), sum > 0 ? sum : null);
|
|
295
|
+
} else domains.set(columnId(column), values.length > 0 ? Math.max(...values) : null);
|
|
296
|
+
}
|
|
297
|
+
const built = rows.map((row, index) => {
|
|
298
|
+
const cells = columns.map((column) => buildCell(column, row, context, domains.get(columnId(column)) ?? null));
|
|
299
|
+
const byField = {};
|
|
300
|
+
for (const cell of cells) byField[cell.id] = cell;
|
|
301
|
+
const identity = labelField != null ? toText(row[labelField]) : null;
|
|
302
|
+
const firstText = cells.find((cell) => cell.kind === "text" && cell.sortValue != null);
|
|
303
|
+
return {
|
|
304
|
+
key: identity ?? `row-${index}`,
|
|
305
|
+
label: (typeof firstText?.sortValue === "string" ? firstText.sortValue : identity) ?? `row-${index}`,
|
|
306
|
+
rank: null,
|
|
307
|
+
cells,
|
|
308
|
+
byField
|
|
309
|
+
};
|
|
310
|
+
});
|
|
311
|
+
const sortColumnDefinition = sort ? columns.find((column) => columnId(column) === sort.field) : void 0;
|
|
312
|
+
const sortColumnResolved = sortColumnDefinition && (sortColumnDefinition.sortable ?? true) ? sortColumnDefinition : void 0;
|
|
313
|
+
let ordered = built;
|
|
314
|
+
if (sort && sortColumnResolved) {
|
|
315
|
+
const factor = sort.direction === "asc" ? 1 : -1;
|
|
316
|
+
const collator = new Intl.Collator(locale, {
|
|
317
|
+
numeric: true,
|
|
318
|
+
sensitivity: "base"
|
|
319
|
+
});
|
|
320
|
+
ordered = built.map((row, index) => ({
|
|
321
|
+
row,
|
|
322
|
+
index
|
|
323
|
+
})).sort((left, right) => {
|
|
324
|
+
const a = left.row.byField[sort.field]?.sortValue ?? null;
|
|
325
|
+
const b = right.row.byField[sort.field]?.sortValue ?? null;
|
|
326
|
+
if (a == null && b == null) return left.index - right.index;
|
|
327
|
+
if (a == null) return 1;
|
|
328
|
+
if (b == null) return -1;
|
|
329
|
+
const compared = typeof a === "string" || typeof b === "string" ? collator.compare(String(a), String(b)) : a - b;
|
|
330
|
+
return compared !== 0 ? compared * factor : left.index - right.index;
|
|
331
|
+
}).map((entry) => entry.row);
|
|
332
|
+
}
|
|
333
|
+
const shown = maxRows != null && maxRows > 0 ? ordered.slice(0, maxRows) : ordered;
|
|
334
|
+
const total = totalRows ?? rows.length;
|
|
335
|
+
let ranked = shown;
|
|
336
|
+
if (showRank && sort && sortColumnResolved) {
|
|
337
|
+
const out = [];
|
|
338
|
+
let previousValue;
|
|
339
|
+
let previousRank = 0;
|
|
340
|
+
shown.forEach((row, index) => {
|
|
341
|
+
const value = row.byField[sort.field]?.sortValue ?? null;
|
|
342
|
+
const rank = index > 0 && value === previousValue ? previousRank : index + 1;
|
|
343
|
+
previousValue = value;
|
|
344
|
+
previousRank = rank;
|
|
345
|
+
out.push({
|
|
346
|
+
...row,
|
|
347
|
+
rank
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
ranked = out;
|
|
351
|
+
}
|
|
352
|
+
const columnModels = columns.map((column) => {
|
|
353
|
+
const id = columnId(column);
|
|
354
|
+
const cells = ranked.map((row) => row.byField[id]).filter((cell) => cell != null);
|
|
355
|
+
const values = cells.map((cell) => cell.value).filter(isFiniteNumber);
|
|
356
|
+
const totalValue = column.total ? aggregate(column.total, values) : null;
|
|
357
|
+
const format = columnFormat(column);
|
|
358
|
+
return {
|
|
359
|
+
column,
|
|
360
|
+
id,
|
|
361
|
+
field: column.field,
|
|
362
|
+
kind: column.kind,
|
|
363
|
+
header: column.header,
|
|
364
|
+
align: column.align ?? DEFAULT_ALIGN[column.kind],
|
|
365
|
+
sortable: column.sortable ?? true,
|
|
366
|
+
domainMax: domains.get(id) ?? null,
|
|
367
|
+
totalValue,
|
|
368
|
+
totalText: column.total ? formatNumber(totalValue, format, locale) : null,
|
|
369
|
+
hasMissing: cells.some((cell) => cell.sortValue == null)
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
return {
|
|
373
|
+
rows: ranked,
|
|
374
|
+
columns: columnModels,
|
|
375
|
+
count: ranked.length,
|
|
376
|
+
total,
|
|
377
|
+
truncated: Math.max(0, total - ranked.length),
|
|
378
|
+
sortColumn: sortColumnResolved ? columnModels.find((model) => model.id === sort?.field) ?? null : null,
|
|
379
|
+
leader: ranked[0] ?? null,
|
|
380
|
+
trailer: ranked.length > 1 ? ranked[ranked.length - 1] ?? null : null,
|
|
381
|
+
hasTrend: columns.some((column) => column.kind === "trend"),
|
|
382
|
+
insufficient: ranked.length === 1 && total === 1
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* The state machine: `loading` with nothing to show, `refreshing` when a
|
|
387
|
+
* reload arrives over a rendered table (the previous render stays under the
|
|
388
|
+
* veil rather than collapsing to a skeleton), `error`, then `empty` or
|
|
389
|
+
* `no-match` — different messages, because "there is no data" and "your
|
|
390
|
+
* filters exclude all of it" call for different actions (Cloudscape's
|
|
391
|
+
* `empty` / `noMatch` split).
|
|
392
|
+
*/
|
|
393
|
+
function deriveTableState({ status, rowCount, filtered }) {
|
|
394
|
+
if (status === "error") return "error";
|
|
395
|
+
if (status === "loading") return rowCount > 0 ? "refreshing" : "loading";
|
|
396
|
+
if (rowCount === 0) return filtered ? "no-match" : "empty";
|
|
397
|
+
return "ready";
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* The card's accessible description (Amy Cesal's alt-text formula: what form,
|
|
401
|
+
* what data, why it is here, as of when) — what the table holds, how it is
|
|
402
|
+
* ordered, which measures it carries, and whether it is capped.
|
|
403
|
+
*/
|
|
404
|
+
function buildTableDescription({ name, summary, labels, sort, updatedAt, updatedAtFormat = "day", locale, timeZone }) {
|
|
405
|
+
const parts = [];
|
|
406
|
+
const measures = summary.columns.filter((column) => column.kind !== "text").map((column) => column.header);
|
|
407
|
+
const rowNoun = summary.count === 1 ? "row" : "rows";
|
|
408
|
+
parts.push(`${name}: a table of ${summary.count} ${rowNoun}`);
|
|
409
|
+
if (sort && summary.sortColumn) parts.push(`ordered by ${summary.sortColumn.header} ${sort.direction === "asc" ? labels.ascending : labels.descending}`);
|
|
410
|
+
if (measures.length > 0) parts.push(`with ${measures.join(", ")}`);
|
|
411
|
+
const sentences = [`${parts.join(", ")}.`];
|
|
412
|
+
if (summary.truncated > 0) sentences.push(`${labels.showingTopOf(summary.count, summary.total)}.`);
|
|
413
|
+
if (summary.leader != null && summary.sortColumn != null) {
|
|
414
|
+
const cell = summary.leader.byField[summary.sortColumn.id];
|
|
415
|
+
if (cell != null) sentences.push(`${summary.leader.label} leads at ${cell.text}.`);
|
|
416
|
+
}
|
|
417
|
+
if (summary.columns.some((column) => column.hasMissing)) sentences.push("Cells with no reading are marked with a dash.");
|
|
418
|
+
if (updatedAt != null) sentences.push(`${labels.asOf} ${formatDate(updatedAt, {
|
|
419
|
+
granularity: updatedAtFormat,
|
|
420
|
+
locale,
|
|
421
|
+
timeZone
|
|
422
|
+
})}.`);
|
|
423
|
+
return sentences.join(" ");
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* The takeaway, as one visible sentence (Chartability's "title, summary,
|
|
427
|
+
* context"): how far the leader stands above the middle of the sorted column,
|
|
428
|
+
* because "who is on top" is already visible and "by how much" is the part a
|
|
429
|
+
* reader has to compute. `null` when the table is unsorted or too short for
|
|
430
|
+
* the comparison to mean anything.
|
|
431
|
+
*/
|
|
432
|
+
function buildTableInsight({ summary, labels, locale }) {
|
|
433
|
+
const column = summary.sortColumn;
|
|
434
|
+
const leader = summary.leader;
|
|
435
|
+
if (column == null || leader == null || summary.count < 3) return null;
|
|
436
|
+
if (column.kind === "text" || column.kind === "badge") return null;
|
|
437
|
+
const values = summary.rows.map((row) => row.byField[column.id]?.sortValue).filter((value) => typeof value === "number" && Number.isFinite(value));
|
|
438
|
+
if (values.length < 3) return null;
|
|
439
|
+
const leaderValue = leader.byField[column.id]?.sortValue;
|
|
440
|
+
if (typeof leaderValue !== "number" || !Number.isFinite(leaderValue)) return null;
|
|
441
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
442
|
+
const middle = sorted.length % 2 === 1 ? sorted[(sorted.length - 1) / 2] : (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2;
|
|
443
|
+
if (middle === 0) return null;
|
|
444
|
+
const ratio = (leaderValue - middle) / Math.abs(middle);
|
|
445
|
+
const share = formatNumber(Math.abs(ratio), "percent:0", locale);
|
|
446
|
+
const leaderText = leader.byField[column.id]?.text ?? "—";
|
|
447
|
+
if (ratio === 0) return labels.insightAtMedian(leader.label, column.header, leaderText, summary.count);
|
|
448
|
+
return (ratio > 0 ? labels.insightAboveMedian : labels.insightBelowMedian)(leader.label, column.header, leaderText, share, summary.count);
|
|
449
|
+
}
|
|
450
|
+
/** The `arcTime` grammar string for a trend cell's tooltip x, built from the card's locale and zone. */
|
|
451
|
+
function trendTimeFormat(granularity, locale, timeZone) {
|
|
452
|
+
return [
|
|
453
|
+
granularity ?? "day",
|
|
454
|
+
locale ?? "",
|
|
455
|
+
timeZone ?? ""
|
|
456
|
+
].join("@").replace(/@+$/, "");
|
|
457
|
+
}
|
|
458
|
+
/** The `arcNumber` grammar string for a column, with the card's locale appended. */
|
|
459
|
+
function columnValueFormat(column, locale) {
|
|
460
|
+
const format = columnFormat(column);
|
|
461
|
+
return locale ? `${format}@${locale}` : format;
|
|
462
|
+
}
|
|
463
|
+
/** Flips a column's order: a fresh click on a numeric column leads with the largest, on a name with A. */
|
|
464
|
+
function nextSort(current, column) {
|
|
465
|
+
if (current?.field === column.id) return {
|
|
466
|
+
field: column.id,
|
|
467
|
+
direction: current.direction === "asc" ? "desc" : "asc"
|
|
468
|
+
};
|
|
469
|
+
return {
|
|
470
|
+
field: column.id,
|
|
471
|
+
direction: column.kind === "text" || column.kind === "badge" ? "asc" : "desc"
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
//#endregion
|
|
475
|
+
export { DEFAULT_TABLE_LABELS, buildTableDescription, buildTableInsight, columnId, columnValueFormat, deltaPolarity, deriveTableState, nextSort, normalizeTable, normalizeTrendPoints, resolveTableLabels, summarizeTrend, trendTimeFormat };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { BuildTableDescriptionInput, BuildTableInsightInput, DEFAULT_TABLE_LABELS, DeriveTableStateInput, NormalizeTableInput, TableCardAlign, TableCardBadgeColumn, TableCardBarColumn, TableCardCell, TableCardCellDelta, TableCardColumn, TableCardColumnKind, TableCardColumnModel, TableCardDeltaColumn, TableCardGoodDirection, TableCardLabels, TableCardNumberColumn, TableCardPolarity, TableCardRow, TableCardRowModel, TableCardSize, TableCardSort, TableCardSortDirection, TableCardState, TableCardStatus, TableCardSummary, TableCardTextColumn, TableCardTone, TableCardTotal, TableCardTrendColumn, TableCardTrendMark, TableCardTrendPoint, TableCardTrendSummary, buildTableDescription, buildTableInsight, columnId, columnValueFormat, deltaPolarity, deriveTableState, nextSort, normalizeTable, normalizeTrendPoints, resolveTableLabels, summarizeTrend, trendTimeFormat } from "./TableCard.model.js";
|
|
2
|
+
import { TableCard, TableCardBaseProps, TableCardLabelProps, TableCardProps } from "./TableCard.js";
|
|
3
|
+
import { TABLE_TREND_DATASET, TABLE_TREND_HOVER_PARAM, TABLE_TREND_LATEST_DATASET, TableTrendSpecOptions, buildTableTrendSpec } from "./TableCard.chart.js";
|
|
4
|
+
export { type BuildTableDescriptionInput, type BuildTableInsightInput, DEFAULT_TABLE_LABELS, type DeriveTableStateInput, type NormalizeTableInput, TABLE_TREND_DATASET, TABLE_TREND_HOVER_PARAM, TABLE_TREND_LATEST_DATASET, TableCard, type TableCardAlign, type TableCardBadgeColumn, type TableCardBarColumn, type TableCardBaseProps, type TableCardCell, type TableCardCellDelta, type TableCardColumn, type TableCardColumnKind, type TableCardColumnModel, type TableCardDeltaColumn, type TableCardGoodDirection, type TableCardLabelProps, type TableCardLabels, type TableCardNumberColumn, type TableCardPolarity, type TableCardProps, type TableCardRow, type TableCardRowModel, type TableCardSize, type TableCardSort, type TableCardSortDirection, type TableCardState, type TableCardStatus, type TableCardSummary, type TableCardTextColumn, type TableCardTone, type TableCardTotal, type TableCardTrendColumn, type TableCardTrendMark, type TableCardTrendPoint, type TableCardTrendSummary, type TableTrendSpecOptions, buildTableDescription, buildTableInsight, buildTableTrendSpec, columnId, columnValueFormat, deltaPolarity, deriveTableState, nextSort, normalizeTable, normalizeTrendPoints, resolveTableLabels, summarizeTrend, trendTimeFormat };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { TABLE_TREND_DATASET, TABLE_TREND_HOVER_PARAM, TABLE_TREND_LATEST_DATASET, buildTableTrendSpec } from "./TableCard.chart.js";
|
|
2
|
+
import { DEFAULT_TABLE_LABELS, buildTableDescription, buildTableInsight, columnId, columnValueFormat, deltaPolarity, deriveTableState, nextSort, normalizeTable, normalizeTrendPoints, resolveTableLabels, summarizeTrend, trendTimeFormat } from "./TableCard.model.js";
|
|
3
|
+
import { TableCard } from "./TableCard.js";
|
|
4
|
+
export { DEFAULT_TABLE_LABELS, TABLE_TREND_DATASET, TABLE_TREND_HOVER_PARAM, TABLE_TREND_LATEST_DATASET, TableCard, buildTableDescription, buildTableInsight, buildTableTrendSpec, columnId, columnValueFormat, deltaPolarity, deriveTableState, nextSort, normalizeTable, normalizeTrendPoints, resolveTableLabels, summarizeTrend, trendTimeFormat };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ChartTokens } from "../../lib/vega/tokens.js";
|
|
2
|
+
import { TopLevelSpec } from "vega-lite";
|
|
3
|
+
//#region src/components/TargetCard/TargetCard.chart.d.ts
|
|
4
|
+
/** One row `{ value, zero, ratio, tone }` — the reading, drawn as the bar from zero. */
|
|
5
|
+
declare const TARGET_ACTUAL_DATASET = "table";
|
|
6
|
+
/** One row `{ min, max, target }` — the x domain (always spanning zero, the target and every marker) and the target's position for the label layout. */
|
|
7
|
+
declare const TARGET_BOUNDS_DATASET = "bounds";
|
|
8
|
+
/** One or two rows `{ value, ratio, role }` with `role` `target` or `pace` — the ticks and their words. */
|
|
9
|
+
declare const TARGET_MARKERS_DATASET = "markers";
|
|
10
|
+
/** Zero or one row `{ from, to, ratio, tone }` — the projected extension beyond the reading. */
|
|
11
|
+
declare const TARGET_PROJECTION_DATASET = "projection";
|
|
12
|
+
/** The x domain's ends, read from the `bounds` dataset. */
|
|
13
|
+
declare const TARGET_MIN_PARAM = "target_x_min";
|
|
14
|
+
declare const TARGET_MAX_PARAM = "target_x_max";
|
|
15
|
+
/** The target's value, read from the `bounds` dataset — the pace word yields to the target word when the two would collide. */
|
|
16
|
+
declare const TARGET_MARKER_PARAM = "target_marker";
|
|
17
|
+
declare const TARGET_LABEL_ROW_PX = 16;
|
|
18
|
+
interface TargetBulletSpecOptions {
|
|
19
|
+
/** Bar and track thickness in pixels (≤ 24). */
|
|
20
|
+
barHeight: number;
|
|
21
|
+
/** Plot height in pixels — the bar row plus, with `labels`, the word row. */
|
|
22
|
+
height: number;
|
|
23
|
+
/** Write the marker words (`Target`, `Pace`) under their ticks. */
|
|
24
|
+
labels: boolean;
|
|
25
|
+
/** Wire the tooltip channel on the bar, the extension and the ticks. */
|
|
26
|
+
tooltip: boolean;
|
|
27
|
+
/** Localised words for the two markers. */
|
|
28
|
+
targetLabel: string;
|
|
29
|
+
paceLabel: string;
|
|
30
|
+
/** `arcNumber` grammar for values (`currency:THB:compact@en-US`). */
|
|
31
|
+
valueFormat: string;
|
|
32
|
+
/** Localised tooltip titles. */
|
|
33
|
+
valueTitle: string;
|
|
34
|
+
targetTitle: string;
|
|
35
|
+
paceTitle: string;
|
|
36
|
+
projectionTitle: string;
|
|
37
|
+
locale?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Builds the bullet graph: a muted track across the whole domain, the reading
|
|
41
|
+
* as a bar from zero (length on a shared zero-based scale — Cleveland &
|
|
42
|
+
* McGill's channel for magnitude, Wilke's proportional ink), a lighter
|
|
43
|
+
* extension to the projection, a thin gray tick where the plan expects the
|
|
44
|
+
* reading to be by now and a heavier tick at the target, each with its word
|
|
45
|
+
* underneath — the pace word yielding when the two would collide — and a
|
|
46
|
+
* tooltip on every mark. The domain is explicit (`bounds`), so a reading past
|
|
47
|
+
* its target extends the scale rather than clipping, and a negative reading
|
|
48
|
+
* grows the track to the left of zero.
|
|
49
|
+
*/
|
|
50
|
+
declare function buildTargetBulletSpec(options: TargetBulletSpecOptions, tokens: ChartTokens): TopLevelSpec;
|
|
51
|
+
//#endregion
|
|
52
|
+
export { TARGET_ACTUAL_DATASET, TARGET_BOUNDS_DATASET, TARGET_LABEL_ROW_PX, TARGET_MARKERS_DATASET, TARGET_MARKER_PARAM, TARGET_MAX_PARAM, TARGET_MIN_PARAM, TARGET_PROJECTION_DATASET, TargetBulletSpecOptions, buildTargetBulletSpec };
|