@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,399 @@
|
|
|
1
|
+
import { formatNumber } from "../../lib/vega/formatters.js";
|
|
2
|
+
//#region src/components/ReportCard/ReportCard.model.ts
|
|
3
|
+
/**
|
|
4
|
+
* The default ladder: a four-step grade on a 0–100 scale, lettered for the
|
|
5
|
+
* row column and worded for the headline. `Good` is neutral on purpose —
|
|
6
|
+
* meeting the standard is not a celebration; only `Excellent` earns the
|
|
7
|
+
* positive hue and only `Poor` the negative one.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_REPORT_BANDS = [
|
|
10
|
+
{
|
|
11
|
+
key: "excellent",
|
|
12
|
+
label: "Excellent",
|
|
13
|
+
short: "A",
|
|
14
|
+
min: 90,
|
|
15
|
+
tone: "positive"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
key: "good",
|
|
19
|
+
label: "Good",
|
|
20
|
+
short: "B",
|
|
21
|
+
min: 75,
|
|
22
|
+
tone: "neutral"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
key: "fair",
|
|
26
|
+
label: "Fair",
|
|
27
|
+
short: "C",
|
|
28
|
+
min: 60,
|
|
29
|
+
tone: "warning"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: "poor",
|
|
33
|
+
label: "Poor",
|
|
34
|
+
short: "D",
|
|
35
|
+
min: 0,
|
|
36
|
+
tone: "negative"
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
/** The key of the row the long tail folds into. Never collides with a consumer key that is a label. */
|
|
40
|
+
const REPORT_OTHER_KEY = "__other__";
|
|
41
|
+
const DEFAULT_DOMAIN = [0, 100];
|
|
42
|
+
const isFinite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
43
|
+
const clamp = (value, min, max) => value < min ? min : value > max ? max : value;
|
|
44
|
+
const DEFAULT_REPORT_LABELS = {
|
|
45
|
+
criteriaCount: (count) => `across ${count} ${count === 1 ? "criterion" : "criteria"}`,
|
|
46
|
+
versusPrevious: "vs previous",
|
|
47
|
+
increase: "Up",
|
|
48
|
+
decrease: "Down",
|
|
49
|
+
unchanged: "No change",
|
|
50
|
+
points: " pts",
|
|
51
|
+
outOf: (ceiling) => `/${ceiling}`,
|
|
52
|
+
notMeasured: (unscored, total) => `${unscored} of ${total} not measured`,
|
|
53
|
+
rowDetail: ({ label, scoreText, gradeLabel, valueText, targetText, targetScoreText, weightText, note }) => {
|
|
54
|
+
const parts = [`${label} ${scoreText}${gradeLabel ? ` · ${gradeLabel}` : ""}`];
|
|
55
|
+
if (valueText) parts.push(targetText ? `${valueText} of ${targetText}` : valueText);
|
|
56
|
+
if (targetScoreText) parts.push(`expected ${targetScoreText}`);
|
|
57
|
+
if (weightText) parts.push(weightText);
|
|
58
|
+
if (note) parts.push(note);
|
|
59
|
+
return parts.join(" · ");
|
|
60
|
+
},
|
|
61
|
+
unscored: "Not measured",
|
|
62
|
+
noData: "No data",
|
|
63
|
+
noMatch: "No matching data",
|
|
64
|
+
clearFilters: "Clear filters",
|
|
65
|
+
error: "Couldn't load this report",
|
|
66
|
+
retry: "Try again",
|
|
67
|
+
insufficient: "No criteria scored yet",
|
|
68
|
+
stale: "Stale",
|
|
69
|
+
asOf: "As of",
|
|
70
|
+
tableCriterion: "Criterion",
|
|
71
|
+
tableValue: "Value",
|
|
72
|
+
tableTarget: "Target",
|
|
73
|
+
tableScore: "Score",
|
|
74
|
+
tableGrade: "Grade",
|
|
75
|
+
tableWeight: "Weight",
|
|
76
|
+
tableShortfall: "Points lost",
|
|
77
|
+
tableOverall: "Overall",
|
|
78
|
+
other: "Other criteria",
|
|
79
|
+
bandsLegend: "Grades",
|
|
80
|
+
bandRange: (from, to) => `${from}–${to}`,
|
|
81
|
+
expected: "Expected",
|
|
82
|
+
selected: "selected",
|
|
83
|
+
weight: "weight",
|
|
84
|
+
showTable: (name) => `Show the data behind ${name}`,
|
|
85
|
+
hideTable: (name) => `Hide the data behind ${name}`,
|
|
86
|
+
about: (name) => `About ${name}`,
|
|
87
|
+
loading: (name) => `Loading ${name}`,
|
|
88
|
+
tableCaption: (name) => `Data behind the ${name} report`,
|
|
89
|
+
criteriaGroup: (name) => `Choose the criterion ${name} details`,
|
|
90
|
+
overallSentence: ({ scoreText, gradeLabel, scored, unscored, weighted, source }) => {
|
|
91
|
+
const grade = gradeLabel ? `, ${gradeLabel}` : "";
|
|
92
|
+
const how = source === "given" ? "" : source === "min" ? " (the weakest criterion)" : weighted ? " (weighted)" : "";
|
|
93
|
+
const missing = unscored > 0 ? ` ${unscored} not measured.` : "";
|
|
94
|
+
return `Overall ${scoreText}${grade}${how} from ${scored} scored ${scored === 1 ? "criterion" : "criteria"}.${missing}`;
|
|
95
|
+
},
|
|
96
|
+
criteriaSummary: (count, list) => `${count} ${count === 1 ? "criterion" : "criteria"}: ${list}.`,
|
|
97
|
+
foldedSummary: (count) => `${count} more folded into Other criteria.`,
|
|
98
|
+
changeSentence: ({ word, points, previous, previousLabel }) => points ? `${word} ${points} ${previousLabel} (${previous}).` : `${word} ${previousLabel} (${previous}).`,
|
|
99
|
+
chartDescription: ({ name, count, ceiling, best, bestScore, worst, worstScore, bands }) => `Bullet chart of ${name}: ${count} scored ${count === 1 ? "criterion" : "criteria"} out of ${ceiling} against the ${bands} bands, ` + (count === 1 || best === worst ? `${best} ${bestScore}.` : `strongest ${best} ${bestScore}, weakest ${worst} ${worstScore}.`),
|
|
100
|
+
insightMark: "Insight"
|
|
101
|
+
};
|
|
102
|
+
function resolveReportLabels(overrides) {
|
|
103
|
+
return overrides ? {
|
|
104
|
+
...DEFAULT_REPORT_LABELS,
|
|
105
|
+
...overrides
|
|
106
|
+
} : DEFAULT_REPORT_LABELS;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The state machine. Error wins; loading over existing criteria is a refresh
|
|
110
|
+
* (the previous render stays under the veil); criteria with no scores at all
|
|
111
|
+
* is `insufficient` — the rows are real, the grade is not; nothing at all is
|
|
112
|
+
* `no-match` when the consumer's filter caused it and `empty` otherwise.
|
|
113
|
+
*/
|
|
114
|
+
function deriveReportState({ status, hasScore, hasData, filtered }) {
|
|
115
|
+
if (status === "error") return "error";
|
|
116
|
+
if (status === "loading") return hasData ? "refreshing" : "loading";
|
|
117
|
+
if (hasScore) return "ready";
|
|
118
|
+
if (hasData) return "insufficient";
|
|
119
|
+
return filtered ? "no-match" : "empty";
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolves the ladder against the scale: bands sorted low to high, each
|
|
123
|
+
* running up to the next one's floor and the last to the ceiling, clipped to
|
|
124
|
+
* the plotted domain so a stripe never paints outside the axis. Bands wholly
|
|
125
|
+
* below the floor or above the ceiling of the plotted domain are dropped.
|
|
126
|
+
*/
|
|
127
|
+
function normalizeBands(bands, domain) {
|
|
128
|
+
const [min, max] = domain;
|
|
129
|
+
const source = (bands && bands.length > 0 ? bands : DEFAULT_REPORT_BANDS).filter((band) => isFinite(band.min)).slice().sort((a, b) => a.min - b.min);
|
|
130
|
+
const spans = [];
|
|
131
|
+
source.forEach((band, index) => {
|
|
132
|
+
const next = source[index + 1];
|
|
133
|
+
const from = clamp(band.min, min, max);
|
|
134
|
+
const to = clamp(next ? next.min : max, min, max);
|
|
135
|
+
if (to <= from) return;
|
|
136
|
+
spans.push({
|
|
137
|
+
key: band.key ?? band.label,
|
|
138
|
+
label: band.label,
|
|
139
|
+
short: band.short ?? band.label,
|
|
140
|
+
tone: band.tone ?? "neutral",
|
|
141
|
+
from,
|
|
142
|
+
to,
|
|
143
|
+
index: spans.length
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
return spans;
|
|
147
|
+
}
|
|
148
|
+
/** The band a score falls in — the highest band whose floor it reaches; the lowest band catches anything under the ladder. */
|
|
149
|
+
function findBand(bands, score) {
|
|
150
|
+
if (!isFinite(score) || bands.length === 0) return null;
|
|
151
|
+
for (let index = bands.length - 1; index >= 0; index -= 1) if (score >= bands[index].from) return bands[index];
|
|
152
|
+
return bands[0];
|
|
153
|
+
}
|
|
154
|
+
/** The score a criterion resolves to, and the route it took. */
|
|
155
|
+
function resolveScore(criterion, { goodDirection = "up", ceiling = 100 } = {}) {
|
|
156
|
+
if (isFinite(criterion.score)) return {
|
|
157
|
+
score: criterion.score,
|
|
158
|
+
source: "score"
|
|
159
|
+
};
|
|
160
|
+
if (!isFinite(criterion.value)) return {
|
|
161
|
+
score: null,
|
|
162
|
+
source: "none"
|
|
163
|
+
};
|
|
164
|
+
const value = criterion.value;
|
|
165
|
+
const direction = criterion.goodDirection ?? goodDirection;
|
|
166
|
+
if (isFinite(criterion.target) && criterion.target !== 0) {
|
|
167
|
+
const ratio = direction === "down" ? criterion.target / value : value / criterion.target;
|
|
168
|
+
return {
|
|
169
|
+
score: Number.isFinite(ratio) ? ratio * ceiling : null,
|
|
170
|
+
source: "target"
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const range = criterion.range;
|
|
174
|
+
if (range && isFinite(range[0]) && isFinite(range[1]) && range[0] !== range[1]) {
|
|
175
|
+
const [worst, best] = range;
|
|
176
|
+
return {
|
|
177
|
+
score: (value - worst) / (best - worst) * ceiling,
|
|
178
|
+
source: "range"
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
score: null,
|
|
183
|
+
source: "none"
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Normalises `criteria` for the plot, the rows and the table: keys default to
|
|
188
|
+
* labels (a repeated key is suffixed so two rows never share a band), each
|
|
189
|
+
* criterion resolves to one score on the declared scale, the plotted domain
|
|
190
|
+
* widens to hold any score past the ends (a reading at 140% of target extends
|
|
191
|
+
* the axis rather than clipping to a full bar), the ladder is resolved and
|
|
192
|
+
* clipped to it, rows are sorted if asked, the tail past `maxCriteria` folds
|
|
193
|
+
* into one `Other` row whose score is the *weighted mean* of the folded rows
|
|
194
|
+
* (a mean folds honestly where a sum would not), and every row's contribution
|
|
195
|
+
* and weighted shortfall are computed against the total weight. Returns `null`
|
|
196
|
+
* when there are no criteria to speak of.
|
|
197
|
+
*/
|
|
198
|
+
function normalizeCriteria(criteria, { bands, scoreDomain = DEFAULT_DOMAIN, goodDirection = "up", passing, overallMode = "weighted", overall, sort = "none", maxCriteria, otherLabel }) {
|
|
199
|
+
if (!criteria) return null;
|
|
200
|
+
const floor = isFinite(scoreDomain[0]) ? scoreDomain[0] : 0;
|
|
201
|
+
const ceiling = isFinite(scoreDomain[1]) && scoreDomain[1] > floor ? scoreDomain[1] : floor + 100;
|
|
202
|
+
const fallbackTick = isFinite(passing) ? passing : null;
|
|
203
|
+
const seen = /* @__PURE__ */ new Map();
|
|
204
|
+
let drafts = criteria.map((criterion) => {
|
|
205
|
+
const base = criterion.key ?? criterion.label;
|
|
206
|
+
const repeats = seen.get(base) ?? 0;
|
|
207
|
+
seen.set(base, repeats + 1);
|
|
208
|
+
const { score, source } = resolveScore(criterion, {
|
|
209
|
+
goodDirection,
|
|
210
|
+
ceiling
|
|
211
|
+
});
|
|
212
|
+
const weight = isFinite(criterion.weight) && criterion.weight > 0 ? criterion.weight : 1;
|
|
213
|
+
return {
|
|
214
|
+
key: repeats === 0 ? base : `${base}·${repeats + 1}`,
|
|
215
|
+
label: criterion.label,
|
|
216
|
+
role: "criterion",
|
|
217
|
+
score,
|
|
218
|
+
source,
|
|
219
|
+
value: isFinite(criterion.value) ? criterion.value : null,
|
|
220
|
+
target: isFinite(criterion.target) ? criterion.target : null,
|
|
221
|
+
targetScore: isFinite(criterion.targetScore) ? criterion.targetScore : fallbackTick,
|
|
222
|
+
weight,
|
|
223
|
+
format: criterion.format,
|
|
224
|
+
note: criterion.note
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
if (sort !== "none") {
|
|
228
|
+
const rank = (draft) => {
|
|
229
|
+
if (draft.score == null) return null;
|
|
230
|
+
if (sort === "impact") return (ceiling - draft.score) * draft.weight;
|
|
231
|
+
return draft.score;
|
|
232
|
+
};
|
|
233
|
+
const direction = sort === "best" || sort === "impact" ? -1 : 1;
|
|
234
|
+
drafts = drafts.map((draft, index) => ({
|
|
235
|
+
draft,
|
|
236
|
+
index
|
|
237
|
+
})).sort((a, b) => {
|
|
238
|
+
const av = rank(a.draft);
|
|
239
|
+
const bv = rank(b.draft);
|
|
240
|
+
if (av == null && bv == null) return a.index - b.index;
|
|
241
|
+
if (av == null) return 1;
|
|
242
|
+
if (bv == null) return -1;
|
|
243
|
+
return av === bv ? a.index - b.index : (av - bv) * direction;
|
|
244
|
+
}).map(({ draft }) => draft);
|
|
245
|
+
}
|
|
246
|
+
let folded = 0;
|
|
247
|
+
if (maxCriteria != null && maxCriteria >= 1 && drafts.length > maxCriteria) {
|
|
248
|
+
const keepCount = Math.max(0, maxCriteria - 1);
|
|
249
|
+
const kept = drafts.slice(0, keepCount);
|
|
250
|
+
const rest = drafts.slice(keepCount);
|
|
251
|
+
if (rest.length >= 2) {
|
|
252
|
+
const scored = rest.filter((draft) => draft.score != null);
|
|
253
|
+
const weight = rest.reduce((total, draft) => total + draft.weight, 0);
|
|
254
|
+
const scoredWeight = scored.reduce((total, draft) => total + draft.weight, 0);
|
|
255
|
+
const score = scored.length > 0 && scoredWeight > 0 ? scored.reduce((total, draft) => total + draft.score * draft.weight, 0) / scoredWeight : null;
|
|
256
|
+
drafts = [...kept, {
|
|
257
|
+
key: REPORT_OTHER_KEY,
|
|
258
|
+
label: otherLabel,
|
|
259
|
+
role: "other",
|
|
260
|
+
score,
|
|
261
|
+
source: score == null ? "none" : "score",
|
|
262
|
+
value: null,
|
|
263
|
+
target: null,
|
|
264
|
+
targetScore: fallbackTick,
|
|
265
|
+
weight
|
|
266
|
+
}];
|
|
267
|
+
folded = rest.length;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const scores = drafts.map((draft) => draft.score).filter(isFinite);
|
|
271
|
+
const domain = [Math.min(floor, ...scores), Math.max(ceiling, ...scores)];
|
|
272
|
+
const spans = normalizeBands(bands, domain);
|
|
273
|
+
const totalWeight = drafts.filter((draft) => draft.score != null).reduce((total, draft) => total + draft.weight, 0);
|
|
274
|
+
const rows = drafts.map((draft) => {
|
|
275
|
+
const band = findBand(spans, draft.score);
|
|
276
|
+
return {
|
|
277
|
+
...draft,
|
|
278
|
+
plotted: draft.score == null ? null : clamp(draft.score, domain[0], domain[1]),
|
|
279
|
+
band,
|
|
280
|
+
tone: band?.tone ?? "neutral",
|
|
281
|
+
contribution: draft.score != null && totalWeight > 0 ? draft.score * draft.weight / totalWeight : null,
|
|
282
|
+
shortfall: draft.score != null && totalWeight > 0 ? (ceiling - draft.score) * draft.weight / totalWeight : null
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
const scoredRows = rows.filter((row) => row.score != null);
|
|
286
|
+
let best = null;
|
|
287
|
+
let worst = null;
|
|
288
|
+
for (const row of scoredRows) {
|
|
289
|
+
if (!best || row.score > best.score) best = row;
|
|
290
|
+
if (!worst || row.score < worst.score) worst = row;
|
|
291
|
+
}
|
|
292
|
+
let overallScore = null;
|
|
293
|
+
let source = overallMode;
|
|
294
|
+
if (isFinite(overall)) {
|
|
295
|
+
overallScore = overall;
|
|
296
|
+
source = "given";
|
|
297
|
+
} else if (overallMode === "weighted" && totalWeight > 0) overallScore = scoredRows.reduce((total, row) => total + row.score * row.weight, 0) / totalWeight;
|
|
298
|
+
else if (overallMode === "min" && scoredRows.length > 0) overallScore = worst?.score ?? null;
|
|
299
|
+
const weights = new Set(scoredRows.map((row) => row.weight));
|
|
300
|
+
return {
|
|
301
|
+
rows,
|
|
302
|
+
bands: spans,
|
|
303
|
+
domain,
|
|
304
|
+
floor,
|
|
305
|
+
ceiling,
|
|
306
|
+
best,
|
|
307
|
+
worst,
|
|
308
|
+
folded,
|
|
309
|
+
overall: {
|
|
310
|
+
score: overallScore,
|
|
311
|
+
band: findBand(spans, overallScore),
|
|
312
|
+
source,
|
|
313
|
+
scored: scoredRows.length,
|
|
314
|
+
unscored: rows.length - scoredRows.length,
|
|
315
|
+
weight: totalWeight,
|
|
316
|
+
weighted: weights.size > 1
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/** The overall score against `previousScore`, in points. A score is always up-is-good, so the polarity follows the direction. */
|
|
321
|
+
function deriveReportDelta(score, previous, { scoreFormat = "number:0", locale } = {}) {
|
|
322
|
+
if (!isFinite(score) || !isFinite(previous)) return null;
|
|
323
|
+
const points = score - previous;
|
|
324
|
+
const direction = points > 0 ? "up" : points < 0 ? "down" : "flat";
|
|
325
|
+
return {
|
|
326
|
+
direction,
|
|
327
|
+
polarity: direction === "up" ? "positive" : direction === "down" ? "negative" : "neutral",
|
|
328
|
+
points,
|
|
329
|
+
text: formatNumber(points, scoreFormat, locale, { signDisplay: "exceptZero" })
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/** The formatters every text surface shares — headline, rows, detail line, table, tooltip, descriptions. */
|
|
333
|
+
function createReportFormatters({ scoreFormat = "number:0", format = "compact", locale, formatScore, formatValue }) {
|
|
334
|
+
return {
|
|
335
|
+
formatScore: (score) => isFinite(score) ? formatScore ? formatScore(score) : formatNumber(score, scoreFormat, locale) : "—",
|
|
336
|
+
formatValue: (value, own) => isFinite(value) ? formatValue ? formatValue(value) : formatNumber(value, own ?? format, locale) : "—",
|
|
337
|
+
formatWeight: (weight) => weight === 1 ? null : `×${formatNumber(weight, "number:2", locale)}`
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const stripSign = (text) => text.replace(/^[+\-−]/, "");
|
|
341
|
+
/**
|
|
342
|
+
* The card's text equivalent (Amy Cesal's alt-text formula: what it is, what
|
|
343
|
+
* it shows, why it is here, when it was true) — the figure's accessible
|
|
344
|
+
* description, and the summary a screen reader gets before the parts. Every
|
|
345
|
+
* criterion is listed with its score and grade, so nothing lives only in the
|
|
346
|
+
* bars, and an unmeasured criterion is named as unmeasured rather than
|
|
347
|
+
* silently missing.
|
|
348
|
+
*/
|
|
349
|
+
function buildReportDescription({ name, state, summary, delta, previousScore, previousLabel, formatters, asOfText, labels }) {
|
|
350
|
+
if (state === "loading") return `${labels.loading(name)}.`;
|
|
351
|
+
if (state === "error") return `${name}: ${labels.error}.`;
|
|
352
|
+
if (state === "empty") return `${name}: ${labels.noData}.`;
|
|
353
|
+
if (state === "no-match") return `${name}: ${labels.noMatch}.`;
|
|
354
|
+
const sentences = [`${name}.`];
|
|
355
|
+
if (state === "insufficient") sentences.push(`${labels.insufficient}.`);
|
|
356
|
+
else if (summary) {
|
|
357
|
+
const { overall, ceiling } = summary;
|
|
358
|
+
const outOf = summary.floor === 0 ? labels.outOf(formatters.formatScore(ceiling)) : "";
|
|
359
|
+
sentences.push(labels.overallSentence({
|
|
360
|
+
scoreText: `${formatters.formatScore(overall.score)}${outOf}`,
|
|
361
|
+
gradeLabel: overall.band?.label ?? null,
|
|
362
|
+
scored: overall.scored,
|
|
363
|
+
unscored: overall.unscored,
|
|
364
|
+
weighted: overall.weighted,
|
|
365
|
+
source: overall.source
|
|
366
|
+
}));
|
|
367
|
+
if (delta && isFinite(previousScore)) {
|
|
368
|
+
const word = delta.direction === "up" ? labels.increase : delta.direction === "down" ? labels.decrease : labels.unchanged;
|
|
369
|
+
sentences.push(labels.changeSentence({
|
|
370
|
+
word,
|
|
371
|
+
points: delta.direction === "flat" ? "" : `${stripSign(delta.text)}${labels.points}`,
|
|
372
|
+
previous: formatters.formatScore(previousScore),
|
|
373
|
+
previousLabel: previousLabel ?? labels.versusPrevious
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (summary && summary.rows.length > 0) {
|
|
378
|
+
const list = summary.rows.map((row) => row.score == null ? `${row.label} ${labels.unscored}` : `${row.label} ${formatters.formatScore(row.score)}${row.band ? ` ${row.band.label}` : ""}`).join(", ");
|
|
379
|
+
sentences.push(labels.criteriaSummary(summary.rows.length, list));
|
|
380
|
+
if (summary.folded > 0) sentences.push(labels.foldedSummary(summary.folded));
|
|
381
|
+
}
|
|
382
|
+
if (asOfText) sentences.push(`${labels.asOf} ${asOfText}.`);
|
|
383
|
+
return sentences.join(" ");
|
|
384
|
+
}
|
|
385
|
+
/** The plot's own `description` — the `aria-label` Vega stamps on the `graphics-document`. */
|
|
386
|
+
function buildReportChartDescription({ name, summary, formatters, labels }) {
|
|
387
|
+
return labels.chartDescription({
|
|
388
|
+
name,
|
|
389
|
+
count: summary.overall.scored,
|
|
390
|
+
ceiling: formatters.formatScore(summary.ceiling),
|
|
391
|
+
best: summary.best?.label ?? "—",
|
|
392
|
+
bestScore: formatters.formatScore(summary.best?.score),
|
|
393
|
+
worst: summary.worst?.label ?? "—",
|
|
394
|
+
worstScore: formatters.formatScore(summary.worst?.score),
|
|
395
|
+
bands: summary.bands.map((band) => band.label).join(", ")
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
//#endregion
|
|
399
|
+
export { DEFAULT_REPORT_BANDS, DEFAULT_REPORT_LABELS, REPORT_OTHER_KEY, buildReportChartDescription, buildReportDescription, createReportFormatters, deriveReportDelta, deriveReportState, findBand, normalizeBands, normalizeCriteria, resolveReportLabels, resolveScore };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { REPORT_BANDS_DATASET, REPORT_BOUNDS_DATASET, REPORT_MAX_BAR_HEIGHT, REPORT_MAX_PARAM, REPORT_MIN_PARAM, REPORT_TABLE_DATASET, ReportBandPaint, ReportBulletSpecOptions, buildReportBulletSpec } from "./ReportCard.chart.js";
|
|
2
|
+
import { BuildReportDescriptionInput, DEFAULT_REPORT_BANDS, DEFAULT_REPORT_LABELS, DeriveReportStateInput, NormalizeCriteriaOptions, REPORT_OTHER_KEY, ReportBand, ReportBandSpan, ReportBandTone, ReportCardLabels, ReportCardSize, ReportCardState, ReportCardStatus, ReportCriterion, ReportDelta, ReportGoodDirection, ReportHeadline, ReportOverall, ReportOverallExplanation, ReportOverallMode, ReportOverallSource, ReportPolarity, ReportRole, ReportRow, ReportScoreDomain, ReportScoreSource, ReportSort, ReportSummary, ReportTextFormatters, buildReportChartDescription, buildReportDescription, createReportFormatters, deriveReportDelta, deriveReportState, findBand, normalizeBands, normalizeCriteria, resolveReportLabels, resolveScore } from "./ReportCard.model.js";
|
|
3
|
+
import { ReportCard, ReportCardProps } from "./ReportCard.js";
|
|
4
|
+
export { type BuildReportDescriptionInput, DEFAULT_REPORT_BANDS, DEFAULT_REPORT_LABELS, type DeriveReportStateInput, type NormalizeCriteriaOptions, REPORT_BANDS_DATASET, REPORT_BOUNDS_DATASET, REPORT_MAX_BAR_HEIGHT, REPORT_MAX_PARAM, REPORT_MIN_PARAM, REPORT_OTHER_KEY, REPORT_TABLE_DATASET, type ReportBand, type ReportBandPaint, type ReportBandSpan, type ReportBandTone, type ReportBulletSpecOptions, ReportCard, type ReportCardLabels, type ReportCardProps, type ReportCardSize, type ReportCardState, type ReportCardStatus, type ReportCriterion, type ReportDelta, type ReportGoodDirection, type ReportHeadline, type ReportOverall, type ReportOverallExplanation, type ReportOverallMode, type ReportOverallSource, type ReportPolarity, type ReportRole, type ReportRow, type ReportScoreDomain, type ReportScoreSource, type ReportSort, type ReportSummary, type ReportTextFormatters, buildReportBulletSpec, buildReportChartDescription, buildReportDescription, createReportFormatters, deriveReportDelta, deriveReportState, findBand, normalizeBands, normalizeCriteria, resolveReportLabels, resolveScore };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { REPORT_BANDS_DATASET, REPORT_BOUNDS_DATASET, REPORT_MAX_BAR_HEIGHT, REPORT_MAX_PARAM, REPORT_MIN_PARAM, REPORT_TABLE_DATASET, buildReportBulletSpec } from "./ReportCard.chart.js";
|
|
2
|
+
import { DEFAULT_REPORT_BANDS, DEFAULT_REPORT_LABELS, REPORT_OTHER_KEY, buildReportChartDescription, buildReportDescription, createReportFormatters, deriveReportDelta, deriveReportState, findBand, normalizeBands, normalizeCriteria, resolveReportLabels, resolveScore } from "./ReportCard.model.js";
|
|
3
|
+
import { ReportCard } from "./ReportCard.js";
|
|
4
|
+
export { DEFAULT_REPORT_BANDS, DEFAULT_REPORT_LABELS, REPORT_BANDS_DATASET, REPORT_BOUNDS_DATASET, REPORT_MAX_BAR_HEIGHT, REPORT_MAX_PARAM, REPORT_MIN_PARAM, REPORT_OTHER_KEY, REPORT_TABLE_DATASET, ReportCard, buildReportBulletSpec, buildReportChartDescription, buildReportDescription, createReportFormatters, deriveReportDelta, deriveReportState, findBand, normalizeBands, normalizeCriteria, resolveReportLabels, resolveScore };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ChartTokens } from "../../lib/vega/tokens.js";
|
|
2
|
+
import { TopLevelSpec } from "vega-lite";
|
|
3
|
+
//#region src/components/SankeyCard/SankeyCard.chart.d.ts
|
|
4
|
+
/** Sampled ribbon edges: `RIBBON_SAMPLES` rows per flow (`SankeyRibbonRow`). */
|
|
5
|
+
declare const SANKEY_RIBBONS_DATASET = "ribbons";
|
|
6
|
+
/** One row per node — the rect, the name and the value line (`SankeyNodeRow`). */
|
|
7
|
+
declare const SANKEY_NODES_DATASET = "nodes";
|
|
8
|
+
/** The pointed flow. */
|
|
9
|
+
declare const SANKEY_HOVER_PARAM = "hover";
|
|
10
|
+
/** The pointed node. */
|
|
11
|
+
declare const SANKEY_NODE_HOVER_PARAM = "nodeHover";
|
|
12
|
+
/**
|
|
13
|
+
* The reader's chosen node, driven from React with `view.signal`. A VARIABLE
|
|
14
|
+
* param, not a selection: consumer-owned state pushed into a selection would
|
|
15
|
+
* live in that selection's `_store` dataset and fight the reader's own
|
|
16
|
+
* pointer, which is the bridge's rule.
|
|
17
|
+
*/
|
|
18
|
+
declare const SANKEY_SELECTED_PARAM = "selectedNode";
|
|
19
|
+
interface SankeyPlotSpecOptions {
|
|
20
|
+
/** Plot height in pixels. The width rides the View API. */
|
|
21
|
+
height: number;
|
|
22
|
+
/** The node rect's width in pixels — a mark property, never data. */
|
|
23
|
+
nodeWidth: number;
|
|
24
|
+
/**
|
|
25
|
+
* Gutters in pixels. The x domain is [0,1] and the end columns sit ON 0 and
|
|
26
|
+
* 1, so their rects and names are drawn into the padding: the left gutter
|
|
27
|
+
* holds half a rect (plus the first column's names when they label
|
|
28
|
+
* outward), the right holds half a rect plus the last column's names.
|
|
29
|
+
*/
|
|
30
|
+
padLeft: number;
|
|
31
|
+
padRight: number;
|
|
32
|
+
padTop: number;
|
|
33
|
+
padBottom: number;
|
|
34
|
+
/**
|
|
35
|
+
* Vega expression for the pixel budget a name may use before it ellipsizes
|
|
36
|
+
* rather than clipping at the frame edge. An expression rather than a
|
|
37
|
+
* number because the budget differs per column — an end column has its
|
|
38
|
+
* whole gutter, an interior one only the gap to the next column, which is a
|
|
39
|
+
* fraction of a width the spec must not be rebuilt to learn.
|
|
40
|
+
*/
|
|
41
|
+
labelLimit: string;
|
|
42
|
+
/** Draw the node names at all. `false` leaves every name to the table. */
|
|
43
|
+
labels: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Label the first and last columns OUTWARD, into their gutters. The reading
|
|
46
|
+
* that never puts a name over a ribbon; interior names have nowhere else to
|
|
47
|
+
* go, so they sit to the right of their node in the foreground text role.
|
|
48
|
+
*/
|
|
49
|
+
labelEndsOutward: boolean;
|
|
50
|
+
/** Print each node's total under its name, for nodes tall enough to hold one. */
|
|
51
|
+
showValues: boolean;
|
|
52
|
+
/** Wire the hover selections and the tooltip channel. */
|
|
53
|
+
tooltip: boolean;
|
|
54
|
+
/** Wire the selected-node param, so picking a node dims the flows that miss it. */
|
|
55
|
+
selectable: boolean;
|
|
56
|
+
/** `arcNumber` grammar for the tooltip's volume. */
|
|
57
|
+
valueFormat: string;
|
|
58
|
+
/** Tooltip titles. */
|
|
59
|
+
fromTitle: string;
|
|
60
|
+
toTitle: string;
|
|
61
|
+
valueTitle: string;
|
|
62
|
+
locale?: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Builds the flow diagram: the ribbons, then the node rects over their ends,
|
|
66
|
+
* then the names.
|
|
67
|
+
*
|
|
68
|
+
* Colour rides the data (`c`, with `scale: null`) rather than a scale, so
|
|
69
|
+
* whichever job the card gave it — one emphasis hue, the declared polarity,
|
|
70
|
+
* or an identity slot per outcome — the spec stays structural and re-colouring
|
|
71
|
+
* is a data push that cannot repaint a survivor it did not mean to.
|
|
72
|
+
*/
|
|
73
|
+
declare function buildSankeyPlotSpec(options: SankeyPlotSpecOptions, tokens: ChartTokens): TopLevelSpec;
|
|
74
|
+
//#endregion
|
|
75
|
+
export { SANKEY_HOVER_PARAM, SANKEY_NODES_DATASET, SANKEY_NODE_HOVER_PARAM, SANKEY_RIBBONS_DATASET, SANKEY_SELECTED_PARAM, SankeyPlotSpecOptions, buildSankeyPlotSpec };
|