@sportsdataverse/sdvtables 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/NOTICE.md +4 -0
- package/README.md +519 -2
- package/data/table_themes.json +12136 -0
- package/data/table_themes.json.sha256 +1 -0
- package/dist/aggrid.d.ts +104 -0
- package/dist/aggrid.js +230 -0
- package/dist/aggrid.js.map +1 -0
- package/dist/chunk-3TJR7TKS.js +223 -0
- package/dist/chunk-3TJR7TKS.js.map +1 -0
- package/dist/chunk-R375P3MN.js +2315 -0
- package/dist/chunk-R375P3MN.js.map +1 -0
- package/dist/chunk-UTOVFIFT.js +15176 -0
- package/dist/chunk-UTOVFIFT.js.map +1 -0
- package/dist/chunk-VLF2S6VW.js +181 -0
- package/dist/chunk-VLF2S6VW.js.map +1 -0
- package/dist/chunk-WBTFIGYK.js +189 -0
- package/dist/chunk-WBTFIGYK.js.map +1 -0
- package/dist/engine-BFIPJUlC.d.ts +190 -0
- package/dist/export.d.ts +88 -0
- package/dist/export.js +310 -0
- package/dist/export.js.map +1 -0
- package/dist/html.d.ts +333 -0
- package/dist/html.js +6 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +264 -0
- package/dist/index.js +141 -0
- package/dist/index.js.map +1 -0
- package/dist/mui.d.ts +134 -0
- package/dist/mui.js +193 -0
- package/dist/mui.js.map +1 -0
- package/dist/react.d.ts +72 -0
- package/dist/react.js +184 -0
- package/dist/react.js.map +1 -0
- package/dist/spec-A88vJX5A.d.ts +748 -0
- package/dist/tanstack-table.d.ts +113 -0
- package/dist/tanstack-table.js +98 -0
- package/dist/tanstack-table.js.map +1 -0
- package/dist/tokens-CHwBt9gs.d.ts +75 -0
- package/package.json +123 -3
|
@@ -0,0 +1,2315 @@
|
|
|
1
|
+
import { TableSpecError, formatNumber, naturalDigits, resolveTheme, TOKEN_KEYS, toNumber, checkChart, isBlank, secondaryOn, RANK_PALETTE, formatValue, fontStack, selectRows, snakeAlign, matches, ordinal, scaledNumber } from './chunk-UTOVFIFT.js';
|
|
2
|
+
import { loadLeague, hex6, warn, resolveSync, onColor, mix, solid, contrast, teamColorsSync, headshotUrl, logoUrlSync } from '@sportsdataverse/sdvplot';
|
|
3
|
+
|
|
4
|
+
var NAMES = /* @__PURE__ */ new Map();
|
|
5
|
+
async function loadTeamNames(leagues) {
|
|
6
|
+
await Promise.all(
|
|
7
|
+
leagues.map(async (l) => {
|
|
8
|
+
for (const t of (await loadLeague(l)).teams) if (t.name) NAMES.set(`${l}:${t.team_id}`, t.name);
|
|
9
|
+
})
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
function teamNameSync(league, teamId) {
|
|
13
|
+
return NAMES.get(`${league}:${teamId}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/html/escape.ts
|
|
17
|
+
var MAP = {
|
|
18
|
+
"&": "&",
|
|
19
|
+
"<": "<",
|
|
20
|
+
">": ">",
|
|
21
|
+
'"': """,
|
|
22
|
+
"'": "'"
|
|
23
|
+
};
|
|
24
|
+
var escapeHtml = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => MAP[c]);
|
|
25
|
+
var escapeAttr = escapeHtml;
|
|
26
|
+
var CSS_UNSAFE = /[;{}<>"\\\n\r]|url\(/i;
|
|
27
|
+
var isCssValue = (v) => !CSS_UNSAFE.test(String(v));
|
|
28
|
+
function cssValue(v, field) {
|
|
29
|
+
const s = String(v);
|
|
30
|
+
if (CSS_UNSAFE.test(s))
|
|
31
|
+
throw new TableSpecError(
|
|
32
|
+
`${field} ${JSON.stringify(s)} is not a CSS value: it may not contain ; { } < > " \\ url( or a line break`
|
|
33
|
+
);
|
|
34
|
+
return s;
|
|
35
|
+
}
|
|
36
|
+
function checkPx(v, field) {
|
|
37
|
+
const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : Number.NaN;
|
|
38
|
+
if (!Number.isFinite(n))
|
|
39
|
+
throw new TableSpecError(`${field} must be a finite number, got ${JSON.stringify(v)}`);
|
|
40
|
+
return n;
|
|
41
|
+
}
|
|
42
|
+
function styleAttr(decl) {
|
|
43
|
+
return Object.entries(decl).filter(([, v]) => v !== null && v !== void 0 && v !== "").map(([k, v]) => `${k}:${escapeAttr(cssValue(v, `CSS ${k}`))}`).join(";");
|
|
44
|
+
}
|
|
45
|
+
function styleOf(decls) {
|
|
46
|
+
const s = decls.filter((x) => x !== "").join(";");
|
|
47
|
+
return s ? ` style="${s}"` : "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/html/icons.ts
|
|
51
|
+
var PATHS = {
|
|
52
|
+
circle: "M8 1a7 7 0 1 0 0 14A7 7 0 1 0 8 1z",
|
|
53
|
+
square: "M2 2h12v12H2z",
|
|
54
|
+
diamond: "M8 0.5 15.5 8 8 15.5 0.5 8z",
|
|
55
|
+
star: "M8 0.8l2.2 4.6 5 .6-3.7 3.5.9 5L8 12l-4.4 2.5.9-5L.8 6l5-.6z",
|
|
56
|
+
"triangle-up": "M8 1.5 15 14.5H1z",
|
|
57
|
+
"triangle-down": "M1 1.5h14L8 14.5z",
|
|
58
|
+
"arrow-up": "M8 1 14 8H10v7H6V8H2z",
|
|
59
|
+
"arrow-down": "M6 1h4v7h4l-6 7-6-7h4z",
|
|
60
|
+
"arrow-right": "M1 6h7V2l7 6-7 6v-4H1z",
|
|
61
|
+
check: "M1.5 8.5 3.6 6.4 6.2 9 12.4 2.8 14.5 4.9 6.2 13.2z",
|
|
62
|
+
xmark: "M3.1 1 8 5.9 12.9 1 15 3.1 10.1 8 15 12.9 12.9 15 8 10.1 3.1 15 1 12.9 5.9 8 1 3.1z",
|
|
63
|
+
minus: "M1 6.5h14v3H1z",
|
|
64
|
+
plus: "M6.5 1h3v5.5H15v3H9.5V15h-3V9.5H1v-3h5.5z",
|
|
65
|
+
heart: "M8 14.5 1.6 8.2A3.9 3.9 0 0 1 8 3.4a3.9 3.9 0 0 1 6.4 4.8z"
|
|
66
|
+
};
|
|
67
|
+
var ICON_NAMES = Object.keys(PATHS);
|
|
68
|
+
function iconSvg(name, color, size) {
|
|
69
|
+
const d = Object.hasOwn(PATHS, name) ? PATHS[name] : void 0;
|
|
70
|
+
if (d === void 0)
|
|
71
|
+
throw new TableSpecError(`unknown icon ${JSON.stringify(name)}; icons are ${ICON_NAMES.join(", ")}`);
|
|
72
|
+
return `<svg class="sdvt-icon" viewBox="0 0 16 16" width="${size}" height="${size}" fill="${escapeAttr(color)}" aria-hidden="true" style="vertical-align:middle"><path d="${d}"/></svg>`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/html/fonts.ts
|
|
76
|
+
function fontsLink(fonts) {
|
|
77
|
+
const byFamily = /* @__PURE__ */ new Map();
|
|
78
|
+
for (const f of fonts) {
|
|
79
|
+
const set = byFamily.get(f.family) ?? /* @__PURE__ */ new Set();
|
|
80
|
+
for (const w of f.weights) set.add(w);
|
|
81
|
+
byFamily.set(f.family, set);
|
|
82
|
+
}
|
|
83
|
+
if (byFamily.size === 0) return "";
|
|
84
|
+
const q = [...byFamily.entries()].sort(([a], [b]) => a.localeCompare(b)).map(
|
|
85
|
+
([fam, ws]) => `family=${encodeURIComponent(fam).replace(/%20/g, "+")}:wght@${[...ws].sort((a, b) => a - b).join(";")}`
|
|
86
|
+
).join("&");
|
|
87
|
+
return `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?${q}&display=swap">`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/table-id.ts
|
|
91
|
+
function fnv1a32(s) {
|
|
92
|
+
let h = 2166136261;
|
|
93
|
+
for (let i = 0; i < s.length; i++) {
|
|
94
|
+
h ^= s.charCodeAt(i);
|
|
95
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
96
|
+
}
|
|
97
|
+
return h.toString(16).padStart(8, "0");
|
|
98
|
+
}
|
|
99
|
+
function tableId(spec) {
|
|
100
|
+
return spec.id ?? `sdvt-${fnv1a32(JSON.stringify(spec))}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/themes/base-css.ts
|
|
104
|
+
var kebab = (s) => s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
105
|
+
function tokensCSS(sel, t) {
|
|
106
|
+
return `${sel}{${TOKEN_KEYS.map((k) => `--sdvt-${kebab(k)}:${t[k]};`).join("")}}`;
|
|
107
|
+
}
|
|
108
|
+
function BASE_CSS(sel) {
|
|
109
|
+
const s = sel;
|
|
110
|
+
return [
|
|
111
|
+
`${s}{display:inline-block;max-width:100%;background:var(--sdvt-bg);color:var(--sdvt-text);font-family:var(--sdvt-font-body);font-size:var(--sdvt-body-size);font-weight:var(--sdvt-body-weight);line-height:var(--sdvt-line-height)}`,
|
|
112
|
+
// font:inherit: a host's table{font} (Observable Framework's 13px sans) outranks inheriting the root's type
|
|
113
|
+
`${s} table{font:inherit;border-collapse:collapse;border-spacing:0;width:auto;border-top:var(--sdvt-table-border-top);border-bottom:var(--sdvt-table-border-bottom);border-left:var(--sdvt-table-border-x);border-right:var(--sdvt-table-border-x)}`,
|
|
114
|
+
`${s} caption{caption-side:top;text-align:var(--sdvt-heading-align);background:var(--sdvt-heading-bg);padding:var(--sdvt-heading-pad) 5px}`,
|
|
115
|
+
`${s} .sdvt-title{font-family:var(--sdvt-font-title);font-weight:var(--sdvt-title-weight);font-size:var(--sdvt-title-size);color:var(--sdvt-title-color);text-transform:var(--sdvt-title-transform);letter-spacing:var(--sdvt-title-tracking)}`,
|
|
116
|
+
`${s} .sdvt-subtitle{display:block;font-size:var(--sdvt-subtitle-size);font-style:var(--sdvt-subtitle-style);font-weight:var(--sdvt-subtitle-weight);color:var(--sdvt-subtitle-color)}`,
|
|
117
|
+
// great_tables draws the subtitle on its own heading row in every theme
|
|
118
|
+
`${s} thead{position:relative}${s} thead::after{content:"";position:absolute;left:0;right:0;bottom:0;height:4px;background:var(--sdvt-horizon)}`,
|
|
119
|
+
`${s} th.sdvt-label{font-family:var(--sdvt-font-label);font-weight:var(--sdvt-label-weight);font-size:var(--sdvt-label-size);font-style:var(--sdvt-label-style);color:var(--sdvt-label-color);background:var(--sdvt-label-bg);text-transform:var(--sdvt-label-transform);letter-spacing:var(--sdvt-label-tracking);border-top:var(--sdvt-label-border-top);border-bottom:var(--sdvt-label-border-bottom);padding:var(--sdvt-label-pad) 5px;vertical-align:bottom}`,
|
|
120
|
+
`${s} .sdvt-subheader{display:block;font-weight:400;font-size:0.8em;color:var(--sdvt-muted);text-transform:none;letter-spacing:normal}`,
|
|
121
|
+
`${s} th.sdvt-group{font-family:var(--sdvt-font-label);font-weight:var(--sdvt-group-weight);font-size:var(--sdvt-group-size);color:var(--sdvt-group-color);background:var(--sdvt-group-bg);text-transform:var(--sdvt-group-transform);border-top:var(--sdvt-group-border-top);border-bottom:var(--sdvt-group-border-bottom);text-align:left;padding:var(--sdvt-group-pad) 5px}`,
|
|
122
|
+
// A host's td{color} (Framework; Infima's --ifm-table-cell-color) outranks the ink a cell only inherits, so the
|
|
123
|
+
// cell asks for it explicitly. inherit, not var(--sdvt-text): the row's ink is the theme's, or spotlight's dim on
|
|
124
|
+
// the <tr>. ponytail: (0,1,0), the scope in :where(): beats any element-only host rule (td, table td), loses to an
|
|
125
|
+
// author's class-qualified rule (.page td at (0,1,1)) and to every theme rule and inline cell style.
|
|
126
|
+
`:where(${s}) .sdvt-cell{color:inherit}`,
|
|
127
|
+
// Hosts paint rows: Infima stripes every second one (table tr:nth-child(2n), (0,1,2)) under cells that leave the
|
|
128
|
+
// ground to the root, and Framework and Infima rule them. sdvtables' own sheets never colour or border a <tr>
|
|
129
|
+
// (themes and decorations style cells; cutlines and watermarks are images; row decorations are inline), so rows
|
|
130
|
+
// stay clear. ponytail: :nth-child(n) lifts this to (0,2,1), past that stripe whatever the stylesheet order.
|
|
131
|
+
`${s} tr:nth-child(n){background-color:transparent;border:0}`,
|
|
132
|
+
// Infima borders every cell (table th, table td { border }, (0,0,2)). ponytail: (0,1,1) clears that, and every
|
|
133
|
+
// border sdvtables draws comes from a more specific rule or a later one (themes and decorations follow BASE_CSS)
|
|
134
|
+
`${s} td,${s} th{border:0}`,
|
|
135
|
+
`${s} td.sdvt-cell{padding:var(--sdvt-pad) 5px;border-top:var(--sdvt-hline);font-variant-numeric:tabular-nums;vertical-align:middle}`,
|
|
136
|
+
`${s} tbody tr:first-child td.sdvt-cell{border-top:none}${s} tbody{border-bottom:var(--sdvt-body-border-bottom)}`,
|
|
137
|
+
// background-color, not the `background` shorthand: the shorthand would reset the hovered row's underline image
|
|
138
|
+
`${s} tr.sdvt-stripe td.sdvt-cell{background-color:var(--sdvt-stripe)}`,
|
|
139
|
+
`${s} .sdvt-left{text-align:left}${s} .sdvt-center{text-align:center}${s} .sdvt-right{text-align:right}`,
|
|
140
|
+
`${s} tfoot td{font-size:var(--sdvt-source-size);font-style:var(--sdvt-source-style);color:var(--sdvt-source-color);padding:var(--sdvt-source-pad) 5px;text-align:left}`,
|
|
141
|
+
`${s} img.sdvt-mark{vertical-align:middle}`,
|
|
142
|
+
`${s} img{max-width:none}`,
|
|
143
|
+
// A47: a host img{max-width:100%} reset (Docusaurus, Tailwind preflight) squashed cell logos
|
|
144
|
+
`${s} table{--bs-table-bg:transparent;--bs-table-color:currentcolor}`,
|
|
145
|
+
// utils-theme.R .theme_bs_host: Bootstrap hosts repaint td
|
|
146
|
+
// Phase 5 controls: renderHTML(table), hydrate and <SdvTable/> (the last rule is J31 A5, a neutral selected-row overlay)
|
|
147
|
+
`${s} .sdvt-sort{all:unset;cursor:pointer;display:inline-block;width:100%}`,
|
|
148
|
+
`${s} .sdvt-sort:focus-visible{outline:2px solid currentColor;outline-offset:2px}`,
|
|
149
|
+
// all:unset drops the UA focus ring (WCAG 2.4.7)
|
|
150
|
+
// the arrow repeats aria-sort, so its alt text is empty (kept out of the button name); the plain declaration is the fallback
|
|
151
|
+
`${s} th[aria-sort="ascending"] .sdvt-sort::after{content:" \u25B2";content:" \u25B2"/"";font-size:.7em}`,
|
|
152
|
+
`${s} th[aria-sort="descending"] .sdvt-sort::after{content:" \u25BC";content:" \u25BC"/"";font-size:.7em}`,
|
|
153
|
+
`${s} .sdvt-toolbar{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px}`,
|
|
154
|
+
// I4 (WCAG 1.4.11): the theme's rule taken 60% of the way to the text colour, 3:1 or better on the table background
|
|
155
|
+
// in every theme (a black or ink rule stays itself); where color-mix is unsupported, the text colour
|
|
156
|
+
`${s} .sdvt-toolbar input{font:inherit;padding:2px 6px;border:1px solid var(--sdvt-text);border-color:color-mix(in srgb,var(--sdvt-text) 60%,var(--sdvt-rule));background:var(--sdvt-bg);color:var(--sdvt-text)}`,
|
|
157
|
+
`${s} .sdvt-pager{display:flex;gap:8px;align-items:center;justify-content:flex-end;margin-top:6px}`,
|
|
158
|
+
`${s} .sdvt-page{font:inherit;cursor:pointer}`,
|
|
159
|
+
`${s} .sdvt-page[aria-disabled="true"]{opacity:.4;cursor:default}`,
|
|
160
|
+
// M1: aria-disabled, so focus stays on an edge button
|
|
161
|
+
`${s} tr.sdvt-selected>td{box-shadow:inset 0 0 0 9999px rgba(127,127,127,.18)}`,
|
|
162
|
+
`${s} tr.sdvt-selected>td:first-child{box-shadow:inset 3px 0 0 var(--sdvt-text),inset 0 0 0 9999px rgba(127,127,127,.18)}`,
|
|
163
|
+
// I4: the tint is 1.2:1; a text-coloured bar gives the state 3:1
|
|
164
|
+
// J31 (A29): a hovered row (its own pointer, or a linked figure's) gets a text-coloured underline, 3:1 like I4's bar.
|
|
165
|
+
// A background image: it sits beside the selected overlay (box-shadow) and moves no layout. The scope and the row
|
|
166
|
+
// sit in :where(), so the rule weighs (0,1,1), td.sdvt-cell alone: the documented one-rule opt-out
|
|
167
|
+
// tr.sdvt-hover>td.sdvt-cell{background-image:none}, at (0,2,2), wins wherever a page puts it
|
|
168
|
+
`:where(${s} tr.sdvt-hover)>td.sdvt-cell{background-image:linear-gradient(var(--sdvt-text),var(--sdvt-text));background-size:100% 2px;background-position:0 100%;background-repeat:no-repeat}`,
|
|
169
|
+
`${s} tr.sdvt-row:focus-visible{outline:2px solid currentColor;outline-offset:-2px}`,
|
|
170
|
+
// Task 10: the grid's tab stop (WCAG 2.4.7)
|
|
171
|
+
`${s} th.sdvt-col-current .sdvt-sort{text-decoration:underline;text-underline-offset:4px}`
|
|
172
|
+
// Task 10: the column `s` sorts
|
|
173
|
+
].join("\n");
|
|
174
|
+
}
|
|
175
|
+
function ramp(palette, [lo, hi], bias = 1) {
|
|
176
|
+
const k = palette.length - 1;
|
|
177
|
+
if (!(bias > 0)) throw new TableSpecError(`bias must be a positive number, got ${String(bias)}`);
|
|
178
|
+
const stops = palette.map((_, i) => k === 0 ? 0 : (i / k) ** bias);
|
|
179
|
+
return (v) => {
|
|
180
|
+
if (!(lo <= v && v <= hi)) return null;
|
|
181
|
+
if (k === 0) return palette[0] ?? null;
|
|
182
|
+
const t = hi === lo ? 0.5 : (v - lo) / (hi - lo);
|
|
183
|
+
if (bias === 1) {
|
|
184
|
+
const i2 = Math.min(Math.floor(t * k), k - 1);
|
|
185
|
+
return mix(palette[i2], palette[i2 + 1], t * k - i2);
|
|
186
|
+
}
|
|
187
|
+
let i = 0;
|
|
188
|
+
while (i < k - 1 && t > stops[i + 1]) i++;
|
|
189
|
+
const a = stops[i];
|
|
190
|
+
const b = stops[i + 1];
|
|
191
|
+
return mix(palette[i], palette[i + 1], Math.min(1, Math.max(0, (t - a) / (b - a))));
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function ntile(values, n) {
|
|
195
|
+
if (!(Number.isInteger(n) && n > 0))
|
|
196
|
+
throw new TableSpecError(`buckets must be a positive integer, got ${String(n)}`);
|
|
197
|
+
const order = values.map((v, i) => [v, i]).filter((p) => p[0] !== null).sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
198
|
+
const len = order.length;
|
|
199
|
+
const out = values.map(() => null);
|
|
200
|
+
const nLarger = len % n;
|
|
201
|
+
const larger = Math.ceil(len / n);
|
|
202
|
+
const smaller = Math.floor(len / n);
|
|
203
|
+
const threshold = larger * nLarger;
|
|
204
|
+
order.forEach(([, i], j) => {
|
|
205
|
+
const x = j + 1;
|
|
206
|
+
out[i] = Math.floor(
|
|
207
|
+
x <= threshold ? (x + larger - 1) / larger : (x - threshold + smaller - 1) / smaller + nLarger
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
function averageRanks(values, desc) {
|
|
213
|
+
const idx = values.map((v, i) => [v, i]).filter((p) => p[0] !== null).sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
214
|
+
const out = values.map(() => null);
|
|
215
|
+
for (let s = 0; s < idx.length; ) {
|
|
216
|
+
let e = s;
|
|
217
|
+
while (e + 1 < idx.length && idx[e + 1]?.[0] === idx[s]?.[0]) e++;
|
|
218
|
+
const r = (s + e) / 2 + 1;
|
|
219
|
+
for (let j = s; j <= e; j++) out[idx[j]?.[1]] = r;
|
|
220
|
+
s = e + 1;
|
|
221
|
+
}
|
|
222
|
+
if (!desc || idx.length === 0) return out;
|
|
223
|
+
let top = 0;
|
|
224
|
+
for (const r of out) if (r !== null && r > top) top = r;
|
|
225
|
+
return out.map((r) => r === null ? null : top - r + 1);
|
|
226
|
+
}
|
|
227
|
+
function domainOf(columns) {
|
|
228
|
+
let lo = Number.POSITIVE_INFINITY;
|
|
229
|
+
let hi = Number.NEGATIVE_INFINITY;
|
|
230
|
+
for (const col of columns)
|
|
231
|
+
for (const v of col)
|
|
232
|
+
if (v !== null) {
|
|
233
|
+
if (v < lo) lo = v;
|
|
234
|
+
if (v > hi) hi = v;
|
|
235
|
+
}
|
|
236
|
+
if (lo > hi) throw new TableSpecError("no domain given and no numeric value to derive one from");
|
|
237
|
+
return [lo, hi];
|
|
238
|
+
}
|
|
239
|
+
function quantile7(xs, p) {
|
|
240
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
241
|
+
const h = (s.length - 1) * p;
|
|
242
|
+
const lo = Math.floor(h);
|
|
243
|
+
return s[lo] + (h - lo) * (s[Math.min(lo + 1, s.length - 1)] - s[lo]);
|
|
244
|
+
}
|
|
245
|
+
function sampleSd(xs) {
|
|
246
|
+
const m = xs.reduce((a, b) => a + b, 0) / xs.length;
|
|
247
|
+
return Math.sqrt(xs.reduce((a, b) => a + (b - m) ** 2, 0) / (xs.length - 1));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/html/named-colors.ts
|
|
251
|
+
var NAMED = new Map(
|
|
252
|
+
Object.entries({
|
|
253
|
+
aliceblue: "f0f8ff",
|
|
254
|
+
antiquewhite: "faebd7",
|
|
255
|
+
aqua: "00ffff",
|
|
256
|
+
aquamarine: "7fffd4",
|
|
257
|
+
azure: "f0ffff",
|
|
258
|
+
beige: "f5f5dc",
|
|
259
|
+
bisque: "ffe4c4",
|
|
260
|
+
black: "000000",
|
|
261
|
+
blanchedalmond: "ffebcd",
|
|
262
|
+
blue: "0000ff",
|
|
263
|
+
blueviolet: "8a2be2",
|
|
264
|
+
brown: "a52a2a",
|
|
265
|
+
burlywood: "deb887",
|
|
266
|
+
cadetblue: "5f9ea0",
|
|
267
|
+
chartreuse: "7fff00",
|
|
268
|
+
chocolate: "d2691e",
|
|
269
|
+
coral: "ff7f50",
|
|
270
|
+
cornflowerblue: "6495ed",
|
|
271
|
+
cornsilk: "fff8dc",
|
|
272
|
+
crimson: "dc143c",
|
|
273
|
+
cyan: "00ffff",
|
|
274
|
+
darkblue: "00008b",
|
|
275
|
+
darkcyan: "008b8b",
|
|
276
|
+
darkgoldenrod: "b8860b",
|
|
277
|
+
darkgray: "a9a9a9",
|
|
278
|
+
darkgreen: "006400",
|
|
279
|
+
darkgrey: "a9a9a9",
|
|
280
|
+
darkkhaki: "bdb76b",
|
|
281
|
+
darkmagenta: "8b008b",
|
|
282
|
+
darkolivegreen: "556b2f",
|
|
283
|
+
darkorange: "ff8c00",
|
|
284
|
+
darkorchid: "9932cc",
|
|
285
|
+
darkred: "8b0000",
|
|
286
|
+
darksalmon: "e9967a",
|
|
287
|
+
darkseagreen: "8fbc8f",
|
|
288
|
+
darkslateblue: "483d8b",
|
|
289
|
+
darkslategray: "2f4f4f",
|
|
290
|
+
darkslategrey: "2f4f4f",
|
|
291
|
+
darkturquoise: "00ced1",
|
|
292
|
+
darkviolet: "9400d3",
|
|
293
|
+
deeppink: "ff1493",
|
|
294
|
+
deepskyblue: "00bfff",
|
|
295
|
+
dimgray: "696969",
|
|
296
|
+
dimgrey: "696969",
|
|
297
|
+
dodgerblue: "1e90ff",
|
|
298
|
+
firebrick: "b22222",
|
|
299
|
+
floralwhite: "fffaf0",
|
|
300
|
+
forestgreen: "228b22",
|
|
301
|
+
fuchsia: "ff00ff",
|
|
302
|
+
gainsboro: "dcdcdc",
|
|
303
|
+
ghostwhite: "f8f8ff",
|
|
304
|
+
gold: "ffd700",
|
|
305
|
+
goldenrod: "daa520",
|
|
306
|
+
gray: "808080",
|
|
307
|
+
green: "008000",
|
|
308
|
+
greenyellow: "adff2f",
|
|
309
|
+
grey: "808080",
|
|
310
|
+
honeydew: "f0fff0",
|
|
311
|
+
hotpink: "ff69b4",
|
|
312
|
+
indianred: "cd5c5c",
|
|
313
|
+
indigo: "4b0082",
|
|
314
|
+
ivory: "fffff0",
|
|
315
|
+
khaki: "f0e68c",
|
|
316
|
+
lavender: "e6e6fa",
|
|
317
|
+
lavenderblush: "fff0f5",
|
|
318
|
+
lawngreen: "7cfc00",
|
|
319
|
+
lemonchiffon: "fffacd",
|
|
320
|
+
lightblue: "add8e6",
|
|
321
|
+
lightcoral: "f08080",
|
|
322
|
+
lightcyan: "e0ffff",
|
|
323
|
+
lightgoldenrodyellow: "fafad2",
|
|
324
|
+
lightgray: "d3d3d3",
|
|
325
|
+
lightgreen: "90ee90",
|
|
326
|
+
lightgrey: "d3d3d3",
|
|
327
|
+
lightpink: "ffb6c1",
|
|
328
|
+
lightsalmon: "ffa07a",
|
|
329
|
+
lightseagreen: "20b2aa",
|
|
330
|
+
lightskyblue: "87cefa",
|
|
331
|
+
lightslategray: "778899",
|
|
332
|
+
lightslategrey: "778899",
|
|
333
|
+
lightsteelblue: "b0c4de",
|
|
334
|
+
lightyellow: "ffffe0",
|
|
335
|
+
lime: "00ff00",
|
|
336
|
+
limegreen: "32cd32",
|
|
337
|
+
linen: "faf0e6",
|
|
338
|
+
magenta: "ff00ff",
|
|
339
|
+
maroon: "800000",
|
|
340
|
+
mediumaquamarine: "66cdaa",
|
|
341
|
+
mediumblue: "0000cd",
|
|
342
|
+
mediumorchid: "ba55d3",
|
|
343
|
+
mediumpurple: "9370db",
|
|
344
|
+
mediumseagreen: "3cb371",
|
|
345
|
+
mediumslateblue: "7b68ee",
|
|
346
|
+
mediumspringgreen: "00fa9a",
|
|
347
|
+
mediumturquoise: "48d1cc",
|
|
348
|
+
mediumvioletred: "c71585",
|
|
349
|
+
midnightblue: "191970",
|
|
350
|
+
mintcream: "f5fffa",
|
|
351
|
+
mistyrose: "ffe4e1",
|
|
352
|
+
moccasin: "ffe4b5",
|
|
353
|
+
navajowhite: "ffdead",
|
|
354
|
+
navy: "000080",
|
|
355
|
+
oldlace: "fdf5e6",
|
|
356
|
+
olive: "808000",
|
|
357
|
+
olivedrab: "6b8e23",
|
|
358
|
+
orange: "ffa500",
|
|
359
|
+
orangered: "ff4500",
|
|
360
|
+
orchid: "da70d6",
|
|
361
|
+
palegoldenrod: "eee8aa",
|
|
362
|
+
palegreen: "98fb98",
|
|
363
|
+
paleturquoise: "afeeee",
|
|
364
|
+
palevioletred: "db7093",
|
|
365
|
+
papayawhip: "ffefd5",
|
|
366
|
+
peachpuff: "ffdab9",
|
|
367
|
+
peru: "cd853f",
|
|
368
|
+
pink: "ffc0cb",
|
|
369
|
+
plum: "dda0dd",
|
|
370
|
+
powderblue: "b0e0e6",
|
|
371
|
+
purple: "800080",
|
|
372
|
+
rebeccapurple: "663399",
|
|
373
|
+
red: "ff0000",
|
|
374
|
+
rosybrown: "bc8f8f",
|
|
375
|
+
royalblue: "4169e1",
|
|
376
|
+
saddlebrown: "8b4513",
|
|
377
|
+
salmon: "fa8072",
|
|
378
|
+
sandybrown: "f4a460",
|
|
379
|
+
seagreen: "2e8b57",
|
|
380
|
+
seashell: "fff5ee",
|
|
381
|
+
sienna: "a0522d",
|
|
382
|
+
silver: "c0c0c0",
|
|
383
|
+
skyblue: "87ceeb",
|
|
384
|
+
slateblue: "6a5acd",
|
|
385
|
+
slategray: "708090",
|
|
386
|
+
slategrey: "708090",
|
|
387
|
+
snow: "fffafa",
|
|
388
|
+
springgreen: "00ff7f",
|
|
389
|
+
steelblue: "4682b4",
|
|
390
|
+
tan: "d2b48c",
|
|
391
|
+
teal: "008080",
|
|
392
|
+
thistle: "d8bfd8",
|
|
393
|
+
tomato: "ff6347",
|
|
394
|
+
turquoise: "40e0d0",
|
|
395
|
+
violet: "ee82ee",
|
|
396
|
+
wheat: "f5deb3",
|
|
397
|
+
white: "ffffff",
|
|
398
|
+
whitesmoke: "f5f5f5",
|
|
399
|
+
yellow: "ffff00",
|
|
400
|
+
yellowgreen: "9acd32"
|
|
401
|
+
}).map(([k, v]) => [k, `#${v}`])
|
|
402
|
+
);
|
|
403
|
+
function namedColor(s) {
|
|
404
|
+
return NAMED.get(s.trim().toLowerCase());
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/html/svg.ts
|
|
408
|
+
var halfEven = (x) => {
|
|
409
|
+
const r = Math.round(x);
|
|
410
|
+
return String(Math.abs(x % 1) === 0.5 && r % 2 !== 0 ? r - 1 : r);
|
|
411
|
+
};
|
|
412
|
+
function cutlineSvg(text, color, size) {
|
|
413
|
+
const t = text.toUpperCase();
|
|
414
|
+
const tracking = 1.1;
|
|
415
|
+
const esc = t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
416
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${halfEven(t.length * (size * 0.8 + tracking) + 4)}" height="${halfEven(size + 4)}"><text x="0" y="${(size + 0.5).toFixed(1)}" font-family="Helvetica,Arial,sans-serif" font-size="${size}" font-weight="700" letter-spacing="${tracking}" fill="${color}">${esc}</text></svg>`;
|
|
417
|
+
return `data:image/svg+xml;charset=utf-8,${pyQuote(svg)}`;
|
|
418
|
+
}
|
|
419
|
+
var pyQuote = (x) => encodeURIComponent(x).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
420
|
+
function watermarkSvg(text, color, opacity, angle, font) {
|
|
421
|
+
const label = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
422
|
+
const size = 100;
|
|
423
|
+
const textW = Math.max(label.length * size * 0.62, size);
|
|
424
|
+
const textH = size * 1.3;
|
|
425
|
+
const rad = Math.abs(angle) * Math.PI / 180;
|
|
426
|
+
const w = Math.ceil(textW * Math.cos(rad) + textH * Math.sin(rad)) + 4;
|
|
427
|
+
const h = Math.ceil(textW * Math.sin(rad) + textH * Math.cos(rad)) + 4;
|
|
428
|
+
const rot = angle !== 0 ? ` transform="rotate(${angle} ${w / 2} ${h / 2})"` : "";
|
|
429
|
+
const attr = (v) => v.replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """);
|
|
430
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}"><text x="50%" y="50%" text-anchor="middle" dominant-baseline="central" font-family="${attr(font)}" font-size="${size}" font-weight="700" fill="${attr(color)}" fill-opacity="${opacity}"${rot}>${label}</text></svg>`;
|
|
431
|
+
return [`data:image/svg+xml,${pyQuote(svg)}`, h > w];
|
|
432
|
+
}
|
|
433
|
+
var cssStr = (v) => v.replace(/[\\"\n\r<]/g, (c) => `\\${c.charCodeAt(0).toString(16)} `);
|
|
434
|
+
var NAMED2 = ["first", "last", "min", "max"];
|
|
435
|
+
var n2 = (v) => String(Math.round(v * 100) / 100 + 0);
|
|
436
|
+
function plainNumber(v) {
|
|
437
|
+
if (Number.isInteger(v)) return String(v);
|
|
438
|
+
const places = Math.max(4, 3 - Math.floor(Math.log10(Math.abs(v))));
|
|
439
|
+
return v.toFixed(places).replace(/0+$/, "").replace(/\.$/, "");
|
|
440
|
+
}
|
|
441
|
+
var fixed = (v, d) => {
|
|
442
|
+
const s = v.toFixed(d);
|
|
443
|
+
return /^-0(.0*)?$/.test(s) ? s.slice(1) : s;
|
|
444
|
+
};
|
|
445
|
+
var att = (v, field) => escapeAttr(cssValue(v, field));
|
|
446
|
+
function quantile(xs, p) {
|
|
447
|
+
const s = [...xs].sort((a2, b) => a2 - b);
|
|
448
|
+
const h = (s.length - 1) * p;
|
|
449
|
+
const lo = Math.floor(h);
|
|
450
|
+
const a = s[lo];
|
|
451
|
+
return a + (h - lo) * (s[Math.min(lo + 1, s.length - 1)] - a);
|
|
452
|
+
}
|
|
453
|
+
var STAT = {
|
|
454
|
+
mean: (xs) => xs.reduce((s, x) => s + x, 0) / xs.length,
|
|
455
|
+
median: (xs) => quantile(xs, 0.5),
|
|
456
|
+
min: (xs) => Math.min(...xs),
|
|
457
|
+
max: (xs) => Math.max(...xs)
|
|
458
|
+
};
|
|
459
|
+
function margins(labels, statline, bars) {
|
|
460
|
+
const any = labels.length > 0;
|
|
461
|
+
const endsOnly = any && !labels.some((n) => n === "min" || n === "max" || n === "all");
|
|
462
|
+
if (bars) {
|
|
463
|
+
if (statline) return any ? [12, 26, 3, 26, 22] : [4, 28, 3, 13, 22];
|
|
464
|
+
return any ? [12, 26, 0, 26, 22] : [3, 13, 0, 13, 22];
|
|
465
|
+
}
|
|
466
|
+
if (statline) return !any ? [4, 28, 3, 13, 22] : endsOnly ? [5, 28, 3, 24, 22] : [14, 28, 10, 13, 28];
|
|
467
|
+
return !any ? [3, 13, 2, 13, 22] : endsOnly ? [5, 24, 3, 24, 22] : [14, 13, 10, 13, 30];
|
|
468
|
+
}
|
|
469
|
+
function frameOf(col, present2, bars, row) {
|
|
470
|
+
const [t = 0, r = 0, b = 0, l = 0, grown = 22] = margins(col.labels, col.statline !== void 0, bars);
|
|
471
|
+
const h = col.chartHeight ?? grown;
|
|
472
|
+
if (col.chartWidth <= l + r || h <= t + b)
|
|
473
|
+
throw new TableSpecError(
|
|
474
|
+
`column "${col.key}": ${col.kind} chartWidth x chartHeight ${plainNumber(col.chartWidth)} x ${plainNumber(h)} leaves no room inside its margins (${l + r} x ${t + b} px with these labels/statline); make the chart larger`
|
|
475
|
+
);
|
|
476
|
+
const lo = col.minValue ?? Math.min(...present2, ...bars ? [0] : []);
|
|
477
|
+
const hi = col.maxValue ?? Math.max(...present2, ...bars ? [0] : []);
|
|
478
|
+
if (lo > hi)
|
|
479
|
+
throw new TableSpecError(
|
|
480
|
+
`column "${col.key}" row ${row}: minValue/maxValue cross the row's data (bottom ${plainNumber(lo)} is above top ${plainNumber(hi)}); give both bounds, or one the data stays on the right side of`
|
|
481
|
+
);
|
|
482
|
+
const bottom = h - b;
|
|
483
|
+
const y = (v) => hi === lo ? (t + bottom) / 2 : t + Math.min(Math.max((hi - v) / (hi - lo), 0), 1) * (bottom - t);
|
|
484
|
+
return { h, top: t, right: col.chartWidth - r, bottom, left: l, lo, hi, y };
|
|
485
|
+
}
|
|
486
|
+
function statlineSvg(col, f, present2) {
|
|
487
|
+
if (col.statline === void 0) return "";
|
|
488
|
+
const v = STAT[col.statline](present2);
|
|
489
|
+
const y = n2(f.y(v));
|
|
490
|
+
const c = att(col.statlineColor, `${col.kind} statlineColor`);
|
|
491
|
+
const size = att(col.statlineLabelSize, `${col.kind} statlineLabelSize`);
|
|
492
|
+
return `<line class="sdvt-statline" x1="${n2(f.left)}" x2="${n2(f.right)}" y1="${y}" y2="${y}" stroke="${c}" stroke-width="1" stroke-dasharray="2,2" stroke-opacity="0.75"/><text class="sdvt-statline-label" x="${n2(f.right + 5)}" y="${y}" dominant-baseline="middle" fill="${c}" font-weight="bold" font-size="${size}">${fixed(v, col.decimals)}</text>`;
|
|
493
|
+
}
|
|
494
|
+
function bandlineSvg(col, f, present2) {
|
|
495
|
+
if (col.bandline === void 0) return "";
|
|
496
|
+
const range = col.bandline === "range" || present2.length < 2;
|
|
497
|
+
const lo = range ? Math.min(...present2) : quantile(present2, 0.25);
|
|
498
|
+
const hi = range ? Math.max(...present2) : quantile(present2, 0.75);
|
|
499
|
+
const y1 = f.y(hi);
|
|
500
|
+
return `<rect class="sdvt-bandline" x="${n2(f.left)}" y="${n2(y1)}" width="${n2(f.right - f.left)}" height="${n2(f.y(lo) - y1)}" fill="${att(col.bandlineColor, `${col.kind} bandlineColor`)}" fill-opacity="${col.bandlineOpacity}"/>`;
|
|
501
|
+
}
|
|
502
|
+
function summary(name, col, present2) {
|
|
503
|
+
const n = present2.length;
|
|
504
|
+
let out = `${name} of ${n} value${n === 1 ? "" : "s"}: min ${plainNumber(Math.min(...present2))}, max ${plainNumber(Math.max(...present2))}, last ${plainNumber(present2[n - 1])}`;
|
|
505
|
+
if (col.statline) out += `; ${col.statline} line at ${fixed(STAT[col.statline](present2), col.decimals)}`;
|
|
506
|
+
if (col.bandline)
|
|
507
|
+
out += `; band over the ${col.bandline === "innerquartiles" ? "inner quartiles" : "range"}`;
|
|
508
|
+
return out;
|
|
509
|
+
}
|
|
510
|
+
function pointsAt(values, idx) {
|
|
511
|
+
const v = (i) => values[i];
|
|
512
|
+
let min = idx[0];
|
|
513
|
+
let max = min;
|
|
514
|
+
for (const i of idx) {
|
|
515
|
+
if (v(i) < v(min)) min = i;
|
|
516
|
+
if (v(i) > v(max)) max = i;
|
|
517
|
+
}
|
|
518
|
+
return { first: idx[0], last: idx.at(-1), min, max };
|
|
519
|
+
}
|
|
520
|
+
function markColors(h, at, idx, field) {
|
|
521
|
+
const named = h === void 0 ? {} : Array.isArray(h) ? Object.fromEntries(h.map((n) => [n, "red"])) : h;
|
|
522
|
+
const out = /* @__PURE__ */ new Map();
|
|
523
|
+
const all = named.all;
|
|
524
|
+
if (all !== void 0) for (const i of idx) out.set(i, att(all, field));
|
|
525
|
+
for (const n of NAMED2) {
|
|
526
|
+
const c = named[n];
|
|
527
|
+
if (c !== void 0) out.set(at[n], att(c, field));
|
|
528
|
+
}
|
|
529
|
+
return new Map([...out].sort((a, b) => a[0] - b[0]));
|
|
530
|
+
}
|
|
531
|
+
var labelled = (labels, at, idx) => labels.includes("all") ? [...idx] : [...new Set(labels.map((n) => at[n]))].sort((a, b) => a - b);
|
|
532
|
+
function chartSvg(kind, w, h, label, body) {
|
|
533
|
+
return `<svg class="sdvt-${kind}" xmlns="http://www.w3.org/2000/svg" width="${n2(w)}" height="${n2(h)}" viewBox="0 0 ${n2(w)} ${n2(h)}" role="img" aria-label="${escapeAttr(label)}" style="display:inline-block;vertical-align:middle;overflow:visible"><title>${escapeHtml(label)}</title>${body}</svg>`;
|
|
534
|
+
}
|
|
535
|
+
var present = (values) => values.filter((v) => v !== null);
|
|
536
|
+
var presentIdx = (values) => values.flatMap((v, i) => v === null ? [] : [i]);
|
|
537
|
+
function sparklineSvg(col, values, color, row) {
|
|
538
|
+
const xs = present(values);
|
|
539
|
+
const idx = presentIdx(values);
|
|
540
|
+
const f = frameOf(col, xs, false, row);
|
|
541
|
+
const n = values.length;
|
|
542
|
+
const step = n > 1 ? (f.right - f.left) / (n - 1) : 0;
|
|
543
|
+
const pts = new Map(
|
|
544
|
+
idx.map((i) => [i, [n > 1 ? f.left + i * step : (f.left + f.right) / 2, f.y(values[i])]])
|
|
545
|
+
);
|
|
546
|
+
const path = [];
|
|
547
|
+
for (let i = 0; i < n; i++) {
|
|
548
|
+
const p = pts.get(i);
|
|
549
|
+
if (!p) continue;
|
|
550
|
+
const down = pts.has(i - 1);
|
|
551
|
+
path.push(`${down ? "L" : "M"}${n2(p[0])},${n2(p[1])}`);
|
|
552
|
+
if (!down && !pts.has(i + 1)) path.push("h0");
|
|
553
|
+
}
|
|
554
|
+
const stroke = att(color, "sparkline lineColor");
|
|
555
|
+
let body = bandlineSvg(col, f, xs);
|
|
556
|
+
if (col.showArea) {
|
|
557
|
+
const runs = [];
|
|
558
|
+
for (const i of idx) {
|
|
559
|
+
if (pts.has(i - 1)) runs.at(-1)?.push(pts.get(i));
|
|
560
|
+
else runs.push([pts.get(i)]);
|
|
561
|
+
}
|
|
562
|
+
const b = n2(f.bottom);
|
|
563
|
+
const d = runs.map(
|
|
564
|
+
(r) => `M${n2(r[0][0])},${b} ${r.map(([x, y]) => `L${n2(x)},${n2(y)}`).join(" ")} L${n2(r.at(-1)[0])},${b} Z`
|
|
565
|
+
).join(" ");
|
|
566
|
+
const fill = col.areaColor === void 0 ? stroke : att(col.areaColor, "sparkline areaColor");
|
|
567
|
+
body += `<path class="sdvt-area" d="${d}" fill="${fill}" fill-opacity="${col.areaOpacity}"/>`;
|
|
568
|
+
}
|
|
569
|
+
body += `<path class="sdvt-line" d="${path.join(" ")}" fill="none" stroke="${stroke}" stroke-width="${col.lineWidth}" stroke-linejoin="round" stroke-linecap="round"/>`;
|
|
570
|
+
body += statlineSvg(col, f, xs);
|
|
571
|
+
const at = pointsAt(values, idx);
|
|
572
|
+
for (const [i, c] of markColors(col.highlightPoints, at, idx, "sparkline highlightPoints")) {
|
|
573
|
+
const [x, y] = pts.get(i);
|
|
574
|
+
body += `<circle class="sdvt-point" cx="${n2(x)}" cy="${n2(y)}" r="${col.pointSize}" fill="${c}" stroke="${c}"/>`;
|
|
575
|
+
}
|
|
576
|
+
const endsOnly = col.labels.length > 0 && !col.labels.some((l) => l === "min" || l === "max" || l === "all");
|
|
577
|
+
const size = att(col.labelSize, "sparkline labelSize");
|
|
578
|
+
for (const i of labelled(col.labels, at, idx)) {
|
|
579
|
+
const [x, y] = pts.get(i);
|
|
580
|
+
const pos = endsOnly && i === at.first ? `x="${n2(x - 6)}" y="${n2(y)}" text-anchor="end" dominant-baseline="middle"` : endsOnly && i === at.last ? `x="${n2(x + 6)}" y="${n2(y)}" text-anchor="start" dominant-baseline="middle"` : `x="${n2(x)}" y="${n2(y - 7)}" text-anchor="middle"`;
|
|
581
|
+
body += `<text class="sdvt-label" ${pos} fill="currentColor" font-size="${size}">${fixed(values[i], col.decimals)}</text>`;
|
|
582
|
+
}
|
|
583
|
+
return chartSvg("sparkline", col.chartWidth, f.h, summary("Sparkline", col, xs), body);
|
|
584
|
+
}
|
|
585
|
+
function sparkbarSvg(col, values, color, row) {
|
|
586
|
+
const xs = present(values);
|
|
587
|
+
const idx = presentIdx(values);
|
|
588
|
+
const f = frameOf(col, xs, true, row);
|
|
589
|
+
const band = (f.right - f.left) / values.length;
|
|
590
|
+
const barW = band * 0.8;
|
|
591
|
+
const base = f.y(Math.min(Math.max(0, f.lo), f.hi));
|
|
592
|
+
const at = pointsAt(values, idx);
|
|
593
|
+
const marks = markColors(col.highlightBars, at, idx, "sparkbar highlightBars");
|
|
594
|
+
const fill = att(color, "sparkbar fillColor");
|
|
595
|
+
let body = bandlineSvg(col, f, xs);
|
|
596
|
+
const tops = /* @__PURE__ */ new Map();
|
|
597
|
+
for (const i of idx) {
|
|
598
|
+
const x = f.left + i * band + (band - barW) / 2;
|
|
599
|
+
const y = f.y(values[i]);
|
|
600
|
+
tops.set(i, [x + barW / 2, Math.min(y, base)]);
|
|
601
|
+
body += `<rect class="sdvt-bar" x="${n2(x)}" y="${n2(Math.min(y, base))}" width="${n2(barW)}" height="${n2(Math.abs(base - y))}" fill="${marks.get(i) ?? fill}" fill-opacity="${col.fillOpacity}"/>`;
|
|
602
|
+
}
|
|
603
|
+
body += statlineSvg(col, f, xs);
|
|
604
|
+
const size = att(col.labelSize, "sparkbar labelSize");
|
|
605
|
+
for (const i of labelled(col.labels, at, idx)) {
|
|
606
|
+
const [x, y] = tops.get(i);
|
|
607
|
+
body += `<text class="sdvt-label" x="${n2(x)}" y="${n2(y - 3)}" text-anchor="middle" fill="currentColor" font-size="${size}">${fixed(values[i], col.decimals)}</text>`;
|
|
608
|
+
}
|
|
609
|
+
return chartSvg("sparkbar", col.chartWidth, f.h, summary("Bar chart", col, xs), body);
|
|
610
|
+
}
|
|
611
|
+
function arc(c, outer, inner, frac) {
|
|
612
|
+
const t = Math.PI * (1 - frac);
|
|
613
|
+
const cos = Math.cos(t);
|
|
614
|
+
const sin = Math.sin(t);
|
|
615
|
+
return `M${n2(c - outer)},${n2(c)} A${n2(outer)},${n2(outer)} 0 0 1 ${n2(c + outer * cos)},${n2(c - outer * sin)} L${n2(c + inner * cos)},${n2(c - inner * sin)} A${n2(inner)},${n2(inner)} 0 0 0 ${n2(c - inner)},${n2(c)} Z`;
|
|
616
|
+
}
|
|
617
|
+
function gaugeSvg(col, v, lo, hi, color, fmt) {
|
|
618
|
+
const w = 50 * col.size;
|
|
619
|
+
const r = w / 2;
|
|
620
|
+
const inner = r * 0.75;
|
|
621
|
+
const frac = hi === lo ? 0 : Math.min(Math.max((Math.abs(v) - lo) / (hi - lo), 0), 1);
|
|
622
|
+
const ink = col.textColor === void 0 ? "currentColor" : att(col.textColor, "gauge textColor");
|
|
623
|
+
const weight = col.boldText ? ' font-weight="bold"' : "";
|
|
624
|
+
const font = col.textSize === void 0 ? "" : ` font-size="${att(col.textSize, "gauge textSize")}"`;
|
|
625
|
+
let body = `<path class="sdvt-track" d="${arc(r, r, inner, 1)}" fill="${att(col.background, "gauge background")}"/>`;
|
|
626
|
+
if (frac > 0)
|
|
627
|
+
body += `<path class="sdvt-fill" d="${arc(r, r, inner, frac)}" fill="${color}" fill-opacity="${col.opacity}"/>`;
|
|
628
|
+
body += `<text class="sdvt-value" x="${n2(r)}" y="${n2(r - 2)}" text-anchor="middle" fill="${ink}"${weight}${font}>${escapeHtml(fmt(v))}</text>`;
|
|
629
|
+
if (col.showMinMax) {
|
|
630
|
+
const y = n2(r + 10 * col.size);
|
|
631
|
+
body += `<text class="sdvt-min" x="0" y="${y}" text-anchor="start" fill="${ink}" font-size="0.7em">${escapeHtml(fmt(lo))}</text><text class="sdvt-max" x="${n2(w)}" y="${y}" text-anchor="end" fill="${ink}" font-size="0.7em">${escapeHtml(fmt(hi))}</text>`;
|
|
632
|
+
}
|
|
633
|
+
const gauged = v < 0 ? ` (its absolute value, ${fmt(Math.abs(v))}, is gauged)` : "";
|
|
634
|
+
const label = `Gauge: ${fmt(v)}${gauged} on a scale from ${fmt(lo)} to ${fmt(hi)}`;
|
|
635
|
+
return chartSvg("gauge", w, r + (col.showMinMax ? 12 * col.size : 0), label, body);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/html/cells.ts
|
|
639
|
+
var cellValue = (row, key) => row[key];
|
|
640
|
+
var blankOr = (v) => isBlank(v) ? "" : escapeHtml(v);
|
|
641
|
+
var tableBg = (ctx) => ctx.theme.tokens.bg === "transparent" ? "#ffffff" : hex6(ctx.theme.tokens.bg);
|
|
642
|
+
function markImg(src, alt, heightPx, teamId) {
|
|
643
|
+
return `<img class="sdvt-mark" src="${escapeAttr(src)}" alt="${escapeAttr(alt)}" style="height:${heightPx}px"${teamId ? ` data-sdvplot-team="${escapeAttr(teamId)}"` : ""}>`;
|
|
644
|
+
}
|
|
645
|
+
function readableInk(primary, secondary, background, target = 4.5) {
|
|
646
|
+
const toward = onColor(background);
|
|
647
|
+
for (const c of [primary, secondary]) if (c !== void 0 && contrast(c, background) >= target) return c;
|
|
648
|
+
for (let i = 0; i < 20; i++) {
|
|
649
|
+
const c = mix(toward, primary, Math.max(0.95 + i * -0.05, 0));
|
|
650
|
+
if (contrast(c, background) >= target) return c;
|
|
651
|
+
}
|
|
652
|
+
return toward;
|
|
653
|
+
}
|
|
654
|
+
function checkHeight(h, kind) {
|
|
655
|
+
if (!(Number.isFinite(h) && h >= 1))
|
|
656
|
+
throw new TableSpecError(`${kind} height must be a number of pixels >= 1, got ${String(h)}`);
|
|
657
|
+
return h;
|
|
658
|
+
}
|
|
659
|
+
function teamIdsOf(spec, rows) {
|
|
660
|
+
const out = /* @__PURE__ */ new Map();
|
|
661
|
+
for (const c of spec.columns) {
|
|
662
|
+
checkRefs(c);
|
|
663
|
+
checkChart(c);
|
|
664
|
+
const optIn = "team" in c && c.kind !== "mergeStackTeamColor" && c.team !== void 0;
|
|
665
|
+
if (!optIn && c.kind !== "logo" && c.kind !== "wordmark" && c.kind !== "mergeStackTeamColor" && c.kind !== "teamColorBar" && c.kind !== "teamColorBg")
|
|
666
|
+
continue;
|
|
667
|
+
if (c.league === void 0)
|
|
668
|
+
throw new TableSpecError(`column "${c.key}" names a team column, so it needs a league`);
|
|
669
|
+
const from = "team" in c && c.team !== void 0 ? c.team : c.key;
|
|
670
|
+
const values = rows.map((r) => {
|
|
671
|
+
const v = cellValue(r, from);
|
|
672
|
+
return isBlank(v) ? null : String(v).trim();
|
|
673
|
+
});
|
|
674
|
+
out.set(
|
|
675
|
+
c.key,
|
|
676
|
+
resolveSync(values, c.league, {
|
|
677
|
+
...c.season !== void 0 ? { season: c.season } : {},
|
|
678
|
+
...c.idSystem ? { idSystem: c.idSystem } : {},
|
|
679
|
+
strict: c.strict ?? false
|
|
680
|
+
})
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
return out;
|
|
684
|
+
}
|
|
685
|
+
var pillLabel = (col, v) => formatValue(
|
|
686
|
+
v !== null && col.formatType === "percent" && col.scalePercent ? v * 100 : v,
|
|
687
|
+
col.digits,
|
|
688
|
+
col.formatType,
|
|
689
|
+
col.suffix
|
|
690
|
+
);
|
|
691
|
+
var isNa = (col, v) => isBlank(v) || col.naStrings.some((n) => col.ignoreCase ? n.toLowerCase() === String(v).toLowerCase() : n === String(v));
|
|
692
|
+
var fmtNum = (n, f) => `${f?.prefix ?? ""}${formatValue(n * (f?.scale ?? 1), f?.digits, f?.type ?? "number", f?.suffix ?? "")}`;
|
|
693
|
+
var clamp01 = (x) => x < 0 ? 0 : x > 1 ? 1 : x;
|
|
694
|
+
function withAlpha(color, opacity, field) {
|
|
695
|
+
if (!(opacity >= 0 && opacity <= 1))
|
|
696
|
+
throw new TableSpecError(`${field} must be between 0 and 1, got ${opacity}`);
|
|
697
|
+
if (opacity === 1) return cssValue(color, field);
|
|
698
|
+
const a = Math.round(opacity * 255).toString(16).padStart(2, "0");
|
|
699
|
+
return `${hex6(color, { dropAlpha: true })}${a}`;
|
|
700
|
+
}
|
|
701
|
+
var STATS = /* @__PURE__ */ new WeakMap();
|
|
702
|
+
function statsOf(ctx, key) {
|
|
703
|
+
let byKey = STATS.get(ctx.domainRows);
|
|
704
|
+
if (!byKey) {
|
|
705
|
+
byKey = /* @__PURE__ */ new Map();
|
|
706
|
+
STATS.set(ctx.domainRows, byKey);
|
|
707
|
+
}
|
|
708
|
+
const hit = byKey.get(key);
|
|
709
|
+
if (hit) return hit;
|
|
710
|
+
const nums = ctx.domainRows.map((r) => toNumber(cellValue(r, key)));
|
|
711
|
+
const present2 = nums.filter((n) => n !== null);
|
|
712
|
+
const fin = present2.filter(Number.isFinite);
|
|
713
|
+
const st = {
|
|
714
|
+
nums,
|
|
715
|
+
min: present2.length ? Math.min(...present2) : Number.NaN,
|
|
716
|
+
max: present2.length ? Math.max(...present2) : Number.NaN,
|
|
717
|
+
maxAbs: present2.length ? Math.max(...present2.map(Math.abs)) : Number.NaN,
|
|
718
|
+
finite: fin.length ? [Math.min(...fin), Math.max(...fin), Math.max(...fin.map(Math.abs))] : [Number.NaN, Number.NaN, Number.NaN],
|
|
719
|
+
index: new Map(ctx.domainRows.map((r, j) => [r, j])),
|
|
720
|
+
tiles: /* @__PURE__ */ new Map()
|
|
721
|
+
};
|
|
722
|
+
byKey.set(key, st);
|
|
723
|
+
return st;
|
|
724
|
+
}
|
|
725
|
+
function tileOf(ctx, key, row, i, n) {
|
|
726
|
+
const st = statsOf(ctx, key);
|
|
727
|
+
const j = ctx.domainRows === ctx.rows ? i : st.index.get(row);
|
|
728
|
+
if (j === void 0) return ntile([...st.nums, toNumber(cellValue(row, key))], n).at(-1) ?? null;
|
|
729
|
+
let t = st.tiles.get(n);
|
|
730
|
+
if (!t) {
|
|
731
|
+
t = ntile(st.nums, n);
|
|
732
|
+
st.tiles.set(n, t);
|
|
733
|
+
}
|
|
734
|
+
return t[j] ?? null;
|
|
735
|
+
}
|
|
736
|
+
function sizeOf(v, st, minValue, maxValue) {
|
|
737
|
+
const top = maxValue ?? st.maxAbs;
|
|
738
|
+
const r = minValue === void 0 ? Math.abs(v) / top : (Math.abs(v) - minValue) / (top - minValue);
|
|
739
|
+
return Number.isFinite(r) ? clamp01(r) : 0;
|
|
740
|
+
}
|
|
741
|
+
var normalized = (v, lo, hi) => hi === lo ? 1 : clamp01((v - lo) / (hi - lo));
|
|
742
|
+
function dataColor(v) {
|
|
743
|
+
if (isBlank(v)) return void 0;
|
|
744
|
+
const s = String(v).trim();
|
|
745
|
+
const named = namedColor(s);
|
|
746
|
+
if (named !== void 0) return named;
|
|
747
|
+
try {
|
|
748
|
+
hex6(s, { dropAlpha: true });
|
|
749
|
+
return s;
|
|
750
|
+
} catch {
|
|
751
|
+
return void 0;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function refColor(col, row, i, ctx) {
|
|
755
|
+
if (col.colorRef !== void 0) {
|
|
756
|
+
const c = dataColor(cellValue(row, col.colorRef));
|
|
757
|
+
if (c !== void 0) return c;
|
|
758
|
+
}
|
|
759
|
+
if (col.team === void 0 || col.league === void 0) return void 0;
|
|
760
|
+
const id = ctx.teamIds.get(col.key)?.[i];
|
|
761
|
+
return id === void 0 ? void 0 : teamColorsSync(col.league, id, { which: col.which ?? "primary", idSystem: "team_id" });
|
|
762
|
+
}
|
|
763
|
+
function checkRefs(c) {
|
|
764
|
+
if (!("mark" in c) || c.mark === void 0) return;
|
|
765
|
+
if ((c.mark === "logo" || c.mark === "wordmark") && (c.team === void 0 || c.league === void 0))
|
|
766
|
+
throw new TableSpecError(
|
|
767
|
+
`column "${c.key}": mark "${c.mark}" reads the team column, so set team and league`
|
|
768
|
+
);
|
|
769
|
+
if (c.mark === "headshot" && (c.markKey === void 0 || c.league === void 0))
|
|
770
|
+
throw new TableSpecError(
|
|
771
|
+
`column "${c.key}": mark "headshot" reads an ESPN athlete id, so set markKey and league`
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
function markOf(col, row, i, ctx) {
|
|
775
|
+
if (col.mark === void 0 || col.league === void 0) return "";
|
|
776
|
+
const h = checkHeight(col.markHeight ?? 20, `${col.kind} mark`);
|
|
777
|
+
if (col.mark === "headshot") {
|
|
778
|
+
const raw = col.markKey === void 0 ? null : cellValue(row, col.markKey);
|
|
779
|
+
if (isBlank(raw)) return "";
|
|
780
|
+
const url2 = headshotUrl(String(raw).trim(), col.league, { idSystem: "espn" });
|
|
781
|
+
return url2 ? markImg(url2, String(raw).trim(), h) : "";
|
|
782
|
+
}
|
|
783
|
+
const id = ctx.teamIds.get(col.key)?.[i];
|
|
784
|
+
if (id === void 0) return "";
|
|
785
|
+
const url = logoUrlSync(id, col.league, {
|
|
786
|
+
...col.season !== void 0 ? { season: col.season } : {},
|
|
787
|
+
markType: col.mark,
|
|
788
|
+
idSystem: "team_id"
|
|
789
|
+
});
|
|
790
|
+
return url ? markImg(url, teamNameSync(col.league, id) ?? id, h, id) : "";
|
|
791
|
+
}
|
|
792
|
+
function dataBar(col, row, i, ctx) {
|
|
793
|
+
const raw = cellValue(row, col.key);
|
|
794
|
+
const v = toNumber(raw);
|
|
795
|
+
if (v === null) return blankOr(raw);
|
|
796
|
+
if (col.fill.length === 0) throw new TableSpecError("dataBar fill needs at least one colour");
|
|
797
|
+
if (col.gradient && col.fill.length < 2)
|
|
798
|
+
throw new TableSpecError("dataBar gradient needs at least two colours in fill");
|
|
799
|
+
const st = statsOf(ctx, col.key);
|
|
800
|
+
const pos = col.textPosition;
|
|
801
|
+
const h = col.barHeight !== void 0 ? checkPx(col.barHeight, "dataBar barHeight") : pos === "above" ? 5 : 18;
|
|
802
|
+
const radius = col.roundEdges ? h / 2 : 0;
|
|
803
|
+
const posNeg = col.posNeg ?? st.min < 0;
|
|
804
|
+
const ref = refColor(col, row, i, ctx);
|
|
805
|
+
const fill = ref ?? (posNeg && col.fill.length === 2 ? col.fill[v < 0 ? 0 : 1] : col.fill.length === 1 ? col.fill[0] : ramp(
|
|
806
|
+
col.fill.map((c) => hex6(c)),
|
|
807
|
+
[0, 1],
|
|
808
|
+
col.bias
|
|
809
|
+
)(normalized(v, st.min, st.max)));
|
|
810
|
+
const gradient = col.gradient ? `linear-gradient(to right, ${col.fill.join(", ")})` : void 0;
|
|
811
|
+
const inside = pos === "inside-end" || pos === "inside-base" || pos === "center";
|
|
812
|
+
const inkOn = col.gradient ? col.fill.at(v < 0 ? 0 : -1) : fill;
|
|
813
|
+
const ink = pos === "none" ? "transparent" : col.textColor ?? (inside ? onColor(solid(inkOn)) : void 0);
|
|
814
|
+
const label = escapeHtml(fmtNum(v, col.numberFormat));
|
|
815
|
+
const m = markOf(col, row, i, ctx);
|
|
816
|
+
const mark = m ? `<span style="display:inline-flex;margin:0 4px">${m}</span>` : "";
|
|
817
|
+
const right = col.align === "right";
|
|
818
|
+
const width = Number((sizeOf(v, st, col.minValue, col.maxValue) * 100).toFixed(4));
|
|
819
|
+
const bar = (dir2, text, gap) => `<div style="${styleAttr({
|
|
820
|
+
display: "flex",
|
|
821
|
+
"align-items": "center",
|
|
822
|
+
"flex-direction": dir2,
|
|
823
|
+
"justify-content": pos === "center" ? "center" : pos === "inside-base" ? "flex-start" : "flex-end",
|
|
824
|
+
background: gradient ? void 0 : fill,
|
|
825
|
+
"background-image": gradient,
|
|
826
|
+
"border-radius": `${radius}px`,
|
|
827
|
+
color: text ? ink : void 0,
|
|
828
|
+
width: `${width}%`,
|
|
829
|
+
height: `${h}px`,
|
|
830
|
+
"white-space": "nowrap",
|
|
831
|
+
[dir2 === "row" ? "margin-right" : "margin-left"]: gap ? "6px" : void 0
|
|
832
|
+
})}">${text ? `<span>${label}</span>` : ""}</div>`;
|
|
833
|
+
const textSpan = `<span${ink ? ` style="color:${escapeAttr(cssValue(ink, "dataBar textColor"))}"` : ""}>${label}</span>`;
|
|
834
|
+
const group = (dir2) => inside || pos === "none" ? `<div style="display:flex;align-items:center;flex-direction:${dir2};width:100%">${bar(dir2, true, false)}${mark}</div>` : `<div style="display:flex;align-items:center;flex-direction:${dir2};width:100%">${bar(dir2, false, true)}${mark}${textSpan}</div>`;
|
|
835
|
+
if (posNeg) {
|
|
836
|
+
const half = (side) => `<div style="flex:1 1 0;display:flex;align-items:center${side === "neg" ? ";justify-content:flex-end" : ""}">${v < 0 === (side === "neg") ? group(side === "neg" ? "row-reverse" : "row") : ""}</div>`;
|
|
837
|
+
return `<div class="sdvt-databar" style="display:flex;align-items:stretch;min-height:${h}px">${half("neg")}<div style="width:1px;background:#666666"></div>${half("pos")}</div>`;
|
|
838
|
+
}
|
|
839
|
+
const dir = right ? "row-reverse" : "row";
|
|
840
|
+
const track = (inner, gap = false) => `<div style="${styleAttr({ "flex-grow": 1, "border-radius": `${radius}px`, background: col.background, [right ? "margin-right" : "margin-left"]: gap ? "6px" : void 0 })}">${inner}</div>`;
|
|
841
|
+
if (pos === "above")
|
|
842
|
+
return `<div class="sdvt-databar"><div style="text-align:${right ? "right" : "left"}${ink ? `;color:${escapeAttr(cssValue(ink, "dataBar textColor"))}` : ""}">${label}</div>${track(`<div style="display:flex;align-items:center;flex-direction:${dir}">${bar(dir, false, false)}${mark}</div>`)}</div>`;
|
|
843
|
+
if (pos === "outside-base") {
|
|
844
|
+
const chars = Math.max(
|
|
845
|
+
1,
|
|
846
|
+
...st.nums.filter((n) => n !== null).map((n) => fmtNum(n, col.numberFormat).length)
|
|
847
|
+
);
|
|
848
|
+
return `<div class="sdvt-databar" style="display:flex;align-items:center;flex-direction:${dir}"><span style="${styleAttr({ "min-width": `${chars}ch`, "text-align": right ? "left" : "right", "font-variant-numeric": "tabular-nums", color: ink })}">${label}</span>${track(`<div style="display:flex;align-items:center;flex-direction:${dir}">${bar(dir, false, false)}${mark}</div>`, true)}</div>`;
|
|
849
|
+
}
|
|
850
|
+
return `<div class="sdvt-databar" style="display:flex">${track(group(dir))}</div>`;
|
|
851
|
+
}
|
|
852
|
+
function iconSet(col, row, i, ctx) {
|
|
853
|
+
const raw = cellValue(row, col.key);
|
|
854
|
+
const v = toNumber(raw);
|
|
855
|
+
if (v === null) return blankOr(raw);
|
|
856
|
+
const named = col.iconRef === void 0 ? void 0 : cellValue(row, col.iconRef);
|
|
857
|
+
const icon = !isBlank(named) && ICON_NAMES.includes(String(named).trim()) ? String(named).trim() : col.icons[(tileOf(ctx, col.key, row, i, col.icons.length) ?? 1) - 1];
|
|
858
|
+
const color = (col.iconColorRef === void 0 ? void 0 : dataColor(cellValue(row, col.iconColorRef))) ?? col.colors[(tileOf(ctx, col.key, row, i, col.colors.length) ?? 1) - 1];
|
|
859
|
+
const svg = iconSvg(
|
|
860
|
+
icon,
|
|
861
|
+
withAlpha(color, col.opacity, "iconSet colors"),
|
|
862
|
+
checkPx(col.iconSize, "iconSet iconSize")
|
|
863
|
+
);
|
|
864
|
+
const label = escapeHtml(fmtNum(v, col.numberFormat));
|
|
865
|
+
switch (col.iconPosition) {
|
|
866
|
+
case "left":
|
|
867
|
+
return `<span style="display:inline-block;margin-right:8px">${svg}</span>${label}`;
|
|
868
|
+
case "above":
|
|
869
|
+
return `<div>${svg}</div><div>${label}</div>`;
|
|
870
|
+
case "below":
|
|
871
|
+
return `<div>${label}</div><div>${svg}</div>`;
|
|
872
|
+
case "over":
|
|
873
|
+
return `${svg}<span style="font-size:0;color:transparent">${label}</span>`;
|
|
874
|
+
default:
|
|
875
|
+
return `${label}<span style="display:inline-block;margin-left:8px">${svg}</span>`;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function iconAssign(col, row, i, ctx) {
|
|
879
|
+
const raw = cellValue(row, col.key);
|
|
880
|
+
const v = toNumber(raw);
|
|
881
|
+
if (v === null) return blankOr(raw);
|
|
882
|
+
const size = checkPx(col.iconSize, "iconAssign iconSize");
|
|
883
|
+
const on = withAlpha(col.fill, col.fillOpacity, "iconAssign fill");
|
|
884
|
+
const off = withAlpha(col.empty, col.emptyOpacity, "iconAssign empty");
|
|
885
|
+
const half = (x) => Math.floor(x + 0.5);
|
|
886
|
+
let steps;
|
|
887
|
+
let filled;
|
|
888
|
+
let title;
|
|
889
|
+
if (col.buckets !== void 0) {
|
|
890
|
+
const b = tileOf(ctx, col.key, row, i, col.buckets) ?? 0;
|
|
891
|
+
steps = Array.from({ length: col.buckets }, (_, k) => k + 1);
|
|
892
|
+
filled = (s) => s <= half(b);
|
|
893
|
+
title = `${b} out of ${col.buckets}`;
|
|
894
|
+
} else {
|
|
895
|
+
if (!(col.seqBy > 0)) throw new TableSpecError(`iconAssign seqBy must be positive, got ${col.seqBy}`);
|
|
896
|
+
const top = half(statsOf(ctx, col.key).max);
|
|
897
|
+
const n = half(v);
|
|
898
|
+
steps = [];
|
|
899
|
+
if (top === 0) steps.push(0);
|
|
900
|
+
else for (let s = 1; s <= top; s += col.seqBy) steps.push(s);
|
|
901
|
+
if (steps.length > 1e3)
|
|
902
|
+
throw new TableSpecError(`iconAssign would draw ${steps.length} icons per cell; set buckets or seqBy`);
|
|
903
|
+
filled = top === 0 ? (s) => s < n : (s) => s <= n;
|
|
904
|
+
title = `${naturalDigits(v)} out of ${top}`;
|
|
905
|
+
}
|
|
906
|
+
const icons = steps.map((s) => iconSvg(col.icon, filled(s) ? on : off, size)).join("");
|
|
907
|
+
const box = `<span title="${escapeAttr(title)}" aria-label="${escapeAttr(title)}" role="img">${icons}</span>`;
|
|
908
|
+
const label = escapeHtml(fmtNum(v, col.numberFormat));
|
|
909
|
+
switch (col.showValues) {
|
|
910
|
+
case "right":
|
|
911
|
+
return `${box}<span style="margin-left:6px">${label}</span>`;
|
|
912
|
+
case "left":
|
|
913
|
+
return `<span style="margin-right:6px">${label}</span>${box}`;
|
|
914
|
+
case "above":
|
|
915
|
+
return `<div>${label}</div><div>${box}</div>`;
|
|
916
|
+
case "below":
|
|
917
|
+
return `<div>${box}</div><div>${label}</div>`;
|
|
918
|
+
default:
|
|
919
|
+
return box;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
function mergeStack(col, row, i, ctx) {
|
|
923
|
+
const main = `font-size:${checkPx(col.size, "mergeStack size")}px;${styleAttr({ color: col.color, "font-weight": col.weight, "font-style": col.style })}`;
|
|
924
|
+
const sub = `font-size:${checkPx(col.stackSize, "mergeStack stackSize")}px;${styleAttr({ color: col.stackColor, "font-weight": col.stackWeight, "font-style": col.stackStyle })}`;
|
|
925
|
+
const gap = checkPx(col.spacing, "mergeStack spacing") + 3;
|
|
926
|
+
const a = blankOr(cellValue(row, col.key));
|
|
927
|
+
const b = blankOr(cellValue(row, col.stack));
|
|
928
|
+
const body = col.position === "below" ? `<div><div style="${main};margin-bottom:${gap}px">${a}</div><div style="${sub}">${b}</div></div>` : col.position === "above" ? `<div><div style="${sub};margin-bottom:${gap}px">${b}</div><div style="${main}">${a}</div></div>` : col.position === "left" ? `<span style="${sub};margin-right:${gap}px">${b}</span><span style="${main}">${a}</span>` : `<span style="${main};margin-right:${gap}px">${a}</span><span style="${sub}">${b}</span>`;
|
|
929
|
+
const m = markOf(col, row, i, ctx);
|
|
930
|
+
const stacked = col.position === "below" || col.position === "above";
|
|
931
|
+
return m ? `<div style="display:flex;align-items:center;gap:6px">${m}${stacked ? body : `<div>${body}</div>`}</div>` : body;
|
|
932
|
+
}
|
|
933
|
+
function bubble(col, row, i, ctx) {
|
|
934
|
+
const raw = cellValue(row, col.key);
|
|
935
|
+
const v = toNumber(raw);
|
|
936
|
+
if (v === null) return blankOr(raw);
|
|
937
|
+
if (col.colors.length === 0) throw new TableSpecError("bubble colors needs at least one colour");
|
|
938
|
+
const st = statsOf(ctx, col.key);
|
|
939
|
+
const px = Number(
|
|
940
|
+
(sizeOf(v, st, col.minValue, col.maxValue) * checkPx(col.maxSize, "bubble maxSize")).toFixed(2)
|
|
941
|
+
);
|
|
942
|
+
const base = refColor(col, row, i, ctx) ?? ramp(
|
|
943
|
+
col.colors.map((c) => hex6(c)),
|
|
944
|
+
[0, 1],
|
|
945
|
+
col.bias
|
|
946
|
+
)(normalized(v, col.minValue ?? st.min, col.maxValue ?? st.max));
|
|
947
|
+
const fill = withAlpha(base, col.opacity, "bubble opacity");
|
|
948
|
+
const ink = col.showText ? col.textColor ?? onColor(solid(fill, tableBg(ctx))) : "transparent";
|
|
949
|
+
return `<div style="${styleAttr({
|
|
950
|
+
background: fill,
|
|
951
|
+
color: ink,
|
|
952
|
+
display: "inline-flex",
|
|
953
|
+
"justify-content": "center",
|
|
954
|
+
"align-items": "center",
|
|
955
|
+
"text-align": "center",
|
|
956
|
+
height: `${px}px`,
|
|
957
|
+
width: `${px}px`,
|
|
958
|
+
"border-radius": col.shape === "circles" ? "50%" : void 0
|
|
959
|
+
})}">${escapeHtml(fmtNum(v, col.numberFormat))}</div>`;
|
|
960
|
+
}
|
|
961
|
+
function chartColor(col, row, i, ctx) {
|
|
962
|
+
if (col.colorRef !== void 0) {
|
|
963
|
+
const c = dataColor(cellValue(row, col.colorRef));
|
|
964
|
+
if (c !== void 0) return c;
|
|
965
|
+
}
|
|
966
|
+
const id = col.team === void 0 ? void 0 : ctx.teamIds.get(col.key)?.[i];
|
|
967
|
+
if (id === void 0 || col.league === void 0) return void 0;
|
|
968
|
+
const which = col.which ?? "primary";
|
|
969
|
+
const own = teamColorsSync(col.league, id, { which, idSystem: "team_id" });
|
|
970
|
+
const other = teamColorsSync(col.league, id, {
|
|
971
|
+
which: which === "primary" ? "secondary" : "primary",
|
|
972
|
+
idSystem: "team_id"
|
|
973
|
+
});
|
|
974
|
+
const first = own ?? other;
|
|
975
|
+
return first === void 0 ? void 0 : readableInk(first, own === void 0 ? void 0 : other, tableBg(ctx), 3);
|
|
976
|
+
}
|
|
977
|
+
function seriesOf(v) {
|
|
978
|
+
if (!Array.isArray(v)) return null;
|
|
979
|
+
return v.map((x) => {
|
|
980
|
+
const n = typeof x === "number" || typeof x === "string" ? toNumber(x) : null;
|
|
981
|
+
return n !== null && Number.isFinite(n) ? n : null;
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
function seriesCell(col, row, i, ctx) {
|
|
985
|
+
const values = seriesOf(cellValue(row, col.key));
|
|
986
|
+
if (values === null || values.every((v) => v === null)) return "";
|
|
987
|
+
const color = chartColor(col, row, i, ctx);
|
|
988
|
+
return col.kind === "sparkline" ? sparklineSvg(col, values, color ?? col.lineColor, i) : sparkbarSvg(col, values, color ?? col.fillColor, i);
|
|
989
|
+
}
|
|
990
|
+
function gauge(col, row, i, ctx) {
|
|
991
|
+
const raw = cellValue(row, col.key);
|
|
992
|
+
const v = toNumber(raw);
|
|
993
|
+
if (v === null) return blankOr(raw);
|
|
994
|
+
if (!Number.isFinite(v)) return "";
|
|
995
|
+
const [cmin, cmax, cabs] = statsOf(ctx, col.key).finite;
|
|
996
|
+
const lo = col.minValue ?? 0;
|
|
997
|
+
const hi = col.maxValue ?? (Number.isFinite(cabs) ? cabs : Math.abs(v));
|
|
998
|
+
if (lo > hi)
|
|
999
|
+
throw new TableSpecError(
|
|
1000
|
+
`column "${col.key}" row ${i}: gauge minValue ${naturalDigits(lo)} is above the column's largest absolute value ${naturalDigits(hi)}; give maxValue too`
|
|
1001
|
+
);
|
|
1002
|
+
const color = chartColor(col, row, i, ctx) ?? (col.fill.length === 1 ? col.fill[0] : ramp(
|
|
1003
|
+
col.fill.map((c) => hex6(c)),
|
|
1004
|
+
[0, 1],
|
|
1005
|
+
col.bias
|
|
1006
|
+
)(normalized(v, Number.isFinite(cmin) ? cmin : v, Number.isFinite(cmax) ? cmax : v)));
|
|
1007
|
+
return gaugeSvg(
|
|
1008
|
+
col,
|
|
1009
|
+
v,
|
|
1010
|
+
lo,
|
|
1011
|
+
hi,
|
|
1012
|
+
escapeAttr(cssValue(color, "gauge fill")),
|
|
1013
|
+
(n) => fmtNum(n, col.numberFormat)
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
function extremeOf(col, row, ctx) {
|
|
1017
|
+
if (col.extreme === void 0) return col.when !== void 0 && matches(col.when, row) ? "max" : null;
|
|
1018
|
+
const v = toNumber(cellValue(row, col.key));
|
|
1019
|
+
if (v === null) return null;
|
|
1020
|
+
const st = statsOf(ctx, col.key);
|
|
1021
|
+
if (col.extreme !== "max" && v === st.min) return "min";
|
|
1022
|
+
if (col.extreme !== "min" && v === st.max) return "max";
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
var isScaled = (col) => col.kind === "colorPills" || col.kind === "colorRanks" || col.kind === "percentileBar";
|
|
1026
|
+
function columnScales(spec, rows, warnFn, domainRows = rows) {
|
|
1027
|
+
const at = domainRows === rows ? null : new Map(domainRows.map((r, j) => [r, j]));
|
|
1028
|
+
const missing = at !== null && rows.some((r) => !at.has(r));
|
|
1029
|
+
const out = /* @__PURE__ */ new Map();
|
|
1030
|
+
for (const col of spec.columns) {
|
|
1031
|
+
if (!isScaled(col)) continue;
|
|
1032
|
+
if (missing)
|
|
1033
|
+
warnFn(
|
|
1034
|
+
`sdvtables:domainRows:${col.key}`,
|
|
1035
|
+
`column "${col.key}": a rendered row is not in domainRows (matched by identity, not by value), so it is colored from its own value (a rank fill: not at all); pass the same row objects as rows`
|
|
1036
|
+
);
|
|
1037
|
+
if (col.domain === void 0 && domainRows.length === 0) continue;
|
|
1038
|
+
const nums = domainRows.map((r) => toNumber(cellValue(r, col.key)));
|
|
1039
|
+
let values = nums;
|
|
1040
|
+
let own = (n) => n;
|
|
1041
|
+
let spanned;
|
|
1042
|
+
let buckets;
|
|
1043
|
+
if (col.kind === "colorPills" && col.fillType === "rank") {
|
|
1044
|
+
values = averageRanks(nums, col.rankOrder === "desc");
|
|
1045
|
+
own = () => null;
|
|
1046
|
+
}
|
|
1047
|
+
if (col.kind === "colorRanks" && col.evenBreaks) {
|
|
1048
|
+
buckets = col.palette.map((c) => hex6(c));
|
|
1049
|
+
values = ntile(nums, buckets.length);
|
|
1050
|
+
own = () => null;
|
|
1051
|
+
} else if (col.kind === "colorRanks" && col.span && col.domain === void 0) {
|
|
1052
|
+
const keys = col.span === true ? [...new Set(domainRows.flatMap((r) => Object.keys(r)))].filter(
|
|
1053
|
+
(k) => domainRows.some((r) => typeof cellValue(r, k) === "number")
|
|
1054
|
+
) : col.span;
|
|
1055
|
+
spanned = domainOf(keys.map((k) => domainRows.map((r) => toNumber(cellValue(r, k)))));
|
|
1056
|
+
}
|
|
1057
|
+
if (col.kind === "percentileBar") {
|
|
1058
|
+
const present2 = nums.filter((n) => n !== null);
|
|
1059
|
+
const [lo, hi] = col.domain;
|
|
1060
|
+
const scale = col.scale;
|
|
1061
|
+
const proportion = scale === "auto" && present2.length > 0 && present2.every((n) => n >= 0 && n <= 1) && hi > 1;
|
|
1062
|
+
own = (n) => n === null ? null : typeof scale === "number" ? n * scale : proportion ? lo + n * (hi - lo) : n;
|
|
1063
|
+
values = nums.map(own);
|
|
1064
|
+
}
|
|
1065
|
+
const domain = buckets ? [1, buckets.length] : col.domain ?? spanned ?? domainOf([values]);
|
|
1066
|
+
const fmt = () => `(${naturalDigits(domain[0])} to ${naturalDigits(domain[1])})`;
|
|
1067
|
+
if (col.kind === "colorPills" && col.domain === void 0)
|
|
1068
|
+
warnFn(
|
|
1069
|
+
`sdvtables:domain:${col.key}:${domain.join(",")}`,
|
|
1070
|
+
`no domain given, so the colors span the observed range ${fmt()}; set domain to compare colors across tables or columns`
|
|
1071
|
+
);
|
|
1072
|
+
const palette = col.palette.map((c) => hex6(c));
|
|
1073
|
+
const ordered = col.reverse ? [...palette].reverse() : palette;
|
|
1074
|
+
const color = buckets ? (b) => ordered[b - 1] ?? null : ramp(ordered, domain, col.kind === "colorRanks" ? col.bias ?? 1 : 1);
|
|
1075
|
+
const outside = values.filter((v) => v !== null && color(v) === null).length;
|
|
1076
|
+
if (col.kind === "colorPills" && outside > 0)
|
|
1077
|
+
warnFn(
|
|
1078
|
+
`sdvtables:outside:${col.key}:${outside}:${domain.join(",")}`,
|
|
1079
|
+
`${outside} value(s) fall outside the domain ${fmt()} and are drawn grey`
|
|
1080
|
+
);
|
|
1081
|
+
const shown = at === null ? values : rows.map((r) => {
|
|
1082
|
+
const j = at.get(r);
|
|
1083
|
+
return j === void 0 ? own(toNumber(cellValue(r, col.key))) : values[j] ?? null;
|
|
1084
|
+
});
|
|
1085
|
+
const shownNums = at === null ? nums : rows.map((r) => toNumber(cellValue(r, col.key)));
|
|
1086
|
+
const labelWidth = col.kind === "colorPills" ? Math.max(1, ...shownNums.map((n) => pillLabel(col, n).length)) : 1;
|
|
1087
|
+
out.set(col.key, { domain, palette, reverse: col.reverse, values: shown, color, labelWidth });
|
|
1088
|
+
}
|
|
1089
|
+
return out;
|
|
1090
|
+
}
|
|
1091
|
+
function renderCell(col, row, i, ctx) {
|
|
1092
|
+
switch (col.kind) {
|
|
1093
|
+
case "text":
|
|
1094
|
+
case "colorResults":
|
|
1095
|
+
return blankOr(cellValue(row, col.key));
|
|
1096
|
+
// ---- Task 6
|
|
1097
|
+
case "num":
|
|
1098
|
+
case "int": {
|
|
1099
|
+
const n = toNumber(cellValue(row, col.key));
|
|
1100
|
+
if (n === null) return "";
|
|
1101
|
+
const sc = ctx.scaled.get(col.key);
|
|
1102
|
+
if (sc) return escapeHtml(scaledNumber(n, sc));
|
|
1103
|
+
return escapeHtml(
|
|
1104
|
+
col.kind === "int" ? formatNumber(n, { digits: 0, big: true }) : formatNumber(n, {
|
|
1105
|
+
...col.digits !== void 0 ? { digits: col.digits } : {},
|
|
1106
|
+
big: col.big ?? false,
|
|
1107
|
+
prefix: col.prefix ?? "",
|
|
1108
|
+
suffix: col.suffix ?? "",
|
|
1109
|
+
forceSign: col.forceSign ?? false
|
|
1110
|
+
})
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
case "pct": {
|
|
1114
|
+
const n = toNumber(cellValue(row, col.key));
|
|
1115
|
+
return n === null ? "" : escapeHtml(`${formatNumber(col.scale ? n * 100 : n, { digits: col.digits })}%`);
|
|
1116
|
+
}
|
|
1117
|
+
case "rank": {
|
|
1118
|
+
const n = toNumber(cellValue(row, col.key));
|
|
1119
|
+
if (n === null) return "";
|
|
1120
|
+
const o = ordinal(n);
|
|
1121
|
+
const num = String(Math.trunc(n));
|
|
1122
|
+
return col.superscript ? `${num}<sup style="font-size:${escapeAttr(cssValue(col.suffixSize, "rank suffixSize"))}">${o.slice(num.length)}</sup>` : escapeHtml(o);
|
|
1123
|
+
}
|
|
1124
|
+
case "delta": {
|
|
1125
|
+
const a = toNumber(cellValue(row, col.key));
|
|
1126
|
+
const b = toNumber(cellValue(row, col.to));
|
|
1127
|
+
if (a === null || b === null) return "";
|
|
1128
|
+
const d = col.percent ? a === 0 ? null : (b - a) / a : b - a;
|
|
1129
|
+
if (d === null || !Number.isFinite(d)) return "";
|
|
1130
|
+
const shown = col.arrows ? Math.abs(d) : d;
|
|
1131
|
+
const text = `${col.arrows ? d > 0 ? "\u25B2 " : d < 0 ? "\u25BC " : "" : ""}${formatNumber(col.percent ? shown * 100 : shown, { digits: col.decimals, forceSign: col.forceSign && !col.arrows })}${col.percent ? "%" : ""}`;
|
|
1132
|
+
const color = !col.color ? null : d > 0 ? col.colorPositive : d < 0 ? col.colorNegative : col.colorNeutral ?? null;
|
|
1133
|
+
return color ? `<span style="color:${escapeAttr(cssValue(color, "delta color"))}">${escapeHtml(text)}</span>` : escapeHtml(text);
|
|
1134
|
+
}
|
|
1135
|
+
case "tally": {
|
|
1136
|
+
const ns = col.keys.map((k) => toNumber(cellValue(row, k)));
|
|
1137
|
+
if (ns.some((n) => n === null)) {
|
|
1138
|
+
const v = cellValue(row, col.key);
|
|
1139
|
+
return blankOr(v);
|
|
1140
|
+
}
|
|
1141
|
+
const xs = ns;
|
|
1142
|
+
if (col.share && !(Number.isInteger(col.shareOf) && col.shareOf >= 0 && col.shareOf < xs.length))
|
|
1143
|
+
throw new TableSpecError(`tally shareOf must index (from 0) one of ${col.keys.join(", ")}`);
|
|
1144
|
+
const text = xs.map((n) => naturalDigits(n)).join(col.separator);
|
|
1145
|
+
const total = xs.reduce((s, n) => s + n, 0);
|
|
1146
|
+
if (!col.share || total === 0) return escapeHtml(text);
|
|
1147
|
+
return escapeHtml(
|
|
1148
|
+
`${text}${col.sharePrefix}${formatNumber((xs[col.shareOf] ?? 0) / total * 100, { digits: col.shareDecimals })}%${col.shareSuffix}`
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
// ---- Task 7
|
|
1152
|
+
case "logo":
|
|
1153
|
+
case "wordmark": {
|
|
1154
|
+
const raw = cellValue(row, col.key);
|
|
1155
|
+
const text = blankOr(raw);
|
|
1156
|
+
const id = ctx.teamIds.get(col.key)?.[i];
|
|
1157
|
+
if (id === void 0) return text;
|
|
1158
|
+
const url = logoUrlSync(id, col.league, {
|
|
1159
|
+
...col.season !== void 0 ? { season: col.season } : {},
|
|
1160
|
+
...col.variant ? { variant: col.variant } : {},
|
|
1161
|
+
markType: col.kind,
|
|
1162
|
+
idSystem: "team_id"
|
|
1163
|
+
});
|
|
1164
|
+
if (!url) return text;
|
|
1165
|
+
return markImg(url, teamNameSync(col.league, id) ?? String(raw), checkHeight(col.height, col.kind), id) + (col.kind === "logo" && col.includeName ? text : "");
|
|
1166
|
+
}
|
|
1167
|
+
case "headshot": {
|
|
1168
|
+
const raw = cellValue(row, col.key);
|
|
1169
|
+
if (isBlank(raw)) return "";
|
|
1170
|
+
const url = headshotUrl(String(raw).trim(), col.league, { idSystem: col.idSystem });
|
|
1171
|
+
return url ? markImg(url, String(raw).trim(), checkHeight(col.height, "headshot")) : blankOr(raw);
|
|
1172
|
+
}
|
|
1173
|
+
case "mergeStackTeamColor": {
|
|
1174
|
+
const id = ctx.teamIds.get(col.key)?.[i];
|
|
1175
|
+
const bg = col.background ? solid(col.background) : tableBg(ctx);
|
|
1176
|
+
const p = id === void 0 ? void 0 : teamColorsSync(col.league, id, { idSystem: "team_id" });
|
|
1177
|
+
const s = id === void 0 ? void 0 : teamColorsSync(col.league, id, { which: "secondary", idSystem: "team_id" });
|
|
1178
|
+
const ink = readableInk(p ?? "#bebebe", s, bg);
|
|
1179
|
+
return `<div style="line-height:${col.fontSizeTop - 2}px"><span style="${styleAttr({ "font-weight": "bold", "font-variant": "small-caps", color: col.color, "font-size": `${col.fontSizeTop}px` })}">${blankOr(cellValue(row, col.key))}</span></div>
|
|
1180
|
+
<div style="line-height:${col.fontSizeBottom - 2}px"><span style="${styleAttr({ "font-weight": "bold", color: ink, "font-size": `${col.fontSizeBottom}px` })}">${blankOr(cellValue(row, col.stack))}</span></div>`;
|
|
1181
|
+
}
|
|
1182
|
+
case "teamColorBar":
|
|
1183
|
+
case "teamColorBg":
|
|
1184
|
+
return blankOr(cellValue(row, col.key));
|
|
1185
|
+
// the color lives on the <td> (kindCellStyle)
|
|
1186
|
+
// ---- Task 8
|
|
1187
|
+
case "colorPills": {
|
|
1188
|
+
const sc = ctx.scales.get(col.key);
|
|
1189
|
+
if (!sc) return "";
|
|
1190
|
+
const pillHeight = checkPx(col.pillHeight, "colorPills pillHeight");
|
|
1191
|
+
const outlineWidth = checkPx(col.outlineWidth, "colorPills outlineWidth");
|
|
1192
|
+
const s = sc.values[i] ?? null;
|
|
1193
|
+
const width = sc.labelWidth;
|
|
1194
|
+
let fill;
|
|
1195
|
+
let text;
|
|
1196
|
+
if (s === null) {
|
|
1197
|
+
if (col.naColor === void 0) return "";
|
|
1198
|
+
fill = col.naColor;
|
|
1199
|
+
text = "";
|
|
1200
|
+
} else {
|
|
1201
|
+
fill = refColor(col, row, i, ctx) ?? sc.color(s) ?? "#808080";
|
|
1202
|
+
text = pillLabel(col, toNumber(cellValue(row, col.key)));
|
|
1203
|
+
}
|
|
1204
|
+
const ink = col.textColor ?? onColor(solid(fill, tableBg(ctx)));
|
|
1205
|
+
const outline = col.outlineColor !== void 0 ? `;border:${outlineWidth}px solid ${escapeAttr(cssValue(col.outlineColor, "colorPills outlineColor"))}` : "";
|
|
1206
|
+
return `<span style="display:inline-block;width:${width}ch;padding-left:3px;padding-right:3px;height:${pillHeight}px;line-height:${pillHeight}px;background-color:${escapeAttr(cssValue(fill, "colorPills naColor"))};color:${escapeAttr(cssValue(ink, "colorPills textColor"))};border-radius:10px;text-align:center${outline}">${escapeHtml(text)}</span>`;
|
|
1207
|
+
}
|
|
1208
|
+
case "colorRanks":
|
|
1209
|
+
case "highlight":
|
|
1210
|
+
return blankOr(cellValue(row, col.key));
|
|
1211
|
+
// the fill lives on the <td> (kindCellStyle)
|
|
1212
|
+
case "highlightNa":
|
|
1213
|
+
return isNa(col, cellValue(row, col.key)) ? escapeHtml(col.missingText ?? "") : blankOr(cellValue(row, col.key));
|
|
1214
|
+
case "percentileBar": {
|
|
1215
|
+
const sc = ctx.scales.get(col.key);
|
|
1216
|
+
const v = sc?.values[i] ?? null;
|
|
1217
|
+
const th = checkPx(col.trackHeight, "percentileBar trackHeight");
|
|
1218
|
+
const m = checkPx(col.markerSize, "percentileBar markerSize");
|
|
1219
|
+
const fontSize = col.fontSize === void 0 ? m / 2 : checkPx(col.fontSize, "percentileBar fontSize");
|
|
1220
|
+
const ringWidth = checkPx(col.ringWidth, "percentileBar ringWidth");
|
|
1221
|
+
const trackColor = escapeAttr(cssValue(col.trackColor, "percentileBar trackColor"));
|
|
1222
|
+
const textColor = escapeAttr(cssValue(col.textColor, "percentileBar textColor"));
|
|
1223
|
+
if (sc === void 0 || v === null) {
|
|
1224
|
+
const seg = `<div style="flex:1;height:${th}px;background:${escapeAttr(cssValue(col.naTrackColor ?? col.trackColor, "percentileBar naTrackColor"))};border-radius:${th}px"></div>`;
|
|
1225
|
+
return `<div class="sdvt-pbar" style="display:flex;align-items:center;height:${m}px;width:100%">${seg}<span style="margin:0 6px;color:${escapeAttr(cssValue(col.naTextColor, "percentileBar naTextColor"))};font-size:${fontSize}px">${escapeHtml(col.naLabel ?? "")}</span>${seg}</div>`;
|
|
1226
|
+
}
|
|
1227
|
+
const [lo, hi] = sc.domain;
|
|
1228
|
+
const clamped = Math.min(hi, Math.max(lo, v));
|
|
1229
|
+
const p = Number((hi === lo ? 50 : (clamped - lo) / (hi - lo) * 100).toFixed(4));
|
|
1230
|
+
const color = sc.color(clamped) ?? trackColor;
|
|
1231
|
+
const track = `<div style="position:absolute;top:50%;left:0;right:0;height:${th}px;margin-top:-${th / 2}px;background:${trackColor};border-radius:${th}px"></div>`;
|
|
1232
|
+
const fill = col.fullTrack ? "" : `<div style="position:absolute;top:50%;left:0;width:${p}%;height:${th}px;margin-top:-${th / 2}px;background:${color};border-radius:${th}px"></div>`;
|
|
1233
|
+
const ring = col.ringColor !== void 0 ? `;box-shadow:0 0 0 ${ringWidth}px ${escapeAttr(cssValue(col.ringColor, "percentileBar ringColor"))}` : "";
|
|
1234
|
+
const marker = `<div style="position:absolute;top:0;left:${p}%;margin-left:-${m / 2}px;width:${m}px;height:${m}px;border-radius:50%;background:${color};color:${textColor};font-size:${fontSize}px;line-height:${m}px;text-align:center;font-weight:700;letter-spacing:-0.02em${ring}">${escapeHtml(formatValue(v, col.decimals, "number", ""))}</div>`;
|
|
1235
|
+
return `<div class="sdvt-pbar" style="position:relative;height:${m}px;width:100%">${track}${fill}${marker}</div>`;
|
|
1236
|
+
}
|
|
1237
|
+
case "indicatorBox": {
|
|
1238
|
+
const on = col.truthy.includes(cellValue(row, col.key));
|
|
1239
|
+
if (col.showOnly === "filled" && !on || col.showOnly === "neutral" && on) return "";
|
|
1240
|
+
return `<span style="${styleAttr({ display: "inline-block", width: `${col.size}px`, height: `${col.size}px`, "background-color": on ? col.fill : col.neutral, "vertical-align": "middle", margin: "4px 1px" })}"></span>`;
|
|
1241
|
+
}
|
|
1242
|
+
case "image": {
|
|
1243
|
+
const v = cellValue(row, col.key);
|
|
1244
|
+
if (isBlank(v)) return "";
|
|
1245
|
+
const alt = col.alt ? cellValue(row, col.alt) : "";
|
|
1246
|
+
return markImg(
|
|
1247
|
+
String(v),
|
|
1248
|
+
isBlank(alt) ? "" : String(alt),
|
|
1249
|
+
checkHeight(Number.parseFloat(col.height), "image")
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
// ---- reactablefmtr-style kinds
|
|
1253
|
+
case "dataBar":
|
|
1254
|
+
return dataBar(col, row, i, ctx);
|
|
1255
|
+
case "iconSet":
|
|
1256
|
+
return iconSet(col, row, i, ctx);
|
|
1257
|
+
case "iconAssign":
|
|
1258
|
+
return iconAssign(col, row, i, ctx);
|
|
1259
|
+
case "mergeStack":
|
|
1260
|
+
return mergeStack(col, row, i, ctx);
|
|
1261
|
+
case "bubble":
|
|
1262
|
+
return bubble(col, row, i, ctx);
|
|
1263
|
+
case "sparkline":
|
|
1264
|
+
case "sparkbar":
|
|
1265
|
+
return seriesCell(col, row, i, ctx);
|
|
1266
|
+
case "gauge":
|
|
1267
|
+
return gauge(col, row, i, ctx);
|
|
1268
|
+
default: {
|
|
1269
|
+
const bad = col;
|
|
1270
|
+
throw new TableSpecError(`unknown column kind ${String(bad.kind)}`);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
function kindCellStyle(col, row, i, ctx) {
|
|
1275
|
+
switch (col.kind) {
|
|
1276
|
+
case "teamColorBar":
|
|
1277
|
+
case "teamColorBg": {
|
|
1278
|
+
const id = ctx.teamIds.get(col.key)?.[i];
|
|
1279
|
+
const color = (id === void 0 ? void 0 : teamColorsSync(col.league, id, { which: col.which, idSystem: "team_id" })) ?? col.naColor;
|
|
1280
|
+
if (col.kind === "teamColorBar")
|
|
1281
|
+
return styleAttr({ "border-left": `${col.barWidth}px solid ${color}` });
|
|
1282
|
+
const a = Math.round(Math.min(1, Math.max(0, col.alpha)) * 255).toString(16).padStart(2, "0");
|
|
1283
|
+
return styleAttr({ "background-color": `${hex6(color, { dropAlpha: true })}${a}` });
|
|
1284
|
+
}
|
|
1285
|
+
case "colorRanks": {
|
|
1286
|
+
const sc = ctx.scales.get(col.key);
|
|
1287
|
+
const s = sc?.values[i] ?? null;
|
|
1288
|
+
const ref = s === null ? void 0 : refColor(col, row, i, ctx);
|
|
1289
|
+
const fill = ref ?? (s === null ? null : sc?.color(s)) ?? "#ffffff";
|
|
1290
|
+
return styleAttr({
|
|
1291
|
+
"background-color": fill,
|
|
1292
|
+
color: col.showText === false ? "transparent" : onColor(solid(fill, tableBg(ctx)))
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
case "highlight": {
|
|
1296
|
+
const hit = extremeOf(col, row, ctx);
|
|
1297
|
+
return hit ? styleAttr({
|
|
1298
|
+
"background-color": col.fill,
|
|
1299
|
+
color: col.textColor ?? (col.extreme === void 0 ? void 0 : hit === "min" ? "red" : "green"),
|
|
1300
|
+
"font-weight": col.bold ? "bold" : void 0
|
|
1301
|
+
}) : "";
|
|
1302
|
+
}
|
|
1303
|
+
case "highlightNa":
|
|
1304
|
+
return isNa(col, cellValue(row, col.key)) ? styleAttr({
|
|
1305
|
+
"background-color": col.fill,
|
|
1306
|
+
color: col.textColor,
|
|
1307
|
+
"font-weight": col.bold ? "bold" : void 0,
|
|
1308
|
+
"font-style": col.italic ? "italic" : void 0
|
|
1309
|
+
}) : "";
|
|
1310
|
+
default:
|
|
1311
|
+
return "";
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/html/legend-icon.ts
|
|
1316
|
+
var LEGEND_ICON_SETS = {
|
|
1317
|
+
"ski rating": [
|
|
1318
|
+
["circle", "#39b54a", "Easy"],
|
|
1319
|
+
["square", "#0f75bc", "Moderate"],
|
|
1320
|
+
["diamond", "#000000", "Difficult"],
|
|
1321
|
+
// Font Awesome "square" turned 45 degrees
|
|
1322
|
+
["double-diamond", "#000000", "Most Difficult"]
|
|
1323
|
+
],
|
|
1324
|
+
medals: [
|
|
1325
|
+
["medal", "#A77044", "Bronze"],
|
|
1326
|
+
["medal", "#D7D7D7", "Silver"],
|
|
1327
|
+
["medal", "#D6AF36", "Gold"]
|
|
1328
|
+
],
|
|
1329
|
+
batteries: [
|
|
1330
|
+
["battery-quarter", "#d7191c", "0-25%"],
|
|
1331
|
+
["battery-half", "#fdae61", "25-50%"],
|
|
1332
|
+
["battery-three-quarters", "#a6d96a", "50-75%"],
|
|
1333
|
+
["battery-full", "#1a9641", "75-100%"]
|
|
1334
|
+
]
|
|
1335
|
+
};
|
|
1336
|
+
var battery = (level) => `<rect x="0.75" y="4.25" width="12.5" height="7.5" rx="1" fill="none" stroke="currentColor" stroke-width="1.5"/><rect x="13.75" y="6.5" width="1.75" height="3" fill="currentColor"/><rect x="2.5" y="6" width="${9 * level}" height="4" fill="currentColor"/>`;
|
|
1337
|
+
var SHAPES = {
|
|
1338
|
+
circle: [16, `<circle cx="8" cy="8" r="7" fill="currentColor"/>`],
|
|
1339
|
+
square: [16, `<rect x="1" y="1" width="14" height="14" fill="currentColor"/>`],
|
|
1340
|
+
diamond: [16, `<path d="M8 .5 15.5 8 8 15.5 .5 8Z" fill="currentColor"/>`],
|
|
1341
|
+
"double-diamond": [
|
|
1342
|
+
30,
|
|
1343
|
+
`<path d="M8 .5 15.5 8 8 15.5 .5 8ZM22 .5 29.5 8 22 15.5 14.5 8Z" fill="currentColor"/>`
|
|
1344
|
+
],
|
|
1345
|
+
"triangle-up": [16, `<path d="M8 1.5 15 14.5H1Z" fill="currentColor"/>`],
|
|
1346
|
+
"triangle-down": [16, `<path d="M1 1.5H15L8 14.5Z" fill="currentColor"/>`],
|
|
1347
|
+
medal: [
|
|
1348
|
+
16,
|
|
1349
|
+
`<path d="M3 0h3.5l2 5.5H5ZM13 0H9.5l-2 5.5H11Z" fill="currentColor"/><circle cx="8" cy="10.5" r="5" fill="currentColor"/>`
|
|
1350
|
+
],
|
|
1351
|
+
"battery-quarter": [16, battery(0.25)],
|
|
1352
|
+
"battery-half": [16, battery(0.5)],
|
|
1353
|
+
"battery-three-quarters": [16, battery(0.75)],
|
|
1354
|
+
"battery-full": [16, battery(1)]
|
|
1355
|
+
};
|
|
1356
|
+
function legendIconSvg(shape, color, size) {
|
|
1357
|
+
const s = Object.hasOwn(SHAPES, shape) ? SHAPES[shape] : void 0;
|
|
1358
|
+
if (!s)
|
|
1359
|
+
throw new TableSpecError(
|
|
1360
|
+
`legendIcon icon must be one of ${Object.keys(SHAPES).join(", ")}, not "${shape}"`
|
|
1361
|
+
);
|
|
1362
|
+
const [w, body] = s;
|
|
1363
|
+
return `<svg class="sdvt-legend-icon" viewBox="0 0 ${w} 16" width="${size * w / 16}" height="${size}" aria-hidden="true" focusable="false" style="color:${escapeAttr(cssValue(color, "legendIcon colors"))};vertical-align:middle">${body}</svg>`;
|
|
1364
|
+
}
|
|
1365
|
+
function legendIconBlock(d) {
|
|
1366
|
+
if (d.align !== "left" && d.align !== "right")
|
|
1367
|
+
throw new TableSpecError(`legendIcon align must be left or right, not "${String(d.align)}"`);
|
|
1368
|
+
if (d.location !== "top" && d.location !== "bottom")
|
|
1369
|
+
throw new TableSpecError(`legendIcon location must be top or bottom, not "${String(d.location)}"`);
|
|
1370
|
+
const size = checkPx(d.iconSize, "legendIcon iconSize");
|
|
1371
|
+
if (size <= 0) throw new TableSpecError(`legendIcon iconSize must be positive, got ${size}`);
|
|
1372
|
+
let items;
|
|
1373
|
+
if (d.iconSet !== void 0 && d.icons !== void 0)
|
|
1374
|
+
throw new TableSpecError("legendIcon takes an iconSet or icons, not both");
|
|
1375
|
+
if (d.iconSet !== void 0) {
|
|
1376
|
+
const set = Object.hasOwn(LEGEND_ICON_SETS, d.iconSet) ? LEGEND_ICON_SETS[d.iconSet] : void 0;
|
|
1377
|
+
if (!set)
|
|
1378
|
+
throw new TableSpecError(
|
|
1379
|
+
`legendIcon iconSet must be "ski rating", "medals" or "batteries", not "${d.iconSet}"`
|
|
1380
|
+
);
|
|
1381
|
+
if (d.labels && d.labels.length !== set.length)
|
|
1382
|
+
throw new TableSpecError(
|
|
1383
|
+
`legendIcon iconSet "${d.iconSet}" needs ${set.length} labels, got ${d.labels.length}`
|
|
1384
|
+
);
|
|
1385
|
+
items = set.map(([shape, color, label], i) => [shape, color, d.labels?.[i] ?? label]);
|
|
1386
|
+
} else {
|
|
1387
|
+
if (!d.icons || d.icons.length === 0) throw new TableSpecError("legendIcon needs an iconSet or icons");
|
|
1388
|
+
const colors = d.colors && d.colors.length > 0 ? d.colors : ["currentColor"];
|
|
1389
|
+
if (d.labels && d.labels.length !== d.icons.length)
|
|
1390
|
+
throw new TableSpecError(
|
|
1391
|
+
`legendIcon needs one label per icon: ${d.icons.length} icons, ${d.labels.length} labels`
|
|
1392
|
+
);
|
|
1393
|
+
if (!d.labels && d.showLabels)
|
|
1394
|
+
throw new TableSpecError("legendIcon icons need labels when showLabels is true");
|
|
1395
|
+
items = d.icons.map((shape, i) => [
|
|
1396
|
+
shape,
|
|
1397
|
+
colors[i % colors.length],
|
|
1398
|
+
d.labels?.[i] ?? shape.replace(/-/g, " ")
|
|
1399
|
+
]);
|
|
1400
|
+
}
|
|
1401
|
+
const icons = items.map(
|
|
1402
|
+
([shape, color, label]) => d.showLabels ? `<span class="sdvt-legend-item" style="display:inline-flex;align-items:center;gap:4px">${legendIconSvg(shape, color, size)}${escapeHtml(label)}</span>` : `<span class="sdvt-legend-item" role="img" aria-label="${escapeAttr(label)}">${legendIconSvg(shape, color, size)}</span>`
|
|
1403
|
+
).join("");
|
|
1404
|
+
const justify = d.align === "left" ? "flex-start" : "flex-end";
|
|
1405
|
+
return `<div class="sdvt-legend sdvt-legend-icon sdvt-legend-${d.location}" style="text-align:${d.align}">${d.title ? `<div class="sdvt-legend-title" style="margin-bottom:8px;font-weight:bold;font-size:90%">${escapeHtml(d.title)}</div>` : ""}<div style="display:flex;flex-wrap:wrap;justify-content:${justify};align-items:center;gap:4px 10px;font-size:13px;word-spacing:1px">${icons}</div>${d.footer ? `<div class="sdvt-legend-sub" style="font-size:70%;color:var(--sdvt-muted)">${escapeHtml(d.footer)}</div>` : ""}</div>`;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/html/social-icons.ts
|
|
1409
|
+
var X = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/></svg>';
|
|
1410
|
+
var IG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z"/></svg>';
|
|
1411
|
+
var BSKY = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M111.8 62.2C170.2 105.9 233 194.7 256 242.4c23-47.6 85.8-136.4 144.2-180.2c42.1-31.6 110.3-56 110.3 21.8c0 15.5-8.9 130.5-14.1 149.2C478.2 298 412 314.6 353.1 304.5c102.9 17.5 129.1 75.5 72.5 133.5c-107.4 110.2-154.3-27.6-166.3-62.9l0 0c-1.7-4.9-2.6-7.8-3.3-7.8s-1.6 3-3.3 7.8l0 0c-12 35.3-59 173.1-166.3 62.9c-56.5-58-30.4-116 72.5-133.5C100 314.6 33.8 298 15.7 233.1C10.4 214.4 1.5 99.4 1.5 83.9c0-77.8 68.2-53.4 110.3-21.8z"/></svg>';
|
|
1412
|
+
var GH = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/></svg>';
|
|
1413
|
+
var YT = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z"/></svg>';
|
|
1414
|
+
var FB = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M504 256C504 119 393 8 256 8S8 119 8 256c0 123.78 90.69 226.38 209.25 245V327.69h-63V256h63v-54.64c0-62.15 37-96.48 93.67-96.48 27.14 0 55.52 4.84 55.52 4.84v61h-31.28c-30.8 0-40.41 19.12-40.41 38.73V256h68.78l-11 71.69h-57.78V501C413.31 482.38 504 379.78 504 256z"/></svg>';
|
|
1415
|
+
var WEB = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M352 256c0 22.2-1.2 43.6-3.3 64H163.3c-2.2-20.4-3.3-41.8-3.3-64s1.2-43.6 3.3-64H348.7c2.2 20.4 3.3 41.8 3.3 64zm28.8-64H503.9c5.3 20.5 8.1 41.9 8.1 64s-2.8 43.5-8.1 64H380.8c2.1-20.6 3.2-42 3.2-64s-1.1-43.4-3.2-64zm112.6-32H376.7c-10-63.9-29.8-117.4-55.3-151.6c78.3 20.7 142 77.5 171.9 151.6zm-149.1 0H167.7c6.1-36.4 15.5-68.6 27-94.7c10.5-23.6 22.2-40.7 33.5-51.5C239.4 3.2 248.7 0 256 0s16.6 3.2 27.8 13.8c11.3 10.8 23 27.9 33.5 51.5c11.6 26 21 58.2 27 94.7zm-209 0H18.6C48.6 85.9 112.2 29.1 190.6 8.4C165.1 42.6 145.3 96.1 135.3 160zM8.1 192H131.2c-2.1 20.6-3.2 42-3.2 64s1.1 43.4 3.2 64H8.1C2.8 299.5 0 278.1 0 256s2.8-43.5 8.1-64zM194.7 446.6c-11.6-26-20.9-58.2-27-94.6H344.3c-6.1 36.4-15.5 68.6-27 94.6c-10.5 23.6-22.2 40.7-33.5 51.5C272.6 508.8 263.3 512 256 512s-16.6-3.2-27.8-13.8c-11.3-10.8-23-27.9-33.5-51.5zM135.3 352c10 63.9 29.8 117.4 55.3 151.6C112.2 482.9 48.6 426.1 18.6 352H135.3zm358.1 0c-30 74.1-93.6 130.9-171.9 151.6c25.5-34.2 45.2-87.7 55.3-151.6H493.4z"/></svg>';
|
|
1416
|
+
var EMAIL = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"/></svg>';
|
|
1417
|
+
var SOCIAL_ICONS = {
|
|
1418
|
+
x: X,
|
|
1419
|
+
twitter: X,
|
|
1420
|
+
ig: IG,
|
|
1421
|
+
bsky: BSKY,
|
|
1422
|
+
gh: GH,
|
|
1423
|
+
yt: YT,
|
|
1424
|
+
fb: FB,
|
|
1425
|
+
web: WEB,
|
|
1426
|
+
website: WEB,
|
|
1427
|
+
link: WEB,
|
|
1428
|
+
email: EMAIL,
|
|
1429
|
+
mail: EMAIL
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
// src/html/decorations.ts
|
|
1433
|
+
function strwrap(words, width) {
|
|
1434
|
+
const w = Math.max(width - 1, 1);
|
|
1435
|
+
const out = [];
|
|
1436
|
+
let cur = "";
|
|
1437
|
+
for (const word of words) {
|
|
1438
|
+
if (cur && cur.length + 1 + word.length > w) {
|
|
1439
|
+
out.push(cur);
|
|
1440
|
+
cur = word;
|
|
1441
|
+
} else cur = cur ? `${cur} ${word}` : word;
|
|
1442
|
+
}
|
|
1443
|
+
return cur ? [...out, cur] : out;
|
|
1444
|
+
}
|
|
1445
|
+
function wrapLabel(text, width, balance) {
|
|
1446
|
+
const words = text.split(/\s+/).filter((x) => x !== "");
|
|
1447
|
+
if (words.length <= 1) return [text];
|
|
1448
|
+
const greedy = strwrap(words, width);
|
|
1449
|
+
if (greedy.length <= 1) return [text];
|
|
1450
|
+
if (!balance) return greedy;
|
|
1451
|
+
const target = Math.ceil(words.reduce((s, x) => s + x.length + 1, 0) / greedy.length);
|
|
1452
|
+
const lines = [];
|
|
1453
|
+
let cur = "";
|
|
1454
|
+
for (const word of words) {
|
|
1455
|
+
const cand = cur ? `${cur} ${word}` : word;
|
|
1456
|
+
if (cand.length > target && cur) {
|
|
1457
|
+
lines.push(cur);
|
|
1458
|
+
cur = word;
|
|
1459
|
+
} else cur = cand;
|
|
1460
|
+
}
|
|
1461
|
+
return cur ? [...lines, cur] : lines;
|
|
1462
|
+
}
|
|
1463
|
+
function groupIndexOf(rows, key) {
|
|
1464
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1465
|
+
return rows.map((r) => {
|
|
1466
|
+
const g = cellValue(r, key);
|
|
1467
|
+
if (!seen.has(g)) seen.set(g, seen.size);
|
|
1468
|
+
return seen.get(g);
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
var isList = (p) => Array.isArray(p);
|
|
1472
|
+
function textStyleAttr(ts) {
|
|
1473
|
+
if (!ts) return "";
|
|
1474
|
+
const s = styleAttr({
|
|
1475
|
+
color: ts.color,
|
|
1476
|
+
"font-size": ts.size,
|
|
1477
|
+
"font-weight": ts.weight,
|
|
1478
|
+
"font-family": ts.font ? fontStack(checkFamily(ts.font)) : void 0,
|
|
1479
|
+
"text-transform": ts.transform === "uppercase" ? "uppercase" : void 0,
|
|
1480
|
+
"font-style": ts.style === "italic" ? "italic" : void 0
|
|
1481
|
+
});
|
|
1482
|
+
return s ? ` style="${s}"` : "";
|
|
1483
|
+
}
|
|
1484
|
+
function checkFamily(f) {
|
|
1485
|
+
if (!/^[\w .-]+$/.test(f))
|
|
1486
|
+
throw new TableSpecError(`font family "${f}" may only hold letters, digits, spaces, "." "-" "_"`);
|
|
1487
|
+
return f;
|
|
1488
|
+
}
|
|
1489
|
+
var checkLocation = (l, arg) => {
|
|
1490
|
+
if (l !== "top" && l !== "bottom")
|
|
1491
|
+
throw new TableSpecError(`${arg} location must be top or bottom, not ${JSON.stringify(l)}`);
|
|
1492
|
+
return l;
|
|
1493
|
+
};
|
|
1494
|
+
var ALIGN = /* @__PURE__ */ new Set(["left", "center", "right"]);
|
|
1495
|
+
var checkAlign = (a, arg) => {
|
|
1496
|
+
if (!ALIGN.has(a)) throw new TableSpecError(`${arg} must be left, center or right, not "${a}"`);
|
|
1497
|
+
return a;
|
|
1498
|
+
};
|
|
1499
|
+
var SCALE_NAMES = {
|
|
1500
|
+
1e3: ["thousands", "(000s)"],
|
|
1501
|
+
1e6: ["millions", "(millions)"],
|
|
1502
|
+
1e9: ["billions", "(billions)"],
|
|
1503
|
+
1e12: ["trillions", "(trillions)"]
|
|
1504
|
+
};
|
|
1505
|
+
function scaleNoteOf(d) {
|
|
1506
|
+
if (!(Number.isFinite(d.divisor) && d.divisor !== 0))
|
|
1507
|
+
throw new TableSpecError(`scaleNote divisor must be a single non-zero number, got ${d.divisor}`);
|
|
1508
|
+
if (d.columns.length === 0) throw new TableSpecError("scaleNote columns matched no columns");
|
|
1509
|
+
const named = SCALE_NAMES[d.divisor];
|
|
1510
|
+
const shown = Number.isInteger(d.divisor) ? formatNumber(d.divisor, { digits: 0, big: true }) : naturalDigits(d.divisor, true);
|
|
1511
|
+
return {
|
|
1512
|
+
note: d.note ?? (named ? `Figures in ${named[0]}.` : `Figures divided by ${shown}.`),
|
|
1513
|
+
suffix: d.labelSuffix ?? (named ? named[1] : `(\xF7${shown})`)
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
function applyDecorations(spec, rows, ctx) {
|
|
1517
|
+
const none = () => "";
|
|
1518
|
+
const out = {
|
|
1519
|
+
caption: "",
|
|
1520
|
+
headRows: "",
|
|
1521
|
+
foot: [],
|
|
1522
|
+
before: "",
|
|
1523
|
+
after: "",
|
|
1524
|
+
css: [],
|
|
1525
|
+
fonts: [],
|
|
1526
|
+
hiddenColumns: /* @__PURE__ */ new Set(),
|
|
1527
|
+
labelText: /* @__PURE__ */ new Map(),
|
|
1528
|
+
label: (_c, t) => escapeHtml(t),
|
|
1529
|
+
labelSuffix: none,
|
|
1530
|
+
labelStyle: none,
|
|
1531
|
+
rowClass: none,
|
|
1532
|
+
rowStyle: none,
|
|
1533
|
+
groupRowClass: none,
|
|
1534
|
+
cellStyle: none,
|
|
1535
|
+
cellSuffix: none
|
|
1536
|
+
};
|
|
1537
|
+
const title = spec.decorations.find(
|
|
1538
|
+
(d) => d.type === "title"
|
|
1539
|
+
);
|
|
1540
|
+
const subtitle = spec.decorations.find(
|
|
1541
|
+
(d) => d.type === "subtitle"
|
|
1542
|
+
);
|
|
1543
|
+
if (title || subtitle)
|
|
1544
|
+
out.caption = `${title ? `<span class="sdvt-title">${escapeHtml(title.text)}</span>` : ""}${subtitle ? `<span class="sdvt-subtitle">${escapeHtml(subtitle.text)}</span>` : ""}`;
|
|
1545
|
+
const rowStyles = /* @__PURE__ */ new Map();
|
|
1546
|
+
const rowClasses = /* @__PURE__ */ new Map();
|
|
1547
|
+
const cellStyles = /* @__PURE__ */ new Map();
|
|
1548
|
+
const addRow = (i, decl) => {
|
|
1549
|
+
rowStyles.set(i, { ...rowStyles.get(i), ...decl });
|
|
1550
|
+
};
|
|
1551
|
+
const addClass = (i, c) => {
|
|
1552
|
+
rowClasses.set(i, [...rowClasses.get(i) ?? [], c]);
|
|
1553
|
+
};
|
|
1554
|
+
const addCell = (i, key, decl) => {
|
|
1555
|
+
cellStyles.set(`${i}:${key}`, { ...cellStyles.get(`${i}:${key}`), ...decl });
|
|
1556
|
+
};
|
|
1557
|
+
const fillCells = (i, decl) => {
|
|
1558
|
+
for (const c of ctx.columns) addCell(i, c.key, decl);
|
|
1559
|
+
};
|
|
1560
|
+
const suffixes = /* @__PURE__ */ new Map();
|
|
1561
|
+
const labelStyles = /* @__PURE__ */ new Map();
|
|
1562
|
+
let recordedKey;
|
|
1563
|
+
let cutCount = 0;
|
|
1564
|
+
const bg = ctx.theme.tokens.bg === "transparent" ? "#ffffff" : hex6(ctx.theme.tokens.bg);
|
|
1565
|
+
const matchesNone = (sel) => selectRows(sel, ctx.domainRows).length === 0;
|
|
1566
|
+
for (const c of spec.columns)
|
|
1567
|
+
if (c.kind === "colorResults") {
|
|
1568
|
+
const [win, loss] = c.resultType === "binary" ? [1, 0] : ["W", "L"];
|
|
1569
|
+
const eq = (v, want) => v === want || typeof v === "boolean" && Number(v) === want;
|
|
1570
|
+
rows.forEach((r, i) => {
|
|
1571
|
+
const v = cellValue(r, c.key);
|
|
1572
|
+
if (isBlank(v)) return;
|
|
1573
|
+
if (eq(v, win)) fillCells(i, { "background-color": c.winColor, color: c.winTextColor });
|
|
1574
|
+
else if (eq(v, loss)) fillCells(i, { "background-color": c.lossColor, color: c.lossTextColor });
|
|
1575
|
+
else if (c.tieColor !== void 0 && eq(v, c.tieValue))
|
|
1576
|
+
fillCells(i, { "background-color": c.tieColor, color: c.tieTextColor });
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
for (const d of spec.decorations)
|
|
1580
|
+
switch (d.type) {
|
|
1581
|
+
case "sourceNote":
|
|
1582
|
+
out.foot.push(d.unsafe ? d.html : escapeHtml(d.html));
|
|
1583
|
+
break;
|
|
1584
|
+
case "wrapLabels": {
|
|
1585
|
+
const prev = out.label;
|
|
1586
|
+
out.label = (col, text) => !d.columns || d.columns.includes(col.key) ? wrapLabel(text, d.width, d.balance).map(escapeHtml).join("<br>") : prev(col, text);
|
|
1587
|
+
break;
|
|
1588
|
+
}
|
|
1589
|
+
// ---- Task 9
|
|
1590
|
+
case "groupStripes": {
|
|
1591
|
+
if (!ctx.groupKey) throw new TableSpecError("groupStripes needs .groupBy(key)");
|
|
1592
|
+
const on = (gi) => gi % 2 === (d.start === 2 ? 1 : 0);
|
|
1593
|
+
groupIndexOf(rows, ctx.groupKey).forEach((g, i) => {
|
|
1594
|
+
if (on(g)) addClass(i, "sdvt-gstripe");
|
|
1595
|
+
});
|
|
1596
|
+
out.css.push(
|
|
1597
|
+
`${ctx.sel} tr.sdvt-gstripe td{background-color:${cssValue(d.color, "groupStripes color")}}`
|
|
1598
|
+
);
|
|
1599
|
+
break;
|
|
1600
|
+
}
|
|
1601
|
+
case "rowAccent": {
|
|
1602
|
+
if (d.side !== "left" && d.side !== "right")
|
|
1603
|
+
throw new TableSpecError(`rowAccent side must be left or right, not ${JSON.stringify(d.side)}`);
|
|
1604
|
+
const keyOf = (r) => {
|
|
1605
|
+
const v = cellValue(r, d.key);
|
|
1606
|
+
return isBlank(v) ? null : String(v);
|
|
1607
|
+
};
|
|
1608
|
+
const keys = rows.map(keyOf);
|
|
1609
|
+
const levels = [...new Set(ctx.domainRows.map(keyOf).filter((k) => k !== null))].sort();
|
|
1610
|
+
const pal = d.palette;
|
|
1611
|
+
const colors = keys.map(
|
|
1612
|
+
(k) => k === null ? void 0 : pal === void 0 ? k : isList(pal) ? pal[levels.indexOf(k) % pal.length] : Object.hasOwn(pal, k) ? pal[k] : void 0
|
|
1613
|
+
);
|
|
1614
|
+
const keep = d.rows ? new Set(selectRows(d.rows, rows, ctx.domainRows)) : null;
|
|
1615
|
+
if (d.rows && keep?.size === 0 && matchesNone(d.rows)) {
|
|
1616
|
+
ctx.warn(`sdvtables:rowAccent:${ctx.id}`, "rows matched no rows; the table is unchanged");
|
|
1617
|
+
break;
|
|
1618
|
+
}
|
|
1619
|
+
const unsafe = /* @__PURE__ */ new Set();
|
|
1620
|
+
colors.forEach((c, i) => {
|
|
1621
|
+
const fill = c ?? d.naColor;
|
|
1622
|
+
if (fill === "transparent" || keep && !keep.has(i)) return;
|
|
1623
|
+
if (pal === void 0 && c !== void 0 && !isCssValue(c)) unsafe.add(c);
|
|
1624
|
+
else addRow(i, { [`border-${d.side}`]: `${d.width}px solid ${fill}` });
|
|
1625
|
+
});
|
|
1626
|
+
if (unsafe.size > 0)
|
|
1627
|
+
ctx.warn(
|
|
1628
|
+
`sdvtables:rowAccent:${ctx.id}:css:${[...unsafe].join(",")}`,
|
|
1629
|
+
`${[...unsafe].map((c) => JSON.stringify(c)).join(", ")} in "${d.key}" cannot be a CSS color, so ${unsafe.size === 1 ? "that row gets" : "those rows get"} no accent`
|
|
1630
|
+
);
|
|
1631
|
+
if (d.hide) out.hiddenColumns.add(d.key);
|
|
1632
|
+
break;
|
|
1633
|
+
}
|
|
1634
|
+
case "boldRows":
|
|
1635
|
+
for (const i of selectRows(d.rows, rows, ctx.domainRows)) {
|
|
1636
|
+
addRow(i, { "font-weight": "bold" });
|
|
1637
|
+
fillCells(i, {
|
|
1638
|
+
color: d.textColor,
|
|
1639
|
+
...d.highlightColor ? { "background-color": d.highlightColor } : {}
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
break;
|
|
1643
|
+
case "spotlight": {
|
|
1644
|
+
const lit = new Set(selectRows(d.rows, rows, ctx.domainRows));
|
|
1645
|
+
if (lit.size === 0 && matchesNone(d.rows)) {
|
|
1646
|
+
ctx.warn(`sdvtables:spotlight:${ctx.id}`, "rows matched no rows, so the table is unchanged");
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
1649
|
+
const dim = d.dimColor === "auto" ? secondaryOn(bg, onColor(bg)) : d.dimColor;
|
|
1650
|
+
const fill = {
|
|
1651
|
+
...d.fill ? { "background-color": d.fill } : {},
|
|
1652
|
+
...d.textColor ? { color: d.textColor } : {}
|
|
1653
|
+
};
|
|
1654
|
+
const look = { ...fill, ...d.bold ? { "font-weight": "bold" } : {} };
|
|
1655
|
+
rows.forEach((_, i) => {
|
|
1656
|
+
if (!lit.has(i)) {
|
|
1657
|
+
if (dim) addRow(i, { color: dim });
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
if (d.accentColor) addRow(i, { "box-shadow": `inset ${d.accentWidth}px 0 0 ${d.accentColor}` });
|
|
1661
|
+
if (!d.columns) {
|
|
1662
|
+
fillCells(i, fill);
|
|
1663
|
+
if (d.bold) addRow(i, { "font-weight": "bold" });
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
for (const c of ctx.columns)
|
|
1667
|
+
if (d.columns.includes(c.key)) addCell(i, c.key, look);
|
|
1668
|
+
else if (dim) addCell(i, c.key, { color: dim });
|
|
1669
|
+
});
|
|
1670
|
+
break;
|
|
1671
|
+
}
|
|
1672
|
+
case "cutline": {
|
|
1673
|
+
if (!d.after.every((a) => Number.isInteger(a)))
|
|
1674
|
+
throw new TableSpecError(`cutline after must be whole row numbers, got ${d.after.join(", ")}`);
|
|
1675
|
+
if (d.gap.length < 1 || d.gap.length > 2 || d.gap.some((g) => !(g >= 0)))
|
|
1676
|
+
throw new TableSpecError("cutline gap must be one or two non-negative numbers");
|
|
1677
|
+
const n = rows.length;
|
|
1678
|
+
const dropped = d.after.filter((a) => a < 0 || a >= n);
|
|
1679
|
+
if (dropped.length > 0)
|
|
1680
|
+
ctx.warn(
|
|
1681
|
+
`sdvtables:cutline:${ctx.id}:${dropped.join(",")}`,
|
|
1682
|
+
`dropped ${dropped.length} cut line(s) at ${dropped.join(", ")}: after must be between 0 and ${n - 1}; a line after the last row is just the table border`
|
|
1683
|
+
);
|
|
1684
|
+
const color = cssValue(d.color, "cutline color");
|
|
1685
|
+
const style = cssValue(d.style, "cutline style");
|
|
1686
|
+
const weight = checkPx(d.weight, "cutline weight");
|
|
1687
|
+
const labelSize = checkPx(d.labelSize, "cutline labelSize");
|
|
1688
|
+
const cn = cutCount++;
|
|
1689
|
+
const above = d.gap[0] ?? 0;
|
|
1690
|
+
const below = d.gap[d.gap.length - 1] ?? 0;
|
|
1691
|
+
d.after.forEach((a, j) => {
|
|
1692
|
+
if (a < 0 || a >= n) return;
|
|
1693
|
+
const label = d.label && d.label.length > 0 ? d.label[j % d.label.length] ?? null : null;
|
|
1694
|
+
const top = d.labelPosition === "below" || a === 0;
|
|
1695
|
+
const labelRow = top ? a : a - 1;
|
|
1696
|
+
addClass(a, `sdvt-cut-${cn}-${j}`);
|
|
1697
|
+
out.css.push(`${ctx.sel} tr.sdvt-cut-${cn}-${j} td{border-top:${weight}px ${style} ${color}}`);
|
|
1698
|
+
if (above > 0 && a > 0 && !(label && labelRow === a - 1)) {
|
|
1699
|
+
addClass(a - 1, `sdvt-cut-${cn}-${j}-above`);
|
|
1700
|
+
out.css.push(`${ctx.sel} tr.sdvt-cut-${cn}-${j}-above td{padding-bottom:${above}px}`);
|
|
1701
|
+
}
|
|
1702
|
+
if (below > 0 && !(label && labelRow === a))
|
|
1703
|
+
out.css.push(`${ctx.sel} tr.sdvt-cut-${cn}-${j} td{padding-top:${below}px}`);
|
|
1704
|
+
if (label) {
|
|
1705
|
+
addClass(labelRow, `sdvt-cut-${cn}-${j}-label`);
|
|
1706
|
+
out.css.push(
|
|
1707
|
+
`${ctx.sel} tr.sdvt-cut-${cn}-${j}-label td{padding-${top ? "top" : "bottom"}:${labelSize + 13 + (top ? below : above)}px;background-color:transparent}`,
|
|
1708
|
+
`${ctx.sel} tr.sdvt-cut-${cn}-${j}-label{background-image:url("${cutlineSvg(label, d.labelColor ?? color, labelSize)}");background-repeat:no-repeat;background-position:${top ? "left 5px" : "left bottom 5px"}}`
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
break;
|
|
1713
|
+
}
|
|
1714
|
+
case "borderGrid": {
|
|
1715
|
+
const color = cssValue(d.color, "borderGrid color");
|
|
1716
|
+
const b = `border-right:${checkPx(d.weight, "borderGrid weight")}px solid ${color}`;
|
|
1717
|
+
out.css.push(
|
|
1718
|
+
`${ctx.sel} td.sdvt-cell:not(:last-child){${b}}`,
|
|
1719
|
+
`${ctx.sel} td.sdvt-cell{border-top-color:${color}}`
|
|
1720
|
+
);
|
|
1721
|
+
if (d.includeLabels) out.css.push(`${ctx.sel} th.sdvt-label:not(:last-child){${b}}`);
|
|
1722
|
+
break;
|
|
1723
|
+
}
|
|
1724
|
+
// ---- Task 10
|
|
1725
|
+
case "titleHeader": {
|
|
1726
|
+
for (const ts of [d.kickerStyle, d.titleStyle, d.subtitleStyle, d.dateStyle])
|
|
1727
|
+
if (ts?.font)
|
|
1728
|
+
out.fonts.push({
|
|
1729
|
+
family: checkFamily(ts.font),
|
|
1730
|
+
weights: [typeof ts.weight === "number" ? ts.weight : 400]
|
|
1731
|
+
});
|
|
1732
|
+
out.css.push(
|
|
1733
|
+
`${ctx.sel} .sdvt-kicker{font-size:0.75em;font-weight:700;color:#C84630;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:0.15em}`,
|
|
1734
|
+
`${ctx.sel} .sdvt-date{font-size:0.85em;font-weight:400;color:#8A8A8A;margin-top:0.15em}`
|
|
1735
|
+
);
|
|
1736
|
+
out.caption = `${d.kicker ? `<div class="sdvt-kicker"${textStyleAttr(d.kickerStyle)}>${escapeHtml(d.kicker)}</div>` : ""}<span class="sdvt-title"${textStyleAttr(d.titleStyle)}>${escapeHtml(d.title)}</span>${d.subtitle ? `<span class="sdvt-subtitle"${textStyleAttr(d.subtitleStyle)}>${escapeHtml(d.subtitle)}</span>` : ""}${d.date ? `<div class="sdvt-date"${textStyleAttr(d.dateStyle)}>${escapeHtml(d.date)}</div>` : ""}`;
|
|
1737
|
+
break;
|
|
1738
|
+
}
|
|
1739
|
+
case "caption538": {
|
|
1740
|
+
const align = checkAlign(d.align, "caption538 align");
|
|
1741
|
+
if (!d.top && !d.bottom)
|
|
1742
|
+
throw new TableSpecError("caption538: nothing to caption; pass top, bottom, or both");
|
|
1743
|
+
const rule = d.ruleColor ?? ctx.theme.tokens.text;
|
|
1744
|
+
if (d.top)
|
|
1745
|
+
out.foot.push(
|
|
1746
|
+
`<div class="sdvt-cap538" style="${styleAttr({ "border-bottom": `${d.ruleWidth}px solid ${rule}`, "font-size": `${d.size}px` })}">${escapeHtml(d.top)}</div>`
|
|
1747
|
+
);
|
|
1748
|
+
if (d.bottom)
|
|
1749
|
+
out.foot.push(
|
|
1750
|
+
`<div class="sdvt-cap538" style="${styleAttr({ "text-align": align })}">${escapeHtml(d.bottom)}</div>`
|
|
1751
|
+
);
|
|
1752
|
+
break;
|
|
1753
|
+
}
|
|
1754
|
+
case "socialTag": {
|
|
1755
|
+
const align = checkAlign(d.align, "socialTag align");
|
|
1756
|
+
const entries = Object.entries(d.accounts);
|
|
1757
|
+
if (entries.length === 0)
|
|
1758
|
+
throw new TableSpecError("socialTag accounts must be a non-empty {platform: handle} object");
|
|
1759
|
+
const parts = entries.map(([k, handle]) => {
|
|
1760
|
+
const svg = SOCIAL_ICONS[k.toLowerCase()];
|
|
1761
|
+
if (!svg)
|
|
1762
|
+
throw new TableSpecError(
|
|
1763
|
+
`socialTag: unknown platform "${k}"; one of ${Object.keys(SOCIAL_ICONS).join(", ")}`
|
|
1764
|
+
);
|
|
1765
|
+
return `<span class="sdvt-handle">${svg.replace("<svg ", `<svg style="${styleAttr({ height: d.iconHeight, "vertical-align": "-0.125em", fill: d.iconColor ?? "currentColor" })}" `)}${escapeHtml(handle)}</span>`;
|
|
1766
|
+
});
|
|
1767
|
+
out.css.push(
|
|
1768
|
+
`${ctx.sel} .sdvt-handle{display:inline-flex;align-items:center;gap:0.3em;white-space:nowrap}`
|
|
1769
|
+
);
|
|
1770
|
+
if (d.caption)
|
|
1771
|
+
out.foot.push(
|
|
1772
|
+
`<div class="sdvt-cap538" style="${styleAttr({ "border-bottom": `1px solid ${ctx.theme.tokens.text}`, "font-size": "12px" })}">${escapeHtml(d.caption)}</div>`
|
|
1773
|
+
);
|
|
1774
|
+
out.foot.push(
|
|
1775
|
+
`<div class="sdvt-social" style="text-align:${align}">${parts.join(d.stack ? "<br>" : escapeHtml(d.separator))}</div>`
|
|
1776
|
+
);
|
|
1777
|
+
break;
|
|
1778
|
+
}
|
|
1779
|
+
case "scaleNote": {
|
|
1780
|
+
const { note, suffix } = scaleNoteOf(d);
|
|
1781
|
+
for (const k of d.columns) ctx.scaled.set(k, { divisor: d.divisor, decimals: d.decimals });
|
|
1782
|
+
if (d.where !== "label") out.foot.push(escapeHtml(note));
|
|
1783
|
+
if (d.where !== "sourceNote") {
|
|
1784
|
+
const prev = out.labelSuffix;
|
|
1785
|
+
out.labelSuffix = (key) => d.columns.includes(key) ? ` ${suffix}` : prev(key);
|
|
1786
|
+
}
|
|
1787
|
+
break;
|
|
1788
|
+
}
|
|
1789
|
+
case "borderBars": {
|
|
1790
|
+
if (d.side !== "top" && d.side !== "bottom")
|
|
1791
|
+
throw new TableSpecError(`borderBars side must be top or bottom, not ${JSON.stringify(d.side)}`);
|
|
1792
|
+
checkAlign(d.barAlign, "borderBars barAlign");
|
|
1793
|
+
const barWidth = escapeAttr(cssValue(d.barWidth, "borderBars barWidth"));
|
|
1794
|
+
const colors = d.colors.map((c) => escapeAttr(cssValue(c, "borderBars colors")));
|
|
1795
|
+
if (d.imgAlign !== "left" && d.imgAlign !== "right")
|
|
1796
|
+
throw new TableSpecError(
|
|
1797
|
+
`borderBars imgAlign must be left or right, not ${JSON.stringify(d.imgAlign)}`
|
|
1798
|
+
);
|
|
1799
|
+
const barHeight = Number(d.barHeight);
|
|
1800
|
+
if (!Number.isFinite(barHeight))
|
|
1801
|
+
throw new TableSpecError(
|
|
1802
|
+
`borderBars barHeight must be a number, got ${JSON.stringify(d.barHeight)}`
|
|
1803
|
+
);
|
|
1804
|
+
if (d.textAlign !== "left" && d.textAlign !== "right")
|
|
1805
|
+
throw new TableSpecError(`borderBars textAlign must be left or right, not "${d.textAlign}"`);
|
|
1806
|
+
const margin = {
|
|
1807
|
+
left: "margin-left:0;margin-right:auto",
|
|
1808
|
+
center: "margin-left:auto;margin-right:auto",
|
|
1809
|
+
right: "margin-left:auto;margin-right:0"
|
|
1810
|
+
}[d.barAlign];
|
|
1811
|
+
let block;
|
|
1812
|
+
if (!d.text && !d.img) {
|
|
1813
|
+
const bars = colors.map((c) => `<div class="sdvt-bar" style="height:${barHeight}px;background-color:${c}"></div>`).join("");
|
|
1814
|
+
block = `<div class="sdvt-bars-box" style="background-color:transparent;width:${barWidth};${margin}">${bars}</div>`;
|
|
1815
|
+
} else {
|
|
1816
|
+
const text = d.text ? `<span class="sdvt-bars-text" style="${styleAttr({ "font-weight": d.textWeight, color: d.textColor, "font-size": `${d.textSize}px`, [`padding-${d.textAlign}`]: `${d.textPadding}px`, "font-family": "inherit" })}">${escapeHtml(d.text)}</span>` : "<span></span>";
|
|
1817
|
+
const img = d.img ? `<img class="sdvt-bars-img" src="${escapeAttr(d.img)}" alt="" style="${styleAttr({ width: `${d.imgWidth}px`, height: `${d.imgHeight}px`, [`padding-${d.imgAlign}`]: `${d.imgPadding}px` })}">` : "";
|
|
1818
|
+
block = `<div class="sdvt-bars-row" style="display:flex;justify-content:space-between;align-items:center;height:${barHeight}px;background-color:${colors[0] ?? "#000"};width:${barWidth};${margin}">${text}${img}</div>`;
|
|
1819
|
+
}
|
|
1820
|
+
const wrapped = `<div class="sdvt-bars sdvt-bars-${d.side}">${block}</div>`;
|
|
1821
|
+
if (d.side === "top") out.before += wrapped;
|
|
1822
|
+
else out.after += wrapped;
|
|
1823
|
+
break;
|
|
1824
|
+
}
|
|
1825
|
+
case "watermark": {
|
|
1826
|
+
if (!d.text === !d.image) throw new TableSpecError("watermark: supply exactly one of text or image");
|
|
1827
|
+
let url;
|
|
1828
|
+
let tall = false;
|
|
1829
|
+
let extra = "";
|
|
1830
|
+
const opacity = checkPx(d.opacity, "watermark opacity");
|
|
1831
|
+
if (d.image) {
|
|
1832
|
+
url = cssStr(d.image);
|
|
1833
|
+
extra = `;opacity:${opacity}`;
|
|
1834
|
+
} else [url, tall] = watermarkSvg(d.text ?? "", d.color, opacity, d.angle, d.font);
|
|
1835
|
+
const size = cssValue(d.size, "watermark size");
|
|
1836
|
+
out.css.push(
|
|
1837
|
+
`${ctx.sel} tbody{background-image:url("${url}");background-repeat:no-repeat;background-position:${cssValue(d.position, "watermark position")};background-size:${tall ? `auto ${size}` : `${size} auto`}${extra}}`
|
|
1838
|
+
);
|
|
1839
|
+
break;
|
|
1840
|
+
}
|
|
1841
|
+
case "font": {
|
|
1842
|
+
const stack = fontStack(checkFamily(d.family));
|
|
1843
|
+
if (d.google)
|
|
1844
|
+
out.fonts.push({ family: d.family, weights: [typeof d.weight === "number" ? d.weight : 400] });
|
|
1845
|
+
out.css.push(
|
|
1846
|
+
`${ctx.sel}{--sdvt-font-body:${stack};--sdvt-font-label:${stack};--sdvt-font-title:${stack}${d.weight ? `;--sdvt-body-weight:${cssValue(d.weight, "font weight")}` : ""}${d.style ? `;font-style:${cssValue(d.style, "font style")}` : ""}}`
|
|
1847
|
+
);
|
|
1848
|
+
break;
|
|
1849
|
+
}
|
|
1850
|
+
// ---- Task 11
|
|
1851
|
+
case "legendContinuous": {
|
|
1852
|
+
const rec = ctx.recorded;
|
|
1853
|
+
const palette = (d.palette ?? rec?.palette ?? RANK_PALETTE).map((c) => hex6(c));
|
|
1854
|
+
let domain = d.domain ?? rec?.domain;
|
|
1855
|
+
if (!domain && d.columns && ctx.domainRows.length > 0)
|
|
1856
|
+
domain = domainOf(d.columns.map((k) => ctx.domainRows.map((r2) => toNumber(cellValue(r2, k)))));
|
|
1857
|
+
if (!domain && ctx.domainRows.length === 0 && (d.columns || ctx.columns.some(isScaled))) break;
|
|
1858
|
+
if (!domain)
|
|
1859
|
+
throw new TableSpecError(
|
|
1860
|
+
"legendContinuous: no recorded scale (color a column with colorPills/colorRanks/percentileBar first) and no domain or columns given"
|
|
1861
|
+
);
|
|
1862
|
+
if (d.nBins < 1) throw new TableSpecError("legendContinuous nBins must be at least 1");
|
|
1863
|
+
const loc = checkLocation(d.location, "legendContinuous");
|
|
1864
|
+
const swW = checkPx(d.swatchWidth, "legendContinuous swatchWidth");
|
|
1865
|
+
const swH = checkPx(d.swatchHeight, "legendContinuous swatchHeight");
|
|
1866
|
+
const rev = d.reverse ?? rec?.reverse ?? false;
|
|
1867
|
+
const r = ramp(rev ? [...palette].reverse() : palette, domain);
|
|
1868
|
+
const [lo, hi] = domain;
|
|
1869
|
+
const sw = Array.from(
|
|
1870
|
+
{ length: d.nBins },
|
|
1871
|
+
(_, b) => `<span class="sdvt-swatch" style="display:inline-block;width:${swW}px;height:${swH}px;background-color:${r(lo + (b + 0.5) / d.nBins * (hi - lo)) ?? "#808080"}"></span>`
|
|
1872
|
+
);
|
|
1873
|
+
const labels = d.labels ?? [lo, hi].map((v) => formatValue(v, d.digits, "comma", ""));
|
|
1874
|
+
const block = `<div class="sdvt-legend sdvt-legend-${loc}" style="display:flex;flex-direction:${d.titlePosition === "left" ? "row" : "column"};align-items:center;gap:6px;justify-content:center">${d.title ? `<div class="sdvt-legend-title">${escapeHtml(d.title)}</div>` : ""}<div class="sdvt-legend-bar" style="display:flex;align-items:center;gap:4px"><span class="sdvt-legend-lab">${escapeHtml(labels[0] ?? "")}</span>${sw.join("")}<span class="sdvt-legend-lab">${escapeHtml(labels[labels.length - 1] ?? "")}</span></div></div>`;
|
|
1875
|
+
if (loc === "top") out.before += block;
|
|
1876
|
+
else out.after += block;
|
|
1877
|
+
break;
|
|
1878
|
+
}
|
|
1879
|
+
case "legendDiscrete": {
|
|
1880
|
+
const key = d.key === "recorded" ? recordedKey : d.key;
|
|
1881
|
+
if (!key)
|
|
1882
|
+
throw new TableSpecError(
|
|
1883
|
+
'legendDiscrete("recorded") needs a .tiers() decoration before it (no recorded key)'
|
|
1884
|
+
);
|
|
1885
|
+
const loc = checkLocation(d.location, "legendDiscrete");
|
|
1886
|
+
const size = checkPx(d.swatchSize, "legendDiscrete swatchSize");
|
|
1887
|
+
const gap = checkPx(d.gap, "legendDiscrete gap");
|
|
1888
|
+
const items = Object.entries(key).map(
|
|
1889
|
+
([label, color]) => `<span class="sdvt-key-item" style="display:inline-flex;align-items:center;gap:6px"><span class="sdvt-swatch" style="${styleAttr({ display: "inline-block", width: `${size}px`, height: `${size}px`, "background-color": color, border: d.border ? `1px solid ${d.borderColor ?? "currentColor"}` : void 0, "border-radius": d.shape === "circle" ? "50%" : void 0 })}"></span>${escapeHtml(label)}</span>`
|
|
1890
|
+
);
|
|
1891
|
+
const align = checkAlign(d.align, "legendDiscrete align");
|
|
1892
|
+
const block = `<div class="sdvt-legend sdvt-legend-${loc}" style="display:flex;flex-direction:column;align-items:${{ left: "flex-start", center: "center", right: "flex-end" }[align]};gap:4px">${d.heading ? `<div class="sdvt-legend-title">${escapeHtml(d.heading)}</div>` : ""}${d.subtitle ? `<div class="sdvt-legend-sub">${escapeHtml(d.subtitle)}</div>` : ""}<div style="display:flex;flex-direction:${d.direction === "vertical" ? "column" : "row"};flex-wrap:wrap;gap:${gap}px">${items.join("")}</div></div>`;
|
|
1893
|
+
if (loc === "top") out.before += block;
|
|
1894
|
+
else out.after += block;
|
|
1895
|
+
break;
|
|
1896
|
+
}
|
|
1897
|
+
case "legendIcon": {
|
|
1898
|
+
const block = legendIconBlock(d);
|
|
1899
|
+
if (d.location === "top") out.before += block;
|
|
1900
|
+
else out.after += block;
|
|
1901
|
+
break;
|
|
1902
|
+
}
|
|
1903
|
+
case "significance": {
|
|
1904
|
+
if (d.levels.length !== d.symbols.length)
|
|
1905
|
+
throw new TableSpecError("significance levels and symbols must be the same length");
|
|
1906
|
+
if (d.levels.some((x, j) => j > 0 && x < (d.levels[j - 1] ?? x)))
|
|
1907
|
+
throw new TableSpecError("significance levels must be in ascending order, strictest first");
|
|
1908
|
+
for (const pair of d.pairs) {
|
|
1909
|
+
rows.forEach((r, i) => {
|
|
1910
|
+
const p = toNumber(cellValue(r, pair.p));
|
|
1911
|
+
if (p === null) return;
|
|
1912
|
+
const s = d.symbols[d.levels.findIndex((x) => p < x)];
|
|
1913
|
+
if (s) {
|
|
1914
|
+
const k = `${i}:${pair.estimate}`;
|
|
1915
|
+
suffixes.set(
|
|
1916
|
+
k,
|
|
1917
|
+
`${suffixes.get(k) ?? ""}${d.superscript ? `<sup style="font-size:0.7em">${escapeHtml(s)}</sup>` : escapeHtml(s)}`
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
if (d.hideP) out.hiddenColumns.add(pair.p);
|
|
1922
|
+
}
|
|
1923
|
+
if (d.note)
|
|
1924
|
+
out.foot.push(
|
|
1925
|
+
escapeHtml(d.levels.map((x, j) => `${d.symbols[j] ?? ""} p < ${naturalDigits(x)}`).join(", "))
|
|
1926
|
+
);
|
|
1927
|
+
break;
|
|
1928
|
+
}
|
|
1929
|
+
case "outliers": {
|
|
1930
|
+
if (d.method === "bounds" && !d.bounds)
|
|
1931
|
+
throw new TableSpecError("outliers bounds must be [lower, upper] when method is 'bounds'");
|
|
1932
|
+
const t = d.threshold ?? (d.method === "sd" ? 3 : 1.5);
|
|
1933
|
+
const shown = d.fill === void 0 ? null : solid(d.fill, bg);
|
|
1934
|
+
const ink = d.color ?? (shown === null || contrast("#B3261E", shown) >= 4.5 ? "#B3261E" : onColor(shown));
|
|
1935
|
+
let flagged = false;
|
|
1936
|
+
for (const k of d.columns) {
|
|
1937
|
+
const xs = rows.map((r) => toNumber(cellValue(r, k)));
|
|
1938
|
+
const nums = ctx.domainRows.map((r) => toNumber(cellValue(r, k))).filter((v) => v !== null);
|
|
1939
|
+
let lo = Number.NEGATIVE_INFINITY;
|
|
1940
|
+
let hi = Number.POSITIVE_INFINITY;
|
|
1941
|
+
if (d.method === "bounds") {
|
|
1942
|
+
lo = d.bounds?.[0] ?? lo;
|
|
1943
|
+
hi = d.bounds?.[1] ?? hi;
|
|
1944
|
+
} else if (nums.length >= 2 && d.method === "sd") {
|
|
1945
|
+
const m = nums.reduce((a, b) => a + b, 0) / nums.length;
|
|
1946
|
+
const s = sampleSd(nums);
|
|
1947
|
+
if (s !== 0) {
|
|
1948
|
+
lo = m - t * s;
|
|
1949
|
+
hi = m + t * s;
|
|
1950
|
+
}
|
|
1951
|
+
} else if (nums.length >= 2) {
|
|
1952
|
+
const q1 = quantile7(nums, 0.25);
|
|
1953
|
+
const q3 = quantile7(nums, 0.75);
|
|
1954
|
+
if (q3 !== q1) {
|
|
1955
|
+
lo = q1 - t * (q3 - q1);
|
|
1956
|
+
hi = q3 + t * (q3 - q1);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
xs.forEach((v, i) => {
|
|
1960
|
+
if (v === null || !(v < lo && d.side !== "high" || v > hi && d.side !== "low")) return;
|
|
1961
|
+
flagged = true;
|
|
1962
|
+
addCell(i, k, {
|
|
1963
|
+
color: ink,
|
|
1964
|
+
"font-weight": d.bold ? "bold" : void 0,
|
|
1965
|
+
"background-color": d.fill
|
|
1966
|
+
});
|
|
1967
|
+
if (d.symbol)
|
|
1968
|
+
suffixes.set(`${i}:${k}`, `${suffixes.get(`${i}:${k}`) ?? ""}${escapeHtml(d.symbol)}`);
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
if (flagged && d.note) {
|
|
1972
|
+
const tail = { both: "", high: " (high side only)", low: " (low side only)" }[d.side];
|
|
1973
|
+
out.foot.push(
|
|
1974
|
+
escapeHtml(
|
|
1975
|
+
typeof d.note === "string" ? d.note : d.method === "sd" ? `Marked values fall more than ${naturalDigits(t)} standard deviation${t === 1 ? "" : "s"} from the column mean${tail}.` : d.method === "iqr" ? `Marked values fall outside ${naturalDigits(t)} \xD7 IQR of the column quartiles${tail}.` : `Marked values fall outside ${d.bounds?.[0] ?? "NA"}\u2013${d.bounds?.[1] ?? "NA"}${tail}.`
|
|
1976
|
+
)
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
break;
|
|
1980
|
+
}
|
|
1981
|
+
case "marginalia": {
|
|
1982
|
+
const ink = onColor(bg);
|
|
1983
|
+
const color = d.color ?? secondaryOn(bg, ink);
|
|
1984
|
+
const rule = d.ruleColor ?? mix(bg, ink, 0.18);
|
|
1985
|
+
const align = checkAlign(d.align, "marginalia align");
|
|
1986
|
+
for (const k of d.columns) {
|
|
1987
|
+
out.labelText.set(k, d.label);
|
|
1988
|
+
if (d.width !== void 0)
|
|
1989
|
+
labelStyles.set(
|
|
1990
|
+
k,
|
|
1991
|
+
`width:${typeof d.width === "number" ? `${d.width}px` : escapeAttr(cssValue(d.width, "marginalia width"))}`
|
|
1992
|
+
);
|
|
1993
|
+
rows.forEach(
|
|
1994
|
+
(_, i) => addCell(i, k, {
|
|
1995
|
+
"font-style": d.italic ? "italic" : "normal",
|
|
1996
|
+
"font-size": d.size,
|
|
1997
|
+
color,
|
|
1998
|
+
"border-left": d.rule ? `1px solid ${rule}` : void 0,
|
|
1999
|
+
"text-align": align
|
|
2000
|
+
})
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
break;
|
|
2004
|
+
}
|
|
2005
|
+
case "tiers": {
|
|
2006
|
+
if (!d.colors)
|
|
2007
|
+
throw new TableSpecError("tiers colors is missing; pass levels and colors as two lists");
|
|
2008
|
+
if (d.colors.length !== d.levels.length)
|
|
2009
|
+
throw new TableSpecError(
|
|
2010
|
+
`tiers levels and colors must be the same length: got ${d.levels.length} levels and ${d.colors.length} colors`
|
|
2011
|
+
);
|
|
2012
|
+
const fills = d.colors.map((c) => hex6(c));
|
|
2013
|
+
const tierOf = (r) => {
|
|
2014
|
+
const v = cellValue(r, d.tierKey);
|
|
2015
|
+
return isBlank(v) ? null : String(v);
|
|
2016
|
+
};
|
|
2017
|
+
const held = [...new Set(ctx.domainRows.map(tierOf).filter((t) => t !== null))];
|
|
2018
|
+
const missing = d.levels.filter((l) => !held.includes(l));
|
|
2019
|
+
if (missing.length > 0)
|
|
2020
|
+
ctx.warn(
|
|
2021
|
+
`sdvtables:tiers:${ctx.id}`,
|
|
2022
|
+
`tier(s) ${missing.join(", ")} are not in "${d.tierKey}", so they get no rows; it holds ${held.join(", ")}`
|
|
2023
|
+
);
|
|
2024
|
+
rows.forEach((r, i) => {
|
|
2025
|
+
const t = tierOf(r);
|
|
2026
|
+
const f = t === null ? void 0 : fills[d.levels.indexOf(t)];
|
|
2027
|
+
if (f) addCell(i, d.tierKey, { "background-color": f, color: onColor(f), "font-weight": "bold" });
|
|
2028
|
+
});
|
|
2029
|
+
recordedKey = Object.fromEntries(d.levels.map((l, j) => [l, fills[j] ?? ""]));
|
|
2030
|
+
break;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
out.cellSuffix = (i, key) => suffixes.get(`${i}:${key}`) ?? "";
|
|
2034
|
+
out.labelStyle = (key) => labelStyles.get(key) ?? "";
|
|
2035
|
+
out.css = out.css.map((r) => r.replace(/</g, "\\3c "));
|
|
2036
|
+
out.rowClass = (i) => (rowClasses.get(i) ?? []).map((c) => ` ${c}`).join("");
|
|
2037
|
+
out.rowStyle = (i) => {
|
|
2038
|
+
const s = rowStyles.get(i);
|
|
2039
|
+
return s ? styleAttr(s) : "";
|
|
2040
|
+
};
|
|
2041
|
+
out.cellStyle = (i, key) => {
|
|
2042
|
+
const s = cellStyles.get(`${i}:${key}`);
|
|
2043
|
+
return s ? styleAttr(s) : "";
|
|
2044
|
+
};
|
|
2045
|
+
return out;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// src/html/layout.ts
|
|
2049
|
+
function expandTiers(spec) {
|
|
2050
|
+
const d = spec.decorations.find((x) => x.type === "tiers");
|
|
2051
|
+
if (!d) return spec;
|
|
2052
|
+
const columns = spec.columns.map((c) => {
|
|
2053
|
+
if (!d.imageColumns.includes(c.key)) return { ...c, label: "" };
|
|
2054
|
+
return {
|
|
2055
|
+
key: c.key,
|
|
2056
|
+
label: "",
|
|
2057
|
+
kind: "image",
|
|
2058
|
+
height: d.imgHeight,
|
|
2059
|
+
...c.align ? { align: c.align } : {},
|
|
2060
|
+
...c.width ? { width: c.width } : {}
|
|
2061
|
+
};
|
|
2062
|
+
});
|
|
2063
|
+
return {
|
|
2064
|
+
...spec,
|
|
2065
|
+
theme: { name: "tier", density: spec.theme.density, options: { style: d.style } },
|
|
2066
|
+
columns
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
function snakeLayout(d, rows, headCells, ncol, rowHtml, trOf, gapTh, gapTd) {
|
|
2070
|
+
const blocks = snakeAlign(
|
|
2071
|
+
rows.map((row, i) => ({ row, i })),
|
|
2072
|
+
{ nCols: d.nCols, ...d.rowsPerCol !== void 0 ? { rowsPerCol: d.rowsPerCol } : {} }
|
|
2073
|
+
);
|
|
2074
|
+
if (blocks.length < 2 || rows.length === 0) return null;
|
|
2075
|
+
const per = blocks[0]?.length ?? 0;
|
|
2076
|
+
const blank = `<td class="sdvt-cell sdvt-blank">${escapeHtml(d.fill)}</td>`.repeat(ncol);
|
|
2077
|
+
const body = Array.from(
|
|
2078
|
+
{ length: per },
|
|
2079
|
+
(_, k) => trOf(
|
|
2080
|
+
blocks[0]?.[k]?.i ?? k,
|
|
2081
|
+
blocks.map((b) => {
|
|
2082
|
+
const e = b[k];
|
|
2083
|
+
return e ? rowHtml(e.row, e.i) : blank;
|
|
2084
|
+
}).join(gapTd)
|
|
2085
|
+
)
|
|
2086
|
+
);
|
|
2087
|
+
return { head: blocks.map(() => headCells).join(gapTh), body };
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
// src/html/parts.ts
|
|
2091
|
+
function checkKeys(spec, rows) {
|
|
2092
|
+
const first = rows[0];
|
|
2093
|
+
if (first === void 0) return;
|
|
2094
|
+
const keys = new Set(Object.keys(first));
|
|
2095
|
+
for (const col of spec.columns) {
|
|
2096
|
+
const need = [
|
|
2097
|
+
col.key,
|
|
2098
|
+
..."to" in col ? [col.to] : [],
|
|
2099
|
+
..."keys" in col ? col.keys : [],
|
|
2100
|
+
...["stack", "team", "colorRef", "markKey", "iconRef", "iconColorRef"].flatMap((f) => {
|
|
2101
|
+
const k = col[f];
|
|
2102
|
+
return typeof k === "string" ? [k] : [];
|
|
2103
|
+
})
|
|
2104
|
+
];
|
|
2105
|
+
for (const k of need)
|
|
2106
|
+
if (!keys.has(k))
|
|
2107
|
+
throw new TableSpecError(
|
|
2108
|
+
`column "${k}" (kind ${col.kind}) is not a key of the first row; keys are ${[...keys].join(", ")}`
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
function alignOf(col) {
|
|
2113
|
+
if (col.align) return col.align;
|
|
2114
|
+
switch (col.kind) {
|
|
2115
|
+
case "num":
|
|
2116
|
+
case "int":
|
|
2117
|
+
case "pct":
|
|
2118
|
+
case "rank":
|
|
2119
|
+
case "delta":
|
|
2120
|
+
case "tally":
|
|
2121
|
+
case "colorPills":
|
|
2122
|
+
case "colorRanks":
|
|
2123
|
+
case "iconSet":
|
|
2124
|
+
return "right";
|
|
2125
|
+
case "bubble":
|
|
2126
|
+
case "gauge":
|
|
2127
|
+
return "center";
|
|
2128
|
+
case "logo":
|
|
2129
|
+
case "wordmark":
|
|
2130
|
+
case "headshot":
|
|
2131
|
+
case "indicatorBox":
|
|
2132
|
+
case "image":
|
|
2133
|
+
return "center";
|
|
2134
|
+
default:
|
|
2135
|
+
return "left";
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
function labelOf(col) {
|
|
2139
|
+
return col.label ?? col.key.replace(/_/g, " ").replace(/\b\w/g, (m) => m.toUpperCase());
|
|
2140
|
+
}
|
|
2141
|
+
function themeKey(ref) {
|
|
2142
|
+
return `sdvt-t-${fnv1a32(JSON.stringify([ref.name, ref.density, Object.entries(ref.options ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)]))}`;
|
|
2143
|
+
}
|
|
2144
|
+
function styleSheet(spec, theme = resolveTheme(spec.theme)) {
|
|
2145
|
+
const sel = `.${themeKey(spec.theme)}`;
|
|
2146
|
+
return `${tokensCSS(sel, theme.tokens)}
|
|
2147
|
+
${BASE_CSS(sel)}
|
|
2148
|
+
${theme.rules(sel)}`;
|
|
2149
|
+
}
|
|
2150
|
+
function sortAria(sort, key) {
|
|
2151
|
+
if (!sort || sort.col !== key) return "none";
|
|
2152
|
+
return sort.dir === "asc" ? "ascending" : "descending";
|
|
2153
|
+
}
|
|
2154
|
+
function attrsText(attrs) {
|
|
2155
|
+
return Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeAttr(v)}"`).join("");
|
|
2156
|
+
}
|
|
2157
|
+
var styleTag = (css) => css ? `<style>${css}</style>` : "";
|
|
2158
|
+
function tableHTML(p) {
|
|
2159
|
+
return `${styleTag(p.rules)}${p.before}<table${p.tableAttrs}>${p.caption}<thead>${p.headRows}<tr>${p.head}</tr></thead><tbody>${p.rows}</tbody>${p.foot}</table>${p.after}`;
|
|
2160
|
+
}
|
|
2161
|
+
function assemble(p, inner = tableHTML(p)) {
|
|
2162
|
+
return `${p.link ? `${p.link}
|
|
2163
|
+
` : ""}<div${attrsText(p.wrapperAttrs)}>${styleTag(p.sheet)}${inner}</div>`;
|
|
2164
|
+
}
|
|
2165
|
+
function renderParts(input, rows, opts = {}) {
|
|
2166
|
+
checkKeys(input, rows);
|
|
2167
|
+
const id = tableId(input);
|
|
2168
|
+
const spec = expandTiers(input);
|
|
2169
|
+
const theme = resolveTheme(spec.theme);
|
|
2170
|
+
if (!/^[A-Za-z][\w-]*$/.test(id))
|
|
2171
|
+
throw new TableSpecError(
|
|
2172
|
+
`table id ${JSON.stringify(id)} must match /^[A-Za-z][\\w-]*$/ (it becomes a CSS selector)`
|
|
2173
|
+
);
|
|
2174
|
+
const sel = `#${id}`;
|
|
2175
|
+
const groupBy = spec.decorations.find(
|
|
2176
|
+
(d) => d.type === "groupBy"
|
|
2177
|
+
);
|
|
2178
|
+
const engineHidden = opts.hidden ?? [];
|
|
2179
|
+
const bad = engineHidden.filter((k) => !spec.columns.some((c) => c.key === k));
|
|
2180
|
+
if (bad.length > 0)
|
|
2181
|
+
throw new TableSpecError(
|
|
2182
|
+
`hidden names no column ${bad.map((k) => JSON.stringify(k)).join(", ")}; columns are ${spec.columns.map((c) => c.key).join(", ")}`
|
|
2183
|
+
);
|
|
2184
|
+
const domainRows = opts.domainRows ?? rows;
|
|
2185
|
+
const grid = opts.interactive === true ? opts.grid : void 0;
|
|
2186
|
+
const tabRow = grid === void 0 ? -1 : Math.min(grid.row, rows.length - 1);
|
|
2187
|
+
const scales = columnScales(spec, rows, warn, domainRows);
|
|
2188
|
+
const ctx = {
|
|
2189
|
+
spec,
|
|
2190
|
+
rows,
|
|
2191
|
+
domainRows,
|
|
2192
|
+
theme,
|
|
2193
|
+
id,
|
|
2194
|
+
sel,
|
|
2195
|
+
columns: spec.columns,
|
|
2196
|
+
groupKey: groupBy?.key,
|
|
2197
|
+
warn,
|
|
2198
|
+
teamIds: teamIdsOf(spec, rows),
|
|
2199
|
+
scales,
|
|
2200
|
+
recorded: scales.get(spec.columns.filter(isScaled).at(-1)?.key ?? ""),
|
|
2201
|
+
// none when the last one was skipped (zero rows)
|
|
2202
|
+
scaled: /* @__PURE__ */ new Map()
|
|
2203
|
+
};
|
|
2204
|
+
const deco = applyDecorations(spec, rows, ctx);
|
|
2205
|
+
const visible = spec.columns.filter((c) => !deco.hiddenColumns.has(c.key) && !engineHidden.includes(c.key));
|
|
2206
|
+
const ncol = visible.length;
|
|
2207
|
+
const span = Math.max(1, ncol);
|
|
2208
|
+
const labels = new Map(
|
|
2209
|
+
visible.map((c) => [c.key, (deco.labelText.get(c.key) ?? labelOf(c)) + deco.labelSuffix(c.key)])
|
|
2210
|
+
);
|
|
2211
|
+
let head = visible.map((c) => {
|
|
2212
|
+
const label = `${deco.label(c, labels.get(c.key))}${c.subheader ? `<span class="sdvt-subheader">${escapeHtml(c.subheader)}</span>` : ""}`;
|
|
2213
|
+
const sortable = opts.interactive === true && c.sortable !== false;
|
|
2214
|
+
const dir = sortAria(opts.sort, c.key);
|
|
2215
|
+
const aria = sortable && dir !== "none" ? ` aria-sort="${dir}"` : "";
|
|
2216
|
+
const inner = sortable ? `<button type="button" class="sdvt-sort" data-sdv-sort="${escapeAttr(c.key)}">${label}</button>` : label;
|
|
2217
|
+
const isCurrent = grid?.col === c.key;
|
|
2218
|
+
const current = isCurrent ? " sdvt-col-current" : "";
|
|
2219
|
+
const currentAria = isCurrent ? ' aria-current="true"' : "";
|
|
2220
|
+
return `<th scope="col" class="sdvt-label sdvt-${escapeAttr(alignOf(c))}${current}" data-col="${escapeAttr(c.key)}" data-kind="${escapeAttr(c.kind)}"${currentAria}${aria}${styleOf([c.width ? `width:${escapeAttr(cssValue(c.width, `column ${c.key} width`))}` : "", deco.labelStyle(c.key)])}>${inner}</th>`;
|
|
2221
|
+
}).join("");
|
|
2222
|
+
const cellOf = (c, row, i) => {
|
|
2223
|
+
const html = renderCell(c, row, i, ctx);
|
|
2224
|
+
if (c.tooltip !== true) return `>${html}`;
|
|
2225
|
+
const text = html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
2226
|
+
const raw = cellValue(row, c.key);
|
|
2227
|
+
const title = text || (raw === null || raw === void 0 ? "" : escapeAttr(raw));
|
|
2228
|
+
return title ? ` title="${title}">${html}` : `>${html}`;
|
|
2229
|
+
};
|
|
2230
|
+
const cellsOf = (row, i) => visible.map(
|
|
2231
|
+
(c) => `<td class="sdvt-cell sdvt-kind-${escapeAttr(c.kind)} sdvt-${escapeAttr(alignOf(c))}" data-col="${escapeAttr(c.key)}"${styleOf([kindCellStyle(c, row, i, ctx), deco.cellStyle(i, c.key)])}${cellOf(c, row, i)}${deco.cellSuffix(i, c.key)}</td>`
|
|
2232
|
+
).join("");
|
|
2233
|
+
const striped = theme.tokens.stripe !== "transparent";
|
|
2234
|
+
let shown = 0;
|
|
2235
|
+
const gridRow = (i) => grid === void 0 ? "" : ` tabindex="${i === tabRow ? 0 : -1}" aria-selected="${opts.selected?.has(i) === true}"`;
|
|
2236
|
+
const trOf = (i, cells) => `<tr class="sdvt-row${striped && shown++ % 2 === 1 ? " sdvt-stripe" : ""}${deco.rowClass(i)}${opts.selected?.has(i) === true ? " sdvt-selected" : ""}" data-row="${i}"${gridRow(i)}${styleOf([deco.rowStyle(i)])}>${cells}</tr>`;
|
|
2237
|
+
let body = [];
|
|
2238
|
+
const snake = spec.decorations.find(
|
|
2239
|
+
(d) => d.type === "snake"
|
|
2240
|
+
);
|
|
2241
|
+
if (snake && ctx.groupKey !== void 0) throw new TableSpecError("snake cannot be combined with groupBy");
|
|
2242
|
+
if (snake && opts.interactive === true)
|
|
2243
|
+
throw new TableSpecError(
|
|
2244
|
+
"snake cannot be combined with an interactive table (renderHTML(table), hydrate, SdvTable); render it statically with renderHTML(spec, rows)"
|
|
2245
|
+
);
|
|
2246
|
+
const gap = snake && snake.gap > 0 ? `style="width:${snake.gap}px;border:none"` : "";
|
|
2247
|
+
const snaked = snake ? snakeLayout(
|
|
2248
|
+
snake,
|
|
2249
|
+
rows,
|
|
2250
|
+
head,
|
|
2251
|
+
ncol,
|
|
2252
|
+
cellsOf,
|
|
2253
|
+
trOf,
|
|
2254
|
+
gap ? `<th class="sdvt-gap" ${gap}></th>` : "",
|
|
2255
|
+
gap ? `<td class="sdvt-gap" ${gap}></td>` : ""
|
|
2256
|
+
) : null;
|
|
2257
|
+
if (snake && snaked) {
|
|
2258
|
+
head = snaked.head;
|
|
2259
|
+
body = snaked.body;
|
|
2260
|
+
if (snake.cleanGaps)
|
|
2261
|
+
deco.css.push(
|
|
2262
|
+
`${sel} .sdvt-gap,${sel} td.sdvt-blank{border:none!important;background:transparent!important;box-shadow:none!important}`
|
|
2263
|
+
);
|
|
2264
|
+
} else {
|
|
2265
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2266
|
+
if (ctx.groupKey === void 0)
|
|
2267
|
+
groups.set(
|
|
2268
|
+
void 0,
|
|
2269
|
+
rows.map((_, i) => i)
|
|
2270
|
+
);
|
|
2271
|
+
else
|
|
2272
|
+
rows.forEach((row, i) => {
|
|
2273
|
+
const g = row[ctx.groupKey];
|
|
2274
|
+
const list = groups.get(g);
|
|
2275
|
+
if (list) list.push(i);
|
|
2276
|
+
else groups.set(g, [i]);
|
|
2277
|
+
});
|
|
2278
|
+
for (const [g, idxs] of groups) {
|
|
2279
|
+
if (ctx.groupKey !== void 0)
|
|
2280
|
+
body.push(
|
|
2281
|
+
`<tr class="sdvt-group-row${deco.groupRowClass()}"><th scope="rowgroup" colspan="${span}" class="sdvt-group">${escapeHtml(g)}</th></tr>`
|
|
2282
|
+
);
|
|
2283
|
+
for (const i of idxs) body.push(trOf(i, cellsOf(rows[i], i)));
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
const fonts = [...theme.fonts, ...deco.fonts];
|
|
2287
|
+
const name = spec.theme.name;
|
|
2288
|
+
return {
|
|
2289
|
+
id,
|
|
2290
|
+
link: opts.fonts === false ? "" : fontsLink(fonts),
|
|
2291
|
+
// css:"none" drops only the shared theme sheet (A98, A100)
|
|
2292
|
+
wrapperAttrs: {
|
|
2293
|
+
class: `sdvt sdvt-theme-${name} ${themeKey(spec.theme)}`,
|
|
2294
|
+
id,
|
|
2295
|
+
"data-sdvt-theme": name,
|
|
2296
|
+
"data-sdvt-density": spec.theme.density
|
|
2297
|
+
},
|
|
2298
|
+
sheet: opts.css === "none" ? "" : styleSheet(spec, theme),
|
|
2299
|
+
rules: deco.css.join("\n"),
|
|
2300
|
+
before: deco.before,
|
|
2301
|
+
caption: deco.caption ? `<caption>${deco.caption}</caption>` : "",
|
|
2302
|
+
headRows: deco.headRows,
|
|
2303
|
+
head,
|
|
2304
|
+
labels,
|
|
2305
|
+
rows: body.join(""),
|
|
2306
|
+
foot: deco.foot.length > 0 ? `<tfoot>${deco.foot.map((f) => `<tr><td colspan="${span}">${f}</td></tr>`).join("")}</tfoot>` : "",
|
|
2307
|
+
after: deco.after,
|
|
2308
|
+
tableAttrs: grid === void 0 ? "" : ' role="grid" aria-multiselectable="true"'
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
var fontsLinkFor = (spec) => renderParts(spec, []).link;
|
|
2312
|
+
|
|
2313
|
+
export { ICON_NAMES, alignOf, assemble, attrsText, escapeAttr, escapeHtml, fontsLink, fontsLinkFor, labelOf, loadTeamNames, renderParts, scaleNoteOf, sortAria, styleSheet, tableHTML, tableId, teamNameSync, themeKey };
|
|
2314
|
+
//# sourceMappingURL=chunk-R375P3MN.js.map
|
|
2315
|
+
//# sourceMappingURL=chunk-R375P3MN.js.map
|