@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,581 @@
|
|
|
1
|
+
import { formatDate, formatDateRange, formatDelta, formatNumber } from "../../lib/vega/formatters.js";
|
|
2
|
+
import { zonedMidnight, zonedParts } from "../../lib/vega/time.js";
|
|
3
|
+
//#region src/components/TargetCard/TargetCard.model.ts
|
|
4
|
+
const DEFAULT_TARGET_LABELS = {
|
|
5
|
+
ofTarget: "of target",
|
|
6
|
+
pairSeparator: "/",
|
|
7
|
+
readingLine: (value, target) => `${value} of ${target}`,
|
|
8
|
+
attainmentLine: (percent, target) => `${percent} of ${target}`,
|
|
9
|
+
target: "Target",
|
|
10
|
+
reached: "Reached",
|
|
11
|
+
remaining: "Remaining",
|
|
12
|
+
overBy: "Over by",
|
|
13
|
+
expectedByNow: "Expected by now",
|
|
14
|
+
aheadOfPace: (direction) => direction === "down" ? "under pace" : "ahead of pace",
|
|
15
|
+
behindPace: (direction) => direction === "down" ? "over pace" : "behind pace",
|
|
16
|
+
onPace: "on pace",
|
|
17
|
+
vsTarget: "vs target",
|
|
18
|
+
aboveTarget: "Above target",
|
|
19
|
+
belowTarget: "Below target",
|
|
20
|
+
onTarget: "On target",
|
|
21
|
+
projected: "Projected",
|
|
22
|
+
period: "Period",
|
|
23
|
+
week: (weekNumber) => `Week ${weekNumber}`,
|
|
24
|
+
quarter: (quarter, year) => `Q${quarter} ${year}`,
|
|
25
|
+
elapsed: (percent) => `${percent} elapsed`,
|
|
26
|
+
elapsedRow: "Elapsed",
|
|
27
|
+
daysRow: "Days elapsed",
|
|
28
|
+
days: (elapsed, total) => `${elapsed} of ${total}`,
|
|
29
|
+
trackNames: {
|
|
30
|
+
achieved: "Target met",
|
|
31
|
+
"on-track": "On track",
|
|
32
|
+
"at-risk": "At risk",
|
|
33
|
+
"off-track": "Off track",
|
|
34
|
+
missed: "Target missed"
|
|
35
|
+
},
|
|
36
|
+
trackRow: "Status",
|
|
37
|
+
increase: "Up",
|
|
38
|
+
decrease: "Down",
|
|
39
|
+
unchanged: "No change",
|
|
40
|
+
percentagePoints: " pp",
|
|
41
|
+
noData: "No data",
|
|
42
|
+
noMatch: "No matching data",
|
|
43
|
+
clearFilters: "Clear filters",
|
|
44
|
+
error: "Couldn't load this target",
|
|
45
|
+
retry: "Try again",
|
|
46
|
+
noTarget: "No target set",
|
|
47
|
+
stale: "Stale",
|
|
48
|
+
asOf: "As of",
|
|
49
|
+
tableItem: "Item",
|
|
50
|
+
tableValue: "Value",
|
|
51
|
+
changePercent: "Change %",
|
|
52
|
+
changeAmount: "Change",
|
|
53
|
+
markNames: {
|
|
54
|
+
actual: "Reached",
|
|
55
|
+
pace: "Pace",
|
|
56
|
+
target: "Target",
|
|
57
|
+
projection: "Projected"
|
|
58
|
+
},
|
|
59
|
+
showTable: (name) => `Show the details behind ${name}`,
|
|
60
|
+
hideTable: (name) => `Hide the details behind ${name}`,
|
|
61
|
+
about: (name) => `About ${name}`,
|
|
62
|
+
loading: (name) => `Loading ${name}`,
|
|
63
|
+
tableCaption: (name) => `Details of ${name} against its target`,
|
|
64
|
+
progressGroup: (name) => `${name} against its target`,
|
|
65
|
+
trackGroup: (name) => `${name} status`,
|
|
66
|
+
insightMark: "Insight",
|
|
67
|
+
explanation: ({ kind, direction, valueText, percentText, targetText, gapKind, gapText, expectedText, expectedPercentText, delta, paceStatus }) => {
|
|
68
|
+
const rows = [[DEFAULT_TARGET_LABELS.reached, `${valueText} (${percentText})`]];
|
|
69
|
+
if (kind === "pace" && expectedText) {
|
|
70
|
+
rows.push([DEFAULT_TARGET_LABELS.expectedByNow, expectedPercentText ? `${expectedText} (${expectedPercentText})` : expectedText]);
|
|
71
|
+
const word = paceStatus === "ahead" ? DEFAULT_TARGET_LABELS.aheadOfPace(direction) : paceStatus === "behind" ? DEFAULT_TARGET_LABELS.behindPace(direction) : DEFAULT_TARGET_LABELS.onPace;
|
|
72
|
+
rows.push([word.charAt(0).toUpperCase() + word.slice(1), delta.text]);
|
|
73
|
+
} else {
|
|
74
|
+
rows.push([DEFAULT_TARGET_LABELS.target, targetText]);
|
|
75
|
+
rows.push([gapKind === "over" ? DEFAULT_TARGET_LABELS.overBy : DEFAULT_TARGET_LABELS.remaining, gapText]);
|
|
76
|
+
rows.push([delta.direction === "up" ? DEFAULT_TARGET_LABELS.aboveTarget : delta.direction === "down" ? DEFAULT_TARGET_LABELS.belowTarget : DEFAULT_TARGET_LABELS.onTarget, delta.text]);
|
|
77
|
+
}
|
|
78
|
+
if (delta.secondaryText) rows.push([delta.renderedMode === "percent" ? DEFAULT_TARGET_LABELS.changeAmount : DEFAULT_TARGET_LABELS.changePercent, delta.secondaryText]);
|
|
79
|
+
return rows;
|
|
80
|
+
},
|
|
81
|
+
attainmentSentence: ({ name, percent, value, target }) => `${name}: ${percent} of target (${value} of ${target}).`,
|
|
82
|
+
gapSentence: ({ kind, amount }) => kind === "over" ? `Over by ${amount}.` : `Remaining ${amount}.`,
|
|
83
|
+
paceSentence: ({ word, primary, secondary, expectedPercent, elapsedPercent }) => {
|
|
84
|
+
const capitalised = word.charAt(0).toUpperCase() + word.slice(1);
|
|
85
|
+
const context = elapsedPercent ? `${expectedPercent} expected by now, ${elapsedPercent} of the period elapsed` : `${expectedPercent} expected by now`;
|
|
86
|
+
return primary ? `${capitalised} by ${primary}${secondary ? ` (${secondary})` : ""}: ${context}.` : `${capitalised}: ${context}.`;
|
|
87
|
+
},
|
|
88
|
+
projectionSentence: ({ value, percent }) => `Projected ${value} (${percent} of target).`,
|
|
89
|
+
periodSentence: ({ name, range }) => name === range ? `Period ${range}.` : `Period ${name} (${range}).`,
|
|
90
|
+
chartDescription: ({ name, value, percent, target, expected, expectedPercent, projected, projectedPercent }) => `Bullet graph of ${name}: reached ${value}, ${percent} of the ${target} target.` + (expected ? ` Pace marker at ${expected}${expectedPercent ? ` (${expectedPercent})` : ""}.` : "") + (projected ? ` Projected ${projected}${projectedPercent ? ` (${projectedPercent})` : ""}.` : "")
|
|
91
|
+
};
|
|
92
|
+
function resolveTargetLabels(overrides) {
|
|
93
|
+
return overrides ? {
|
|
94
|
+
...DEFAULT_TARGET_LABELS,
|
|
95
|
+
...overrides
|
|
96
|
+
} : DEFAULT_TARGET_LABELS;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The state machine. Error wins; loading over existing data is a refresh (the
|
|
100
|
+
* previous render stays under the veil); a missing reading is `no-match` when
|
|
101
|
+
* the consumer's filter caused it and `empty` otherwise. A reading without a
|
|
102
|
+
* target is `ready` — the card then shows the reading and says no target is
|
|
103
|
+
* set (`data-insufficient`).
|
|
104
|
+
*/
|
|
105
|
+
function deriveTargetState({ status, hasValue, hasData, filtered }) {
|
|
106
|
+
if (status === "error") return "error";
|
|
107
|
+
if (status === "loading") return hasData ? "refreshing" : "loading";
|
|
108
|
+
if (hasValue) return "ready";
|
|
109
|
+
return filtered ? "no-match" : "empty";
|
|
110
|
+
}
|
|
111
|
+
const isFinite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
112
|
+
const DAY_MS = 864e5;
|
|
113
|
+
const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
114
|
+
function toInstant(value) {
|
|
115
|
+
return value instanceof Date ? value.getTime() : new Date(value).getTime();
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The period as instants in the card's time zone. A date-only bound is a
|
|
119
|
+
* whole calendar day: `start` opens at its midnight and `end` closes at the
|
|
120
|
+
* midnight after it, so `"2026-09-01"` – `"2026-09-30"` is all of September
|
|
121
|
+
* (30 days, not 29). Any other form is the exact instant. `null` when a bound
|
|
122
|
+
* cannot be parsed or the period has no length.
|
|
123
|
+
*/
|
|
124
|
+
function resolvePeriod(period, timeZone) {
|
|
125
|
+
if (!period) return null;
|
|
126
|
+
const resolve = (bound, inclusiveEnd) => {
|
|
127
|
+
if (typeof bound === "string") {
|
|
128
|
+
const match = DATE_ONLY.exec(bound);
|
|
129
|
+
if (match) {
|
|
130
|
+
const [, year, month, day] = match;
|
|
131
|
+
return zonedMidnight(Number(year), Number(month) - 1, Number(day) + (inclusiveEnd ? 1 : 0), timeZone);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const time = toInstant(bound);
|
|
135
|
+
return Number.isNaN(time) ? null : time;
|
|
136
|
+
};
|
|
137
|
+
const start = resolve(period.start, false);
|
|
138
|
+
const end = resolve(period.end, true);
|
|
139
|
+
if (start == null || end == null || end <= start) return null;
|
|
140
|
+
const dateOnly = typeof period.start === "string" && DATE_ONLY.test(period.start) && typeof period.end === "string" && DATE_ONLY.test(period.end);
|
|
141
|
+
return {
|
|
142
|
+
start,
|
|
143
|
+
end,
|
|
144
|
+
label: period.label,
|
|
145
|
+
dateOnly
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* How far the period has run at `asOf`. The fraction is clamped for the
|
|
150
|
+
* plan's expectation (nothing is expected before the start, everything by the
|
|
151
|
+
* end); `rawFraction` keeps the overshoot so a reading after the end knows
|
|
152
|
+
* the period is over. `null` when `asOf` is missing or unparseable.
|
|
153
|
+
*/
|
|
154
|
+
function deriveElapsed(bounds, asOf, locale) {
|
|
155
|
+
if (!bounds || asOf == null) return null;
|
|
156
|
+
const now = toInstant(asOf);
|
|
157
|
+
if (Number.isNaN(now)) return null;
|
|
158
|
+
const rawFraction = (now - bounds.start) / (bounds.end - bounds.start);
|
|
159
|
+
const fraction = Math.min(1, Math.max(0, rawFraction));
|
|
160
|
+
return {
|
|
161
|
+
fraction,
|
|
162
|
+
rawFraction,
|
|
163
|
+
started: rawFraction >= 0,
|
|
164
|
+
ended: rawFraction >= 1,
|
|
165
|
+
percentText: formatNumber(fraction, "percent:0", locale)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** The whole days a date-only period spans. */
|
|
169
|
+
function periodDays(bounds) {
|
|
170
|
+
return Math.round((bounds.end - bounds.start) / DAY_MS);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The period counted in days — `18 of 30` on 19 September. Only for a period
|
|
174
|
+
* whose bounds are whole calendar days: an intraday window has no honest day
|
|
175
|
+
* count, and `null` keeps the row out of the table rather than rounding one
|
|
176
|
+
* into existence. Counted between calendar dates in the card's zone, so a
|
|
177
|
+
* daylight-saving change never shifts the count by a day.
|
|
178
|
+
*/
|
|
179
|
+
function deriveDays(bounds, asOf, timeZone) {
|
|
180
|
+
if (!bounds || !bounds.dateOnly || asOf == null) return null;
|
|
181
|
+
const now = toInstant(asOf);
|
|
182
|
+
if (Number.isNaN(now)) return null;
|
|
183
|
+
const total = periodDays(bounds);
|
|
184
|
+
const start = zonedParts(bounds.start, timeZone);
|
|
185
|
+
const current = zonedParts(now, timeZone);
|
|
186
|
+
const startDay = Date.UTC(start.year, start.month - 1, start.day);
|
|
187
|
+
const currentDay = Date.UTC(current.year, current.month - 1, current.day);
|
|
188
|
+
const elapsed = Math.round((currentDay - startDay) / DAY_MS);
|
|
189
|
+
return {
|
|
190
|
+
elapsed: Math.min(total, Math.max(0, elapsed)),
|
|
191
|
+
total
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/** The formatters every text surface shares — headline, badge, plot tooltip, table, descriptions. */
|
|
195
|
+
function createTargetFormatters({ format, locale, formatValue }) {
|
|
196
|
+
return {
|
|
197
|
+
formatValue: (value) => isFinite(value) ? formatValue ? formatValue(value) : formatNumber(value, format, locale) : "—",
|
|
198
|
+
formatPercent: (ratio) => isFinite(ratio) ? formatNumber(ratio, "percent:0", locale) : "—"
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const NUMERIC_CORE = /^[\d.,'\s]+$/;
|
|
202
|
+
/**
|
|
203
|
+
* Joins a reading and its target into one compact pair — `฿2.6M` and `฿4M`
|
|
204
|
+
* become `฿2.6/4M`, `187 ms` and `200 ms` become `187/200 ms`, `2.7%` and `3%`
|
|
205
|
+
* become `2.7/3%` — by lifting out the symbol they both start with and the
|
|
206
|
+
* magnitude they both end with. A leading sign stays on the reading
|
|
207
|
+
* (`-฿120/500K`).
|
|
208
|
+
*
|
|
209
|
+
* Returns `null` whenever the two cannot share their parts — `฿850K` against
|
|
210
|
+
* `฿4M` is not `฿850/4M`, and a locale or formatter this heuristic does not
|
|
211
|
+
* recognise must not be guessed at — so the caller falls back to the full
|
|
212
|
+
* `X of Y` line rather than printing something untrue.
|
|
213
|
+
*/
|
|
214
|
+
function formatValuePair(valueText, targetText, separator = "/") {
|
|
215
|
+
const sign = /^[-−+]/.exec(valueText)?.[0] ?? "";
|
|
216
|
+
const value = valueText.slice(sign.length);
|
|
217
|
+
if (!value || !targetText) return null;
|
|
218
|
+
const isDigit = (char) => char >= "0" && char <= "9";
|
|
219
|
+
let prefix = 0;
|
|
220
|
+
while (prefix < value.length && prefix < targetText.length && value[prefix] === targetText[prefix] && !isDigit(value[prefix])) prefix += 1;
|
|
221
|
+
let suffix = 0;
|
|
222
|
+
while (suffix < value.length - prefix && suffix < targetText.length - prefix && value[value.length - 1 - suffix] === targetText[targetText.length - 1 - suffix] && !isDigit(value[value.length - 1 - suffix])) suffix += 1;
|
|
223
|
+
const head = value.slice(0, prefix);
|
|
224
|
+
const tail = suffix > 0 ? value.slice(value.length - suffix) : "";
|
|
225
|
+
const valueCore = value.slice(prefix, value.length - suffix);
|
|
226
|
+
const targetCore = targetText.slice(prefix, targetText.length - suffix);
|
|
227
|
+
if (!NUMERIC_CORE.test(valueCore) || !NUMERIC_CORE.test(targetCore)) return null;
|
|
228
|
+
return `${sign}${head}${valueCore}${separator}${targetCore}${tail}`;
|
|
229
|
+
}
|
|
230
|
+
/** The reading against a reference (the target, or the plan's expectation), with polarity resolved through `goodDirection`. */
|
|
231
|
+
function deriveTargetDelta(value, reference, { deltaMode, goodDirection, format, locale, percentagePoints }) {
|
|
232
|
+
const delta = formatDelta(value, reference, {
|
|
233
|
+
mode: deltaMode,
|
|
234
|
+
format,
|
|
235
|
+
locale,
|
|
236
|
+
pointsSuffix: percentagePoints
|
|
237
|
+
});
|
|
238
|
+
const other = formatDelta(value, reference, {
|
|
239
|
+
mode: delta.renderedMode === "percent" ? "absolute" : "percent",
|
|
240
|
+
format,
|
|
241
|
+
locale,
|
|
242
|
+
pointsSuffix: percentagePoints
|
|
243
|
+
});
|
|
244
|
+
let polarity = "neutral";
|
|
245
|
+
if (delta.direction !== "flat") polarity = delta.direction === "up" === (goodDirection === "up") ? "positive" : "negative";
|
|
246
|
+
return {
|
|
247
|
+
direction: delta.direction,
|
|
248
|
+
polarity,
|
|
249
|
+
text: delta.text,
|
|
250
|
+
renderedMode: delta.renderedMode,
|
|
251
|
+
secondaryText: other.renderedMode === delta.renderedMode ? null : other.text
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Attainment, pace, projection and verdict — `null` without a finite reading
|
|
256
|
+
* and a finite, non-zero target (a zero target has no ratio).
|
|
257
|
+
*
|
|
258
|
+
* Pace is `target × elapsed` unless the product supplies its own `expected`
|
|
259
|
+
* (a seasonal plan curve); the projection is the linear run rate
|
|
260
|
+
* `value / elapsed` unless the product supplies `projected` — both are
|
|
261
|
+
* declared as such in `source`, and a run rate is only projected while the
|
|
262
|
+
* period is still running with some of it elapsed. The verdict: an `up`
|
|
263
|
+
* target is achieved the moment the reading reaches it and missed once the
|
|
264
|
+
* period ends short of it; a `down` target is missed the moment the reading
|
|
265
|
+
* exceeds it and achieved once the period ends within it. While the period
|
|
266
|
+
* runs, the gap to the plan's expectation in points of the target decides
|
|
267
|
+
* between on track, at risk (behind by no more than `tolerance`) and off
|
|
268
|
+
* track. Without a period or an `expected` there is no pace and no verdict
|
|
269
|
+
* short of achieved or missed.
|
|
270
|
+
*/
|
|
271
|
+
function deriveTargetProgress({ value, target, formatters, tolerance, elapsed, expected, projected, ...deltaOptions }) {
|
|
272
|
+
if (!isFinite(value) || !isFinite(target) || target === 0) return null;
|
|
273
|
+
const { goodDirection, locale, percentagePoints } = deltaOptions;
|
|
274
|
+
const ratio = value / target;
|
|
275
|
+
const good = (reading, reference) => goodDirection === "up" ? reading - reference : reference - reading;
|
|
276
|
+
const withinTarget = goodDirection === "up" ? value >= target : value <= target;
|
|
277
|
+
const gapKind = value > target ? "over" : "remaining";
|
|
278
|
+
const targetDelta = deriveTargetDelta(value, target, deltaOptions);
|
|
279
|
+
let pace = null;
|
|
280
|
+
const expectedValue = expected === null ? null : isFinite(expected) ? expected : elapsed ? target * elapsed.fraction : null;
|
|
281
|
+
if (expectedValue != null) {
|
|
282
|
+
const expectedRatio = expectedValue / target;
|
|
283
|
+
const delta = deriveTargetDelta(value, expectedValue, deltaOptions);
|
|
284
|
+
const points = formatDelta(ratio, expectedRatio, {
|
|
285
|
+
mode: "absolute",
|
|
286
|
+
format: "percent:1",
|
|
287
|
+
locale,
|
|
288
|
+
pointsSuffix: percentagePoints
|
|
289
|
+
});
|
|
290
|
+
const advantage = good(value, expectedValue);
|
|
291
|
+
pace = {
|
|
292
|
+
expected: expectedValue,
|
|
293
|
+
expectedRatio,
|
|
294
|
+
expectedText: formatters.formatValue(expectedValue),
|
|
295
|
+
expectedPercentText: formatters.formatPercent(expectedRatio),
|
|
296
|
+
delta,
|
|
297
|
+
pointsText: points.text,
|
|
298
|
+
status: advantage > 0 ? "ahead" : advantage < 0 ? "behind" : "on",
|
|
299
|
+
source: isFinite(expected) ? "explicit" : "linear"
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
let projection = null;
|
|
303
|
+
const projectedValue = projected === null ? null : isFinite(projected) ? projected : elapsed && elapsed.fraction > 0 && !elapsed.ended ? value / elapsed.fraction : null;
|
|
304
|
+
if (projectedValue != null) projection = {
|
|
305
|
+
value: projectedValue,
|
|
306
|
+
ratio: projectedValue / target,
|
|
307
|
+
text: formatters.formatValue(projectedValue),
|
|
308
|
+
percentText: formatters.formatPercent(projectedValue / target),
|
|
309
|
+
source: isFinite(projected) ? "explicit" : "linear",
|
|
310
|
+
extends: Math.sign(projectedValue) === Math.sign(value) && Math.abs(projectedValue) > Math.abs(value)
|
|
311
|
+
};
|
|
312
|
+
let track = null;
|
|
313
|
+
const ended = elapsed?.ended ?? false;
|
|
314
|
+
if (goodDirection === "up") {
|
|
315
|
+
if (value >= target) track = "achieved";
|
|
316
|
+
else if (ended) track = "missed";
|
|
317
|
+
} else if (value > target) track = "missed";
|
|
318
|
+
else if (ended) track = "achieved";
|
|
319
|
+
if (track == null && pace) {
|
|
320
|
+
const gap = good(ratio, pace.expectedRatio);
|
|
321
|
+
track = gap >= 0 ? "on-track" : gap >= -tolerance ? "at-risk" : "off-track";
|
|
322
|
+
}
|
|
323
|
+
const tone = track === "achieved" ? "positive" : track === "missed" ? "negative" : "primary";
|
|
324
|
+
const markers = [
|
|
325
|
+
0,
|
|
326
|
+
target,
|
|
327
|
+
value,
|
|
328
|
+
pace?.expected ?? 0,
|
|
329
|
+
projection?.value ?? 0
|
|
330
|
+
];
|
|
331
|
+
return {
|
|
332
|
+
value,
|
|
333
|
+
target,
|
|
334
|
+
ratio,
|
|
335
|
+
valueText: formatters.formatValue(value),
|
|
336
|
+
targetText: formatters.formatValue(target),
|
|
337
|
+
percentText: formatters.formatPercent(ratio),
|
|
338
|
+
withinTarget,
|
|
339
|
+
gapKind,
|
|
340
|
+
gapText: formatters.formatValue(Math.abs(target - value)),
|
|
341
|
+
targetDelta,
|
|
342
|
+
elapsed,
|
|
343
|
+
pace,
|
|
344
|
+
projection,
|
|
345
|
+
track,
|
|
346
|
+
tone,
|
|
347
|
+
bounds: {
|
|
348
|
+
min: Math.min(...markers),
|
|
349
|
+
max: Math.max(...markers)
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/** The word the pace badge's comparison line and explanation use for the reading's standing against the plan. */
|
|
354
|
+
function paceWord(pace, direction, labels) {
|
|
355
|
+
return pace.status === "ahead" ? labels.aheadOfPace(direction) : pace.status === "behind" ? labels.behindPace(direction) : labels.onPace;
|
|
356
|
+
}
|
|
357
|
+
/** The rows behind the badge: the reading against the plan's expectation when there is a pace, else against the target. */
|
|
358
|
+
function buildTargetExplanation(progress, direction, labels) {
|
|
359
|
+
const { pace } = progress;
|
|
360
|
+
return labels.explanation({
|
|
361
|
+
kind: pace ? "pace" : "target",
|
|
362
|
+
direction,
|
|
363
|
+
valueText: progress.valueText,
|
|
364
|
+
percentText: progress.percentText,
|
|
365
|
+
targetText: progress.targetText,
|
|
366
|
+
gapKind: progress.gapKind,
|
|
367
|
+
gapText: progress.gapText,
|
|
368
|
+
expectedText: pace?.expectedText ?? null,
|
|
369
|
+
expectedPercentText: pace?.expectedPercentText ?? null,
|
|
370
|
+
delta: pace ? pace.delta : progress.targetDelta,
|
|
371
|
+
paceStatus: pace?.status ?? null
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Every value the card shows, as label/value rows: target, reading, the gap,
|
|
376
|
+
* the plan's expectation and the pace against it, the elapsed share, the
|
|
377
|
+
* projection, the period and the verdict — the popover's table, so nothing
|
|
378
|
+
* lives only in the plot or on hover.
|
|
379
|
+
*/
|
|
380
|
+
function buildTargetDetailRows({ progress, direction, periodText, days, labels }) {
|
|
381
|
+
const rows = [
|
|
382
|
+
{
|
|
383
|
+
key: "target",
|
|
384
|
+
label: labels.target,
|
|
385
|
+
value: progress.targetText,
|
|
386
|
+
mark: "target"
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
key: "reached",
|
|
390
|
+
label: labels.reached,
|
|
391
|
+
value: `${progress.valueText} (${progress.percentText})`,
|
|
392
|
+
mark: "actual"
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
key: "gap",
|
|
396
|
+
label: progress.gapKind === "over" ? labels.overBy : labels.remaining,
|
|
397
|
+
value: progress.gapText
|
|
398
|
+
}
|
|
399
|
+
];
|
|
400
|
+
if (progress.pace) {
|
|
401
|
+
const word = paceWord(progress.pace, direction, labels);
|
|
402
|
+
rows.push({
|
|
403
|
+
key: "expected",
|
|
404
|
+
label: labels.expectedByNow,
|
|
405
|
+
value: `${progress.pace.expectedText} (${progress.pace.expectedPercentText})`,
|
|
406
|
+
mark: "pace"
|
|
407
|
+
}, {
|
|
408
|
+
key: "pace",
|
|
409
|
+
label: word.charAt(0).toUpperCase() + word.slice(1),
|
|
410
|
+
value: progress.pace.delta.secondaryText ? `${progress.pace.delta.text} (${progress.pace.delta.secondaryText})` : progress.pace.delta.text,
|
|
411
|
+
tone: progress.pace.delta.polarity
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
if (progress.projection) rows.push({
|
|
415
|
+
key: "projected",
|
|
416
|
+
label: labels.projected,
|
|
417
|
+
value: `${progress.projection.text} (${progress.projection.percentText})`,
|
|
418
|
+
mark: "projection"
|
|
419
|
+
});
|
|
420
|
+
if (periodText) rows.push({
|
|
421
|
+
key: "period",
|
|
422
|
+
label: labels.period,
|
|
423
|
+
value: periodText
|
|
424
|
+
});
|
|
425
|
+
if (progress.elapsed) rows.push({
|
|
426
|
+
key: "elapsed",
|
|
427
|
+
label: labels.elapsedRow,
|
|
428
|
+
value: progress.elapsed.percentText
|
|
429
|
+
});
|
|
430
|
+
if (days) rows.push({
|
|
431
|
+
key: "days",
|
|
432
|
+
label: labels.daysRow,
|
|
433
|
+
value: labels.days(days.elapsed, days.total)
|
|
434
|
+
});
|
|
435
|
+
if (progress.track) rows.push({
|
|
436
|
+
key: "track",
|
|
437
|
+
label: labels.trackRow,
|
|
438
|
+
value: labels.trackNames[progress.track]
|
|
439
|
+
});
|
|
440
|
+
return rows;
|
|
441
|
+
}
|
|
442
|
+
/** The ISO-8601 week number of a Gregorian date (weeks run Monday to Sunday; week 1 holds the first Thursday). */
|
|
443
|
+
function isoWeekNumber(year, month, day) {
|
|
444
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
445
|
+
date.setUTCDate(date.getUTCDate() - (date.getUTCDay() + 6) % 7 + 3);
|
|
446
|
+
const firstThursday = new Date(Date.UTC(date.getUTCFullYear(), 0, 4));
|
|
447
|
+
firstThursday.setUTCDate(firstThursday.getUTCDate() - (firstThursday.getUTCDay() + 6) % 7 + 3);
|
|
448
|
+
return 1 + Math.round((date.getTime() - firstThursday.getTime()) / (7 * DAY_MS));
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Which calendar unit the bounds describe. A period qualifies only when it
|
|
452
|
+
* opens on a zoned midnight and closes exactly one week, month, quarter or
|
|
453
|
+
* year later in the same zone — so a DST weekend still measures seven days,
|
|
454
|
+
* and a range that merely looks month-sized (`Sep 2` to `Oct 1`) stays
|
|
455
|
+
* `custom` rather than being renamed after a month it is not.
|
|
456
|
+
*/
|
|
457
|
+
function describePeriodShape(bounds, timeZone) {
|
|
458
|
+
if (!bounds.dateOnly) return "custom";
|
|
459
|
+
const start = zonedParts(bounds.start, timeZone);
|
|
460
|
+
if (start.hour !== 0 || start.minute !== 0 || start.second !== 0) return "custom";
|
|
461
|
+
const closes = (years, months, days) => zonedMidnight(start.year + years, start.month - 1 + months, start.day + days, timeZone) === bounds.end;
|
|
462
|
+
if (start.day === 1) {
|
|
463
|
+
if (start.month === 1 && closes(1, 0, 0)) return "year";
|
|
464
|
+
if ((start.month - 1) % 3 === 0 && closes(0, 3, 0)) return "quarter";
|
|
465
|
+
if (closes(0, 1, 0)) return "month";
|
|
466
|
+
}
|
|
467
|
+
if (closes(0, 0, 7)) return "week";
|
|
468
|
+
return "custom";
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Names the period and spells out its bounds. The name is the consumer's
|
|
472
|
+
* `label` when it gave one; otherwise the period's own shape supplies it
|
|
473
|
+
* (`Week 32`, `Sep 2026`, `Q3 2026`, `2026`), and a shape the calendar has no
|
|
474
|
+
* word for falls back to the range itself. Every part goes through `Intl` in
|
|
475
|
+
* the card's locale and zone, so a Thai card on the Buddhist calendar reads
|
|
476
|
+
* `ก.ย. 2569` while the arithmetic stays Gregorian.
|
|
477
|
+
*/
|
|
478
|
+
function formatPeriod(bounds, { locale, timeZone, labels = DEFAULT_TARGET_LABELS }) {
|
|
479
|
+
if (!bounds) return null;
|
|
480
|
+
const lastInstant = bounds.dateOnly ? bounds.end - DAY_MS / 2 : bounds.end;
|
|
481
|
+
const granularity = bounds.dateOnly ? "day" : "datetime";
|
|
482
|
+
const range = formatDateRange(bounds.start, lastInstant, {
|
|
483
|
+
granularity,
|
|
484
|
+
locale,
|
|
485
|
+
timeZone
|
|
486
|
+
});
|
|
487
|
+
const shape = describePeriodShape(bounds, timeZone);
|
|
488
|
+
let derived = range;
|
|
489
|
+
if (shape === "year") derived = formatDate(bounds.start, {
|
|
490
|
+
granularity: "year",
|
|
491
|
+
locale,
|
|
492
|
+
timeZone
|
|
493
|
+
});
|
|
494
|
+
else if (shape === "month") derived = formatDate(bounds.start, {
|
|
495
|
+
granularity: "month",
|
|
496
|
+
locale,
|
|
497
|
+
timeZone
|
|
498
|
+
});
|
|
499
|
+
else if (shape === "quarter") {
|
|
500
|
+
const start = zonedParts(bounds.start, timeZone);
|
|
501
|
+
derived = labels.quarter(Math.floor((start.month - 1) / 3) + 1, formatDate(bounds.start, {
|
|
502
|
+
granularity: "year",
|
|
503
|
+
locale,
|
|
504
|
+
timeZone
|
|
505
|
+
}));
|
|
506
|
+
} else if (shape === "week") {
|
|
507
|
+
const midweek = zonedParts(bounds.start + 3 * DAY_MS, timeZone);
|
|
508
|
+
derived = labels.week(isoWeekNumber(midweek.year, midweek.month, midweek.day), formatDate(bounds.start, {
|
|
509
|
+
granularity: "year",
|
|
510
|
+
locale,
|
|
511
|
+
timeZone
|
|
512
|
+
}));
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
name: bounds.label ?? derived,
|
|
516
|
+
shape,
|
|
517
|
+
labelled: bounds.label != null,
|
|
518
|
+
range
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
const stripSign = (text) => text.replace(/^[+\-−]/, "");
|
|
522
|
+
/**
|
|
523
|
+
* The card's text equivalent (Amy Cesal's alt-text formula: what it is, what
|
|
524
|
+
* it shows, why it is here, when it was true) — the figure's accessible
|
|
525
|
+
* description, and the summary a screen reader gets before the parts.
|
|
526
|
+
*/
|
|
527
|
+
function buildTargetDescription({ name, state, valueText, progress, direction, periodText, asOfText, labels }) {
|
|
528
|
+
if (state === "loading") return `${labels.loading(name)}.`;
|
|
529
|
+
if (state === "error") return `${name}: ${labels.error}.`;
|
|
530
|
+
if (state === "empty") return `${name}: ${labels.noData}.`;
|
|
531
|
+
if (state === "no-match") return `${name}: ${labels.noMatch}.`;
|
|
532
|
+
const sentences = [];
|
|
533
|
+
if (!progress) sentences.push(`${name}: ${valueText}.`, `${labels.noTarget}.`);
|
|
534
|
+
else {
|
|
535
|
+
sentences.push(labels.attainmentSentence({
|
|
536
|
+
name,
|
|
537
|
+
percent: progress.percentText,
|
|
538
|
+
value: progress.valueText,
|
|
539
|
+
target: progress.targetText
|
|
540
|
+
}), labels.gapSentence({
|
|
541
|
+
kind: progress.gapKind,
|
|
542
|
+
amount: progress.gapText
|
|
543
|
+
}));
|
|
544
|
+
if (progress.pace) {
|
|
545
|
+
const flat = progress.pace.delta.direction === "flat";
|
|
546
|
+
sentences.push(labels.paceSentence({
|
|
547
|
+
word: paceWord(progress.pace, direction, labels),
|
|
548
|
+
primary: flat ? "" : stripSign(progress.pace.delta.text),
|
|
549
|
+
secondary: flat || !progress.pace.delta.secondaryText ? null : stripSign(progress.pace.delta.secondaryText),
|
|
550
|
+
expectedPercent: progress.pace.expectedPercentText,
|
|
551
|
+
elapsedPercent: progress.elapsed?.percentText ?? null
|
|
552
|
+
}));
|
|
553
|
+
}
|
|
554
|
+
if (progress.track) sentences.push(`${labels.trackNames[progress.track]}.`);
|
|
555
|
+
if (progress.projection) sentences.push(labels.projectionSentence({
|
|
556
|
+
value: progress.projection.text,
|
|
557
|
+
percent: progress.projection.percentText
|
|
558
|
+
}));
|
|
559
|
+
}
|
|
560
|
+
if (periodText) sentences.push(labels.periodSentence({
|
|
561
|
+
name: periodText.name,
|
|
562
|
+
range: periodText.range
|
|
563
|
+
}));
|
|
564
|
+
if (asOfText) sentences.push(`${labels.asOf} ${asOfText}.`);
|
|
565
|
+
return sentences.join(" ");
|
|
566
|
+
}
|
|
567
|
+
/** The bullet graph's own `description` — the `aria-label` Vega stamps on the `graphics-document`. */
|
|
568
|
+
function buildTargetChartDescription({ name, progress, labels }) {
|
|
569
|
+
return labels.chartDescription({
|
|
570
|
+
name,
|
|
571
|
+
value: progress.valueText,
|
|
572
|
+
percent: progress.percentText,
|
|
573
|
+
target: progress.targetText,
|
|
574
|
+
expected: progress.pace?.expectedText ?? null,
|
|
575
|
+
expectedPercent: progress.pace?.expectedPercentText ?? null,
|
|
576
|
+
projected: progress.projection?.text ?? null,
|
|
577
|
+
projectedPercent: progress.projection?.percentText ?? null
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
//#endregion
|
|
581
|
+
export { DEFAULT_TARGET_LABELS, buildTargetChartDescription, buildTargetDescription, buildTargetDetailRows, buildTargetExplanation, createTargetFormatters, deriveDays, deriveElapsed, deriveTargetDelta, deriveTargetProgress, deriveTargetState, describePeriodShape, formatPeriod, formatValuePair, isoWeekNumber, paceWord, periodDays, resolvePeriod, resolveTargetLabels };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { BuildTargetDescriptionInput, BuildTargetDetailRowsInput, DEFAULT_TARGET_LABELS, DeriveTargetDeltaOptions, DeriveTargetProgressInput, DeriveTargetStateInput, TargetBounds, TargetCardLabels, TargetCardSize, TargetCardState, TargetCardStatus, TargetDelta, TargetDeltaMode, TargetDetailRow, TargetElapsed, TargetExplanationInput, TargetGoodDirection, TargetHeadline, TargetPace, TargetPaceStatus, TargetPeriod, TargetPeriodBounds, TargetPolarity, TargetProgress, TargetProjection, TargetTextFormatters, TargetTone, TargetTrack, buildTargetChartDescription, buildTargetDescription, buildTargetDetailRows, buildTargetExplanation, createTargetFormatters, deriveElapsed, deriveTargetDelta, deriveTargetProgress, deriveTargetState, formatPeriod, formatValuePair, paceWord, periodDays, resolvePeriod, resolveTargetLabels } from "./TargetCard.model.js";
|
|
2
|
+
import { TargetCard, TargetCardProps } from "./TargetCard.js";
|
|
3
|
+
import { 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 } from "./TargetCard.chart.js";
|
|
4
|
+
export { type BuildTargetDescriptionInput, type BuildTargetDetailRowsInput, DEFAULT_TARGET_LABELS, type DeriveTargetDeltaOptions, type DeriveTargetProgressInput, type DeriveTargetStateInput, 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, type TargetBounds, type TargetBulletSpecOptions, TargetCard, type TargetCardLabels, type TargetCardProps, type TargetCardSize, type TargetCardState, type TargetCardStatus, type TargetDelta, type TargetDeltaMode, type TargetDetailRow, type TargetElapsed, type TargetExplanationInput, type TargetGoodDirection, type TargetHeadline, type TargetPace, type TargetPaceStatus, type TargetPeriod, type TargetPeriodBounds, type TargetPolarity, type TargetProgress, type TargetProjection, type TargetTextFormatters, type TargetTone, type TargetTrack, buildTargetBulletSpec, buildTargetChartDescription, buildTargetDescription, buildTargetDetailRows, buildTargetExplanation, createTargetFormatters, deriveElapsed, deriveTargetDelta, deriveTargetProgress, deriveTargetState, formatPeriod, formatValuePair, paceWord, periodDays, resolvePeriod, resolveTargetLabels };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { 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, buildTargetBulletSpec } from "./TargetCard.chart.js";
|
|
2
|
+
import { DEFAULT_TARGET_LABELS, buildTargetChartDescription, buildTargetDescription, buildTargetDetailRows, buildTargetExplanation, createTargetFormatters, deriveElapsed, deriveTargetDelta, deriveTargetProgress, deriveTargetState, formatPeriod, formatValuePair, paceWord, periodDays, resolvePeriod, resolveTargetLabels } from "./TargetCard.model.js";
|
|
3
|
+
import { TargetCard } from "./TargetCard.js";
|
|
4
|
+
export { DEFAULT_TARGET_LABELS, 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, TargetCard, buildTargetBulletSpec, buildTargetChartDescription, buildTargetDescription, buildTargetDetailRows, buildTargetExplanation, createTargetFormatters, deriveElapsed, deriveTargetDelta, deriveTargetProgress, deriveTargetState, formatPeriod, formatValuePair, paceWord, periodDays, resolvePeriod, resolveTargetLabels };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ChartTokens } from "../../lib/vega/tokens.js";
|
|
2
|
+
import { TopLevelSpec } from "vega-lite";
|
|
3
|
+
//#region src/components/TreeCard/TreeCard.chart.d.ts
|
|
4
|
+
/** The visible rows: `{ key, label, depth, value, share, role, inFocus }` per `TreeRow`, in display order. */
|
|
5
|
+
declare const TREE_TABLE_DATASET = "table";
|
|
6
|
+
/** Zero or one row `{ value: 0 }` — a hairline at zero, pushed only when a visible value is negative. */
|
|
7
|
+
declare const TREE_ZERO_DATASET = "zero";
|
|
8
|
+
/** The hovered row's point selection. */
|
|
9
|
+
declare const TREE_HOVER_PARAM = "hover";
|
|
10
|
+
/**
|
|
11
|
+
* The upper bound of the shared value scale — the tree's total, pushed as a
|
|
12
|
+
* signal.
|
|
13
|
+
*
|
|
14
|
+
* Pinning the domain to the whole rather than to the tallest visible bar is
|
|
15
|
+
* what makes a bar's length read as its share of the total at every depth,
|
|
16
|
+
* whether or not the total is itself a row. It rides a signal, not the spec,
|
|
17
|
+
* because the total is data: baking it into the spec would re-embed the view
|
|
18
|
+
* on every refresh.
|
|
19
|
+
*/
|
|
20
|
+
declare const TREE_TOTAL_PARAM = "treeTotal";
|
|
21
|
+
declare const TREE_MAX_BAR_HEIGHT = 24;
|
|
22
|
+
/**
|
|
23
|
+
* Depth reads through the sequential ramp's top steps, darkest at the root.
|
|
24
|
+
*
|
|
25
|
+
* Only three: the ramp's lighter steps (`chart-sequential-1…4`) do not clear
|
|
26
|
+
* WCAG 1.4.11's 3:1 against `background` — `primary-400` measures 2.7:1 on
|
|
27
|
+
* white — so a fourth shade would buy a depth cue by making the deepest bars
|
|
28
|
+
* unreadable. Depth past the third level therefore repeats the third step;
|
|
29
|
+
* indentation, the disclosure control and `aria-level` carry the rest, so the
|
|
30
|
+
* hierarchy never depends on the ramp alone.
|
|
31
|
+
*/
|
|
32
|
+
declare const TREE_DEPTH_STEPS: readonly [6, 5, 4];
|
|
33
|
+
/** The exact hue a row's bar wears — the swatches in the legend, chips and tooltip read from this too. */
|
|
34
|
+
declare function treeBarColor(tokens: ChartTokens, { depth, role, inFocus }: {
|
|
35
|
+
depth: number;
|
|
36
|
+
role?: string;
|
|
37
|
+
inFocus?: boolean;
|
|
38
|
+
}, colorBy: "depth" | "emphasis", emphasize: boolean): string;
|
|
39
|
+
interface TreeBarsSpecOptions {
|
|
40
|
+
/** Bar thickness in pixels, capped at 24. Centered in each row's band. */
|
|
41
|
+
barHeight: number;
|
|
42
|
+
/** Initial plot height — rows × step. The live value rides the View API. */
|
|
43
|
+
height: number;
|
|
44
|
+
/**
|
|
45
|
+
* Draw a hairline at zero (the `zero` dataset carries the one row) — only
|
|
46
|
+
* when a visible value is negative: for all-positive bars the left edge is
|
|
47
|
+
* the baseline already, and a hairline there would read as a border.
|
|
48
|
+
* Structural rather than data-driven because Vega warns about the infinite
|
|
49
|
+
* extent of an empty dataset that feeds a scale.
|
|
50
|
+
*/
|
|
51
|
+
zeroRule: boolean;
|
|
52
|
+
/** Wire the hover selection and the tooltip channel. */
|
|
53
|
+
tooltip: boolean;
|
|
54
|
+
/** `depth` ramps the sequential steps by level; `emphasis` paints the selected branch and grays the rest. */
|
|
55
|
+
colorBy: "depth" | "emphasis";
|
|
56
|
+
/** A row is selected, so `emphasis` has a branch to paint. Without one every bar takes the emphasis hue. */
|
|
57
|
+
emphasize: boolean;
|
|
58
|
+
/** Tooltip titles. */
|
|
59
|
+
itemTitle: string;
|
|
60
|
+
valueTitle: string;
|
|
61
|
+
shareTitle: string;
|
|
62
|
+
/** `arcNumber` grammar for values (`currency:THB:compact@en-US`). */
|
|
63
|
+
valueFormat: string;
|
|
64
|
+
/** `arcNumber` grammar for shares (`percent:0@en-US`). */
|
|
65
|
+
shareFormat: string;
|
|
66
|
+
locale?: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Builds the bars: one horizontal bar per visible row on a shared zero-based
|
|
70
|
+
* scale (length compares magnitude — Cleveland & McGill's second-best channel
|
|
71
|
+
* — and, because the whole is on the same scale, reads as share of it), the
|
|
72
|
+
* fill carrying depth through the sequential ramp or the selected branch
|
|
73
|
+
* through the emphasis hue, a hairline at zero once a value is negative, and
|
|
74
|
+
* a hover selection that keeps the pointed bar solid while its siblings
|
|
75
|
+
* recede. Every band has no padding, so the bar centers line up with the DOM
|
|
76
|
+
* rows that carry the names, values and shares.
|
|
77
|
+
*/
|
|
78
|
+
declare function buildTreeBarsSpec(options: TreeBarsSpecOptions, tokens: ChartTokens): TopLevelSpec;
|
|
79
|
+
//#endregion
|
|
80
|
+
export { TREE_DEPTH_STEPS, TREE_HOVER_PARAM, TREE_MAX_BAR_HEIGHT, TREE_TABLE_DATASET, TREE_TOTAL_PARAM, TREE_ZERO_DATASET, TreeBarsSpecOptions, buildTreeBarsSpec, treeBarColor };
|