@sportsdataverse/sdvtables 0.0.0-stage → 0.1.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 +561 -2
- package/dist/chunk-4VKYHOHL.js +2081 -0
- package/dist/chunk-4VKYHOHL.js.map +1 -0
- package/dist/chunk-U6W6MWKW.js +179 -0
- package/dist/chunk-U6W6MWKW.js.map +1 -0
- package/dist/chunk-UCRLJGDL.js +1628 -0
- package/dist/chunk-UCRLJGDL.js.map +1 -0
- package/dist/engine-CkfxYXAL.d.ts +190 -0
- package/dist/export.d.ts +88 -0
- package/dist/export.js +309 -0
- package/dist/export.js.map +1 -0
- package/dist/html.d.ts +330 -0
- package/dist/html.js +5 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.js +93 -0
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +72 -0
- package/dist/react.js +183 -0
- package/dist/react.js.map +1 -0
- package/dist/spec-BEhLDtj_.d.ts +430 -0
- package/dist/tokens-CpYFsFtf.d.ts +75 -0
- package/package.json +86 -3
|
@@ -0,0 +1,1628 @@
|
|
|
1
|
+
import { resolveTheme, TableSpecError, nextSortDir, TOKEN_KEYS, toNumber, isBlank, secondaryOn, naturalDigits, RANK_PALETTE, formatValue, fontStack, formatNumber, selectRows, snakeAlign, matches, ordinal } from './chunk-4VKYHOHL.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]/;
|
|
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 ; { } < > " \\ 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/fonts.ts
|
|
51
|
+
function fontsLink(fonts) {
|
|
52
|
+
const byFamily = /* @__PURE__ */ new Map();
|
|
53
|
+
for (const f of fonts) {
|
|
54
|
+
const set = byFamily.get(f.family) ?? /* @__PURE__ */ new Set();
|
|
55
|
+
for (const w of f.weights) set.add(w);
|
|
56
|
+
byFamily.set(f.family, set);
|
|
57
|
+
}
|
|
58
|
+
if (byFamily.size === 0) return "";
|
|
59
|
+
const q = [...byFamily.entries()].sort(([a], [b]) => a.localeCompare(b)).map(
|
|
60
|
+
([fam, ws]) => `family=${encodeURIComponent(fam).replace(/%20/g, "+")}:wght@${[...ws].sort((a, b) => a - b).join(";")}`
|
|
61
|
+
).join("&");
|
|
62
|
+
return `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?${q}&display=swap">`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/table-id.ts
|
|
66
|
+
function fnv1a32(s) {
|
|
67
|
+
let h = 2166136261;
|
|
68
|
+
for (let i = 0; i < s.length; i++) {
|
|
69
|
+
h ^= s.charCodeAt(i);
|
|
70
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
71
|
+
}
|
|
72
|
+
return h.toString(16).padStart(8, "0");
|
|
73
|
+
}
|
|
74
|
+
function tableId(spec) {
|
|
75
|
+
return spec.id ?? `sdvt-${fnv1a32(JSON.stringify(spec))}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/themes/base-css.ts
|
|
79
|
+
var kebab = (s) => s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
80
|
+
function tokensCSS(sel, t) {
|
|
81
|
+
return `${sel}{${TOKEN_KEYS.map((k) => `--sdvt-${kebab(k)}:${t[k]};`).join("")}}`;
|
|
82
|
+
}
|
|
83
|
+
function BASE_CSS(sel) {
|
|
84
|
+
const s = sel;
|
|
85
|
+
return [
|
|
86
|
+
`${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)}`,
|
|
87
|
+
// font:inherit: a host's table{font} (Observable Framework's 13px sans) outranks inheriting the root's type
|
|
88
|
+
`${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)}`,
|
|
89
|
+
`${s} caption{caption-side:top;text-align:var(--sdvt-heading-align);background:var(--sdvt-heading-bg);padding:var(--sdvt-heading-pad) 5px}`,
|
|
90
|
+
`${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)}`,
|
|
91
|
+
`${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)}`,
|
|
92
|
+
// great_tables draws the subtitle on its own heading row in every theme
|
|
93
|
+
`${s} thead{position:relative}${s} thead::after{content:"";position:absolute;left:0;right:0;bottom:0;height:4px;background:var(--sdvt-horizon)}`,
|
|
94
|
+
`${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}`,
|
|
95
|
+
`${s} .sdvt-subheader{display:block;font-weight:400;font-size:0.8em;color:var(--sdvt-muted);text-transform:none;letter-spacing:normal}`,
|
|
96
|
+
`${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}`,
|
|
97
|
+
// A host's td{color} (Framework; Infima's --ifm-table-cell-color) outranks the ink a cell only inherits, so the
|
|
98
|
+
// cell asks for it explicitly. inherit, not var(--sdvt-text): the row's ink is the theme's, or spotlight's dim on
|
|
99
|
+
// the <tr>. ponytail: (0,1,0), the scope in :where(): beats any element-only host rule (td, table td), loses to an
|
|
100
|
+
// author's class-qualified rule (.page td at (0,1,1)) and to every theme rule and inline cell style.
|
|
101
|
+
`:where(${s}) .sdvt-cell{color:inherit}`,
|
|
102
|
+
// Hosts paint rows: Infima stripes every second one (table tr:nth-child(2n), (0,1,2)) under cells that leave the
|
|
103
|
+
// ground to the root, and Framework and Infima rule them. sdvtables' own sheets never colour or border a <tr>
|
|
104
|
+
// (themes and decorations style cells; cutlines and watermarks are images; row decorations are inline), so rows
|
|
105
|
+
// stay clear. ponytail: :nth-child(n) lifts this to (0,2,1), past that stripe whatever the stylesheet order.
|
|
106
|
+
`${s} tr:nth-child(n){background-color:transparent;border:0}`,
|
|
107
|
+
// Infima borders every cell (table th, table td { border }, (0,0,2)). ponytail: (0,1,1) clears that, and every
|
|
108
|
+
// border sdvtables draws comes from a more specific rule or a later one (themes and decorations follow BASE_CSS)
|
|
109
|
+
`${s} td,${s} th{border:0}`,
|
|
110
|
+
`${s} td.sdvt-cell{padding:var(--sdvt-pad) 5px;border-top:var(--sdvt-hline);font-variant-numeric:tabular-nums;vertical-align:middle}`,
|
|
111
|
+
`${s} tbody tr:first-child td.sdvt-cell{border-top:none}${s} tbody{border-bottom:var(--sdvt-body-border-bottom)}`,
|
|
112
|
+
// background-color, not the `background` shorthand: the shorthand would reset the hovered row's underline image
|
|
113
|
+
`${s} tr.sdvt-stripe td.sdvt-cell{background-color:var(--sdvt-stripe)}`,
|
|
114
|
+
`${s} .sdvt-left{text-align:left}${s} .sdvt-center{text-align:center}${s} .sdvt-right{text-align:right}`,
|
|
115
|
+
`${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}`,
|
|
116
|
+
`${s} img.sdvt-mark{vertical-align:middle}`,
|
|
117
|
+
`${s} img{max-width:none}`,
|
|
118
|
+
// A47: a host img{max-width:100%} reset (Docusaurus, Tailwind preflight) squashed cell logos
|
|
119
|
+
`${s} table{--bs-table-bg:transparent;--bs-table-color:currentcolor}`,
|
|
120
|
+
// utils-theme.R .theme_bs_host: Bootstrap hosts repaint td
|
|
121
|
+
// Phase 5 controls: renderHTML(table), hydrate and <SdvTable/> (the last rule is J31 A5, a neutral selected-row overlay)
|
|
122
|
+
`${s} .sdvt-sort{all:unset;cursor:pointer;display:inline-block;width:100%}`,
|
|
123
|
+
`${s} .sdvt-sort:focus-visible{outline:2px solid currentColor;outline-offset:2px}`,
|
|
124
|
+
// all:unset drops the UA focus ring (WCAG 2.4.7)
|
|
125
|
+
// the arrow repeats aria-sort, so its alt text is empty (kept out of the button name); the plain declaration is the fallback
|
|
126
|
+
`${s} th[aria-sort="ascending"] .sdvt-sort::after{content:" \u25B2";content:" \u25B2"/"";font-size:.7em}`,
|
|
127
|
+
`${s} th[aria-sort="descending"] .sdvt-sort::after{content:" \u25BC";content:" \u25BC"/"";font-size:.7em}`,
|
|
128
|
+
`${s} .sdvt-toolbar{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px}`,
|
|
129
|
+
// 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
|
|
130
|
+
// in every theme (a black or ink rule stays itself); where color-mix is unsupported, the text colour
|
|
131
|
+
`${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)}`,
|
|
132
|
+
`${s} .sdvt-pager{display:flex;gap:8px;align-items:center;justify-content:flex-end;margin-top:6px}`,
|
|
133
|
+
`${s} .sdvt-page{font:inherit;cursor:pointer}`,
|
|
134
|
+
`${s} .sdvt-page[aria-disabled="true"]{opacity:.4;cursor:default}`,
|
|
135
|
+
// M1: aria-disabled, so focus stays on an edge button
|
|
136
|
+
`${s} tr.sdvt-selected>td{box-shadow:inset 0 0 0 9999px rgba(127,127,127,.18)}`,
|
|
137
|
+
`${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)}`,
|
|
138
|
+
// I4: the tint is 1.2:1; a text-coloured bar gives the state 3:1
|
|
139
|
+
// J31 (A29): a hovered row (its own pointer, or a linked figure's) gets a text-coloured underline, 3:1 like I4's bar.
|
|
140
|
+
// A background image: it sits beside the selected overlay (box-shadow) and moves no layout. The scope and the row
|
|
141
|
+
// sit in :where(), so the rule weighs (0,1,1), td.sdvt-cell alone: the documented one-rule opt-out
|
|
142
|
+
// tr.sdvt-hover>td.sdvt-cell{background-image:none}, at (0,2,2), wins wherever a page puts it
|
|
143
|
+
`: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}`,
|
|
144
|
+
`${s} tr.sdvt-row:focus-visible{outline:2px solid currentColor;outline-offset:-2px}`,
|
|
145
|
+
// Task 10: the grid's tab stop (WCAG 2.4.7)
|
|
146
|
+
`${s} th.sdvt-col-current .sdvt-sort{text-decoration:underline;text-underline-offset:4px}`
|
|
147
|
+
// Task 10: the column `s` sorts
|
|
148
|
+
].join("\n");
|
|
149
|
+
}
|
|
150
|
+
function ramp(palette, [lo, hi]) {
|
|
151
|
+
const k = palette.length - 1;
|
|
152
|
+
return (v) => {
|
|
153
|
+
if (!(lo <= v && v <= hi)) return null;
|
|
154
|
+
if (k === 0) return palette[0] ?? null;
|
|
155
|
+
const t = hi === lo ? 0.5 : (v - lo) / (hi - lo);
|
|
156
|
+
const i = Math.min(Math.floor(t * k), k - 1);
|
|
157
|
+
return mix(palette[i], palette[i + 1], t * k - i);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function averageRanks(values, desc) {
|
|
161
|
+
const idx = values.map((v, i) => [v, i]).filter((p) => p[0] !== null).sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
162
|
+
const out = values.map(() => null);
|
|
163
|
+
for (let s = 0; s < idx.length; ) {
|
|
164
|
+
let e = s;
|
|
165
|
+
while (e + 1 < idx.length && idx[e + 1]?.[0] === idx[s]?.[0]) e++;
|
|
166
|
+
const r = (s + e) / 2 + 1;
|
|
167
|
+
for (let j = s; j <= e; j++) out[idx[j]?.[1]] = r;
|
|
168
|
+
s = e + 1;
|
|
169
|
+
}
|
|
170
|
+
if (!desc || idx.length === 0) return out;
|
|
171
|
+
let top = 0;
|
|
172
|
+
for (const r of out) if (r !== null && r > top) top = r;
|
|
173
|
+
return out.map((r) => r === null ? null : top - r + 1);
|
|
174
|
+
}
|
|
175
|
+
function domainOf(columns) {
|
|
176
|
+
let lo = Number.POSITIVE_INFINITY;
|
|
177
|
+
let hi = Number.NEGATIVE_INFINITY;
|
|
178
|
+
for (const col of columns)
|
|
179
|
+
for (const v of col)
|
|
180
|
+
if (v !== null) {
|
|
181
|
+
if (v < lo) lo = v;
|
|
182
|
+
if (v > hi) hi = v;
|
|
183
|
+
}
|
|
184
|
+
if (lo > hi) throw new TableSpecError("no domain given and no numeric value to derive one from");
|
|
185
|
+
return [lo, hi];
|
|
186
|
+
}
|
|
187
|
+
function quantile7(xs, p) {
|
|
188
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
189
|
+
const h = (s.length - 1) * p;
|
|
190
|
+
const lo = Math.floor(h);
|
|
191
|
+
return s[lo] + (h - lo) * (s[Math.min(lo + 1, s.length - 1)] - s[lo]);
|
|
192
|
+
}
|
|
193
|
+
function sampleSd(xs) {
|
|
194
|
+
const m = xs.reduce((a, b) => a + b, 0) / xs.length;
|
|
195
|
+
return Math.sqrt(xs.reduce((a, b) => a + (b - m) ** 2, 0) / (xs.length - 1));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/html/cells.ts
|
|
199
|
+
var cellValue = (row, key) => row[key];
|
|
200
|
+
var blankOr = (v) => isBlank(v) ? "" : escapeHtml(v);
|
|
201
|
+
var tableBg = (ctx) => ctx.theme.tokens.bg === "transparent" ? "#ffffff" : hex6(ctx.theme.tokens.bg);
|
|
202
|
+
function markImg(src, alt, heightPx, teamId) {
|
|
203
|
+
return `<img class="sdvt-mark" src="${escapeAttr(src)}" alt="${escapeAttr(alt)}" style="height:${heightPx}px"${teamId ? ` data-sdvplot-team="${escapeAttr(teamId)}"` : ""}>`;
|
|
204
|
+
}
|
|
205
|
+
function readableInk(primary, secondary, background, target = 4.5) {
|
|
206
|
+
const toward = onColor(background);
|
|
207
|
+
for (const c of [primary, secondary]) if (c !== void 0 && contrast(c, background) >= target) return c;
|
|
208
|
+
for (let i = 0; i < 20; i++) {
|
|
209
|
+
const c = mix(toward, primary, Math.max(0.95 + i * -0.05, 0));
|
|
210
|
+
if (contrast(c, background) >= target) return c;
|
|
211
|
+
}
|
|
212
|
+
return toward;
|
|
213
|
+
}
|
|
214
|
+
function checkHeight(h, kind) {
|
|
215
|
+
if (!(Number.isFinite(h) && h >= 1))
|
|
216
|
+
throw new TableSpecError(`${kind} height must be a number of pixels >= 1, got ${String(h)}`);
|
|
217
|
+
return h;
|
|
218
|
+
}
|
|
219
|
+
function teamIdsOf(spec, rows) {
|
|
220
|
+
const out = /* @__PURE__ */ new Map();
|
|
221
|
+
for (const c of spec.columns) {
|
|
222
|
+
if (c.kind !== "logo" && c.kind !== "wordmark" && c.kind !== "mergeStackTeamColor" && c.kind !== "teamColorBar" && c.kind !== "teamColorBg")
|
|
223
|
+
continue;
|
|
224
|
+
const from = c.kind === "mergeStackTeamColor" ? c.team : c.key;
|
|
225
|
+
const values = rows.map((r) => {
|
|
226
|
+
const v = cellValue(r, from);
|
|
227
|
+
return isBlank(v) ? null : String(v).trim();
|
|
228
|
+
});
|
|
229
|
+
out.set(
|
|
230
|
+
c.key,
|
|
231
|
+
resolveSync(values, c.league, {
|
|
232
|
+
...c.season !== void 0 ? { season: c.season } : {},
|
|
233
|
+
...c.idSystem ? { idSystem: c.idSystem } : {},
|
|
234
|
+
strict: c.strict ?? false
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
var pillLabel = (col, v) => formatValue(
|
|
241
|
+
v !== null && col.formatType === "percent" && col.scalePercent ? v * 100 : v,
|
|
242
|
+
col.digits,
|
|
243
|
+
col.formatType,
|
|
244
|
+
col.suffix
|
|
245
|
+
);
|
|
246
|
+
var isNa = (col, v) => isBlank(v) || col.naStrings.some((n) => col.ignoreCase ? n.toLowerCase() === String(v).toLowerCase() : n === String(v));
|
|
247
|
+
var isScaled = (col) => col.kind === "colorPills" || col.kind === "colorRanks" || col.kind === "percentileBar";
|
|
248
|
+
function columnScales(spec, rows, warnFn, domainRows = rows) {
|
|
249
|
+
const at = domainRows === rows ? null : new Map(domainRows.map((r, j) => [r, j]));
|
|
250
|
+
const missing = at !== null && rows.some((r) => !at.has(r));
|
|
251
|
+
const out = /* @__PURE__ */ new Map();
|
|
252
|
+
for (const col of spec.columns) {
|
|
253
|
+
if (!isScaled(col)) continue;
|
|
254
|
+
if (missing)
|
|
255
|
+
warnFn(
|
|
256
|
+
`sdvtables:domainRows:${col.key}`,
|
|
257
|
+
`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`
|
|
258
|
+
);
|
|
259
|
+
if (col.domain === void 0 && domainRows.length === 0) continue;
|
|
260
|
+
const nums = domainRows.map((r) => toNumber(cellValue(r, col.key)));
|
|
261
|
+
let values = nums;
|
|
262
|
+
let own = (n) => n;
|
|
263
|
+
if (col.kind === "colorPills" && col.fillType === "rank") {
|
|
264
|
+
values = averageRanks(nums, col.rankOrder === "desc");
|
|
265
|
+
own = () => null;
|
|
266
|
+
}
|
|
267
|
+
if (col.kind === "percentileBar") {
|
|
268
|
+
const present = nums.filter((n) => n !== null);
|
|
269
|
+
const [lo, hi] = col.domain;
|
|
270
|
+
const scale = col.scale;
|
|
271
|
+
const proportion = scale === "auto" && present.length > 0 && present.every((n) => n >= 0 && n <= 1) && hi > 1;
|
|
272
|
+
own = (n) => n === null ? null : typeof scale === "number" ? n * scale : proportion ? lo + n * (hi - lo) : n;
|
|
273
|
+
values = nums.map(own);
|
|
274
|
+
}
|
|
275
|
+
const domain = col.domain ?? domainOf([values]);
|
|
276
|
+
const fmt = () => `(${naturalDigits(domain[0])} to ${naturalDigits(domain[1])})`;
|
|
277
|
+
if (col.kind === "colorPills" && col.domain === void 0)
|
|
278
|
+
warnFn(
|
|
279
|
+
`sdvtables:domain:${col.key}:${domain.join(",")}`,
|
|
280
|
+
`no domain given, so the colors span the observed range ${fmt()}; set domain to compare colors across tables or columns`
|
|
281
|
+
);
|
|
282
|
+
const palette = col.palette.map((c) => hex6(c));
|
|
283
|
+
const color = ramp(col.reverse ? [...palette].reverse() : palette, domain);
|
|
284
|
+
const outside = values.filter((v) => v !== null && color(v) === null).length;
|
|
285
|
+
if (col.kind === "colorPills" && outside > 0)
|
|
286
|
+
warnFn(
|
|
287
|
+
`sdvtables:outside:${col.key}:${outside}:${domain.join(",")}`,
|
|
288
|
+
`${outside} value(s) fall outside the domain ${fmt()} and are drawn grey`
|
|
289
|
+
);
|
|
290
|
+
const shown = at === null ? values : rows.map((r) => {
|
|
291
|
+
const j = at.get(r);
|
|
292
|
+
return j === void 0 ? own(toNumber(cellValue(r, col.key))) : values[j] ?? null;
|
|
293
|
+
});
|
|
294
|
+
const shownNums = at === null ? nums : rows.map((r) => toNumber(cellValue(r, col.key)));
|
|
295
|
+
const labelWidth = col.kind === "colorPills" ? Math.max(1, ...shownNums.map((n) => pillLabel(col, n).length)) : 1;
|
|
296
|
+
out.set(col.key, { domain, palette, reverse: col.reverse, values: shown, color, labelWidth });
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
function renderCell(col, row, i, ctx) {
|
|
301
|
+
switch (col.kind) {
|
|
302
|
+
case "text":
|
|
303
|
+
case "colorResults":
|
|
304
|
+
return blankOr(cellValue(row, col.key));
|
|
305
|
+
// ---- Task 6
|
|
306
|
+
case "num":
|
|
307
|
+
case "int": {
|
|
308
|
+
const n = toNumber(cellValue(row, col.key));
|
|
309
|
+
if (n === null) return "";
|
|
310
|
+
const sc = ctx.scaled.get(col.key);
|
|
311
|
+
if (sc) return escapeHtml(formatNumber(n / sc.divisor, { digits: sc.decimals, big: true }));
|
|
312
|
+
return escapeHtml(
|
|
313
|
+
col.kind === "int" ? formatNumber(n, { digits: 0, big: true }) : formatNumber(n, {
|
|
314
|
+
...col.digits !== void 0 ? { digits: col.digits } : {},
|
|
315
|
+
big: col.big ?? false,
|
|
316
|
+
prefix: col.prefix ?? "",
|
|
317
|
+
suffix: col.suffix ?? "",
|
|
318
|
+
forceSign: col.forceSign ?? false
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
case "pct": {
|
|
323
|
+
const n = toNumber(cellValue(row, col.key));
|
|
324
|
+
return n === null ? "" : escapeHtml(`${formatNumber(col.scale ? n * 100 : n, { digits: col.digits })}%`);
|
|
325
|
+
}
|
|
326
|
+
case "rank": {
|
|
327
|
+
const n = toNumber(cellValue(row, col.key));
|
|
328
|
+
if (n === null) return "";
|
|
329
|
+
const o = ordinal(n);
|
|
330
|
+
const num = String(Math.trunc(n));
|
|
331
|
+
return col.superscript ? `${num}<sup style="font-size:${escapeAttr(cssValue(col.suffixSize, "rank suffixSize"))}">${o.slice(num.length)}</sup>` : escapeHtml(o);
|
|
332
|
+
}
|
|
333
|
+
case "delta": {
|
|
334
|
+
const a = toNumber(cellValue(row, col.key));
|
|
335
|
+
const b = toNumber(cellValue(row, col.to));
|
|
336
|
+
if (a === null || b === null) return "";
|
|
337
|
+
const d = col.percent ? a === 0 ? null : (b - a) / a : b - a;
|
|
338
|
+
if (d === null || !Number.isFinite(d)) return "";
|
|
339
|
+
const shown = col.arrows ? Math.abs(d) : d;
|
|
340
|
+
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 ? "%" : ""}`;
|
|
341
|
+
const color = !col.color ? null : d > 0 ? col.colorPositive : d < 0 ? col.colorNegative : col.colorNeutral ?? null;
|
|
342
|
+
return color ? `<span style="color:${escapeAttr(cssValue(color, "delta color"))}">${escapeHtml(text)}</span>` : escapeHtml(text);
|
|
343
|
+
}
|
|
344
|
+
case "tally": {
|
|
345
|
+
const ns = col.keys.map((k) => toNumber(cellValue(row, k)));
|
|
346
|
+
if (ns.some((n) => n === null)) {
|
|
347
|
+
const v = cellValue(row, col.key);
|
|
348
|
+
return blankOr(v);
|
|
349
|
+
}
|
|
350
|
+
const xs = ns;
|
|
351
|
+
if (col.share && !(Number.isInteger(col.shareOf) && col.shareOf >= 0 && col.shareOf < xs.length))
|
|
352
|
+
throw new TableSpecError(`tally shareOf must index (from 0) one of ${col.keys.join(", ")}`);
|
|
353
|
+
const text = xs.map((n) => naturalDigits(n)).join(col.separator);
|
|
354
|
+
const total = xs.reduce((s, n) => s + n, 0);
|
|
355
|
+
if (!col.share || total === 0) return escapeHtml(text);
|
|
356
|
+
return escapeHtml(
|
|
357
|
+
`${text}${col.sharePrefix}${formatNumber((xs[col.shareOf] ?? 0) / total * 100, { digits: col.shareDecimals })}%${col.shareSuffix}`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
// ---- Task 7
|
|
361
|
+
case "logo":
|
|
362
|
+
case "wordmark": {
|
|
363
|
+
const raw = cellValue(row, col.key);
|
|
364
|
+
const text = blankOr(raw);
|
|
365
|
+
const id = ctx.teamIds.get(col.key)?.[i];
|
|
366
|
+
if (id === void 0) return text;
|
|
367
|
+
const url = logoUrlSync(id, col.league, {
|
|
368
|
+
...col.season !== void 0 ? { season: col.season } : {},
|
|
369
|
+
...col.variant ? { variant: col.variant } : {},
|
|
370
|
+
markType: col.kind,
|
|
371
|
+
idSystem: "team_id"
|
|
372
|
+
});
|
|
373
|
+
if (!url) return text;
|
|
374
|
+
return markImg(url, teamNameSync(col.league, id) ?? String(raw), checkHeight(col.height, col.kind), id) + (col.kind === "logo" && col.includeName ? text : "");
|
|
375
|
+
}
|
|
376
|
+
case "headshot": {
|
|
377
|
+
const raw = cellValue(row, col.key);
|
|
378
|
+
if (isBlank(raw)) return "";
|
|
379
|
+
const url = headshotUrl(String(raw).trim(), col.league, { idSystem: col.idSystem });
|
|
380
|
+
return url ? markImg(url, String(raw).trim(), checkHeight(col.height, "headshot")) : blankOr(raw);
|
|
381
|
+
}
|
|
382
|
+
case "mergeStackTeamColor": {
|
|
383
|
+
const id = ctx.teamIds.get(col.key)?.[i];
|
|
384
|
+
const bg = col.background ? solid(col.background) : tableBg(ctx);
|
|
385
|
+
const p = id === void 0 ? void 0 : teamColorsSync(col.league, id, { idSystem: "team_id" });
|
|
386
|
+
const s = id === void 0 ? void 0 : teamColorsSync(col.league, id, { which: "secondary", idSystem: "team_id" });
|
|
387
|
+
const ink = readableInk(p ?? "#bebebe", s, bg);
|
|
388
|
+
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>
|
|
389
|
+
<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>`;
|
|
390
|
+
}
|
|
391
|
+
case "teamColorBar":
|
|
392
|
+
case "teamColorBg":
|
|
393
|
+
return blankOr(cellValue(row, col.key));
|
|
394
|
+
// the color lives on the <td> (kindCellStyle)
|
|
395
|
+
// ---- Task 8
|
|
396
|
+
case "colorPills": {
|
|
397
|
+
const sc = ctx.scales.get(col.key);
|
|
398
|
+
if (!sc) return "";
|
|
399
|
+
const pillHeight = checkPx(col.pillHeight, "colorPills pillHeight");
|
|
400
|
+
const outlineWidth = checkPx(col.outlineWidth, "colorPills outlineWidth");
|
|
401
|
+
const s = sc.values[i] ?? null;
|
|
402
|
+
const width = sc.labelWidth;
|
|
403
|
+
let fill;
|
|
404
|
+
let text;
|
|
405
|
+
if (s === null) {
|
|
406
|
+
if (col.naColor === void 0) return "";
|
|
407
|
+
fill = col.naColor;
|
|
408
|
+
text = "";
|
|
409
|
+
} else {
|
|
410
|
+
fill = sc.color(s) ?? "#808080";
|
|
411
|
+
text = pillLabel(col, toNumber(cellValue(row, col.key)));
|
|
412
|
+
}
|
|
413
|
+
const ink = col.textColor ?? onColor(solid(fill, tableBg(ctx)));
|
|
414
|
+
const outline = col.outlineColor !== void 0 ? `;border:${outlineWidth}px solid ${escapeAttr(cssValue(col.outlineColor, "colorPills outlineColor"))}` : "";
|
|
415
|
+
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>`;
|
|
416
|
+
}
|
|
417
|
+
case "colorRanks":
|
|
418
|
+
case "highlight":
|
|
419
|
+
return blankOr(cellValue(row, col.key));
|
|
420
|
+
// the fill lives on the <td> (kindCellStyle)
|
|
421
|
+
case "highlightNa":
|
|
422
|
+
return isNa(col, cellValue(row, col.key)) ? escapeHtml(col.missingText ?? "") : blankOr(cellValue(row, col.key));
|
|
423
|
+
case "percentileBar": {
|
|
424
|
+
const sc = ctx.scales.get(col.key);
|
|
425
|
+
const v = sc?.values[i] ?? null;
|
|
426
|
+
const th = checkPx(col.trackHeight, "percentileBar trackHeight");
|
|
427
|
+
const m = checkPx(col.markerSize, "percentileBar markerSize");
|
|
428
|
+
const fontSize = col.fontSize === void 0 ? m / 2 : checkPx(col.fontSize, "percentileBar fontSize");
|
|
429
|
+
const ringWidth = checkPx(col.ringWidth, "percentileBar ringWidth");
|
|
430
|
+
const trackColor = escapeAttr(cssValue(col.trackColor, "percentileBar trackColor"));
|
|
431
|
+
const textColor = escapeAttr(cssValue(col.textColor, "percentileBar textColor"));
|
|
432
|
+
if (sc === void 0 || v === null) {
|
|
433
|
+
const seg = `<div style="flex:1;height:${th}px;background:${escapeAttr(cssValue(col.naTrackColor ?? col.trackColor, "percentileBar naTrackColor"))};border-radius:${th}px"></div>`;
|
|
434
|
+
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>`;
|
|
435
|
+
}
|
|
436
|
+
const [lo, hi] = sc.domain;
|
|
437
|
+
const clamped = Math.min(hi, Math.max(lo, v));
|
|
438
|
+
const p = Number((hi === lo ? 50 : (clamped - lo) / (hi - lo) * 100).toFixed(4));
|
|
439
|
+
const color = sc.color(clamped) ?? trackColor;
|
|
440
|
+
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>`;
|
|
441
|
+
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>`;
|
|
442
|
+
const ring = col.ringColor !== void 0 ? `;box-shadow:0 0 0 ${ringWidth}px ${escapeAttr(cssValue(col.ringColor, "percentileBar ringColor"))}` : "";
|
|
443
|
+
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>`;
|
|
444
|
+
return `<div class="sdvt-pbar" style="position:relative;height:${m}px;width:100%">${track}${fill}${marker}</div>`;
|
|
445
|
+
}
|
|
446
|
+
case "indicatorBox": {
|
|
447
|
+
const on = col.truthy.includes(cellValue(row, col.key));
|
|
448
|
+
if (col.showOnly === "filled" && !on || col.showOnly === "neutral" && on) return "";
|
|
449
|
+
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>`;
|
|
450
|
+
}
|
|
451
|
+
case "image": {
|
|
452
|
+
const v = cellValue(row, col.key);
|
|
453
|
+
if (isBlank(v)) return "";
|
|
454
|
+
const alt = col.alt ? cellValue(row, col.alt) : "";
|
|
455
|
+
return markImg(
|
|
456
|
+
String(v),
|
|
457
|
+
isBlank(alt) ? "" : String(alt),
|
|
458
|
+
checkHeight(Number.parseFloat(col.height), "image")
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
default: {
|
|
462
|
+
const bad = col;
|
|
463
|
+
throw new TableSpecError(`unknown column kind ${String(bad.kind)}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function kindCellStyle(col, row, i, ctx) {
|
|
468
|
+
switch (col.kind) {
|
|
469
|
+
case "teamColorBar":
|
|
470
|
+
case "teamColorBg": {
|
|
471
|
+
const id = ctx.teamIds.get(col.key)?.[i];
|
|
472
|
+
const color = (id === void 0 ? void 0 : teamColorsSync(col.league, id, { which: col.which, idSystem: "team_id" })) ?? col.naColor;
|
|
473
|
+
if (col.kind === "teamColorBar")
|
|
474
|
+
return styleAttr({ "border-left": `${col.barWidth}px solid ${color}` });
|
|
475
|
+
const a = Math.round(Math.min(1, Math.max(0, col.alpha)) * 255).toString(16).padStart(2, "0");
|
|
476
|
+
return styleAttr({ "background-color": `${hex6(color, { dropAlpha: true })}${a}` });
|
|
477
|
+
}
|
|
478
|
+
case "colorRanks": {
|
|
479
|
+
const sc = ctx.scales.get(col.key);
|
|
480
|
+
const s = sc?.values[i] ?? null;
|
|
481
|
+
const fill = (s === null ? null : sc?.color(s)) ?? "#ffffff";
|
|
482
|
+
return styleAttr({ "background-color": fill, color: onColor(solid(fill, tableBg(ctx))) });
|
|
483
|
+
}
|
|
484
|
+
case "highlight":
|
|
485
|
+
return matches(col.when, row) ? styleAttr({
|
|
486
|
+
"background-color": col.fill,
|
|
487
|
+
color: col.textColor,
|
|
488
|
+
"font-weight": col.bold ? "bold" : void 0
|
|
489
|
+
}) : "";
|
|
490
|
+
case "highlightNa":
|
|
491
|
+
return isNa(col, cellValue(row, col.key)) ? styleAttr({
|
|
492
|
+
"background-color": col.fill,
|
|
493
|
+
color: col.textColor,
|
|
494
|
+
"font-weight": col.bold ? "bold" : void 0,
|
|
495
|
+
"font-style": col.italic ? "italic" : void 0
|
|
496
|
+
}) : "";
|
|
497
|
+
default:
|
|
498
|
+
return "";
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/html/social-icons.ts
|
|
503
|
+
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>';
|
|
504
|
+
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>';
|
|
505
|
+
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>';
|
|
506
|
+
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>';
|
|
507
|
+
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>';
|
|
508
|
+
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>';
|
|
509
|
+
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>';
|
|
510
|
+
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>';
|
|
511
|
+
var SOCIAL_ICONS = {
|
|
512
|
+
x: X,
|
|
513
|
+
twitter: X,
|
|
514
|
+
ig: IG,
|
|
515
|
+
bsky: BSKY,
|
|
516
|
+
gh: GH,
|
|
517
|
+
yt: YT,
|
|
518
|
+
fb: FB,
|
|
519
|
+
web: WEB,
|
|
520
|
+
website: WEB,
|
|
521
|
+
link: WEB,
|
|
522
|
+
email: EMAIL,
|
|
523
|
+
mail: EMAIL
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
// src/html/svg.ts
|
|
527
|
+
var halfEven = (x) => {
|
|
528
|
+
const r = Math.round(x);
|
|
529
|
+
return String(Math.abs(x % 1) === 0.5 && r % 2 !== 0 ? r - 1 : r);
|
|
530
|
+
};
|
|
531
|
+
function cutlineSvg(text, color, size) {
|
|
532
|
+
const t = text.toUpperCase();
|
|
533
|
+
const tracking = 1.1;
|
|
534
|
+
const esc = t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
535
|
+
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>`;
|
|
536
|
+
return `data:image/svg+xml;charset=utf-8,${pyQuote(svg)}`;
|
|
537
|
+
}
|
|
538
|
+
var pyQuote = (x) => encodeURIComponent(x).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
539
|
+
function watermarkSvg(text, color, opacity, angle, font) {
|
|
540
|
+
const label = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
541
|
+
const size = 100;
|
|
542
|
+
const textW = Math.max(label.length * size * 0.62, size);
|
|
543
|
+
const textH = size * 1.3;
|
|
544
|
+
const rad = Math.abs(angle) * Math.PI / 180;
|
|
545
|
+
const w = Math.ceil(textW * Math.cos(rad) + textH * Math.sin(rad)) + 4;
|
|
546
|
+
const h = Math.ceil(textW * Math.sin(rad) + textH * Math.cos(rad)) + 4;
|
|
547
|
+
const rot = angle !== 0 ? ` transform="rotate(${angle} ${w / 2} ${h / 2})"` : "";
|
|
548
|
+
const attr = (v) => v.replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """);
|
|
549
|
+
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>`;
|
|
550
|
+
return [`data:image/svg+xml,${pyQuote(svg)}`, h > w];
|
|
551
|
+
}
|
|
552
|
+
var cssStr = (v) => v.replace(/[\\"\n\r<]/g, (c) => `\\${c.charCodeAt(0).toString(16)} `);
|
|
553
|
+
|
|
554
|
+
// src/html/decorations.ts
|
|
555
|
+
function strwrap(words, width) {
|
|
556
|
+
const w = Math.max(width - 1, 1);
|
|
557
|
+
const out = [];
|
|
558
|
+
let cur = "";
|
|
559
|
+
for (const word of words) {
|
|
560
|
+
if (cur && cur.length + 1 + word.length > w) {
|
|
561
|
+
out.push(cur);
|
|
562
|
+
cur = word;
|
|
563
|
+
} else cur = cur ? `${cur} ${word}` : word;
|
|
564
|
+
}
|
|
565
|
+
return cur ? [...out, cur] : out;
|
|
566
|
+
}
|
|
567
|
+
function wrapLabel(text, width, balance) {
|
|
568
|
+
const words = text.split(/\s+/).filter((x) => x !== "");
|
|
569
|
+
if (words.length <= 1) return [text];
|
|
570
|
+
const greedy = strwrap(words, width);
|
|
571
|
+
if (greedy.length <= 1) return [text];
|
|
572
|
+
if (!balance) return greedy;
|
|
573
|
+
const target = Math.ceil(words.reduce((s, x) => s + x.length + 1, 0) / greedy.length);
|
|
574
|
+
const lines = [];
|
|
575
|
+
let cur = "";
|
|
576
|
+
for (const word of words) {
|
|
577
|
+
const cand = cur ? `${cur} ${word}` : word;
|
|
578
|
+
if (cand.length > target && cur) {
|
|
579
|
+
lines.push(cur);
|
|
580
|
+
cur = word;
|
|
581
|
+
} else cur = cand;
|
|
582
|
+
}
|
|
583
|
+
return cur ? [...lines, cur] : lines;
|
|
584
|
+
}
|
|
585
|
+
function groupIndexOf(rows, key) {
|
|
586
|
+
const seen = /* @__PURE__ */ new Map();
|
|
587
|
+
return rows.map((r) => {
|
|
588
|
+
const g = cellValue(r, key);
|
|
589
|
+
if (!seen.has(g)) seen.set(g, seen.size);
|
|
590
|
+
return seen.get(g);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
var isList = (p) => Array.isArray(p);
|
|
594
|
+
function textStyleAttr(ts) {
|
|
595
|
+
if (!ts) return "";
|
|
596
|
+
const s = styleAttr({
|
|
597
|
+
color: ts.color,
|
|
598
|
+
"font-size": ts.size,
|
|
599
|
+
"font-weight": ts.weight,
|
|
600
|
+
"font-family": ts.font ? fontStack(checkFamily(ts.font)) : void 0,
|
|
601
|
+
"text-transform": ts.transform === "uppercase" ? "uppercase" : void 0,
|
|
602
|
+
"font-style": ts.style === "italic" ? "italic" : void 0
|
|
603
|
+
});
|
|
604
|
+
return s ? ` style="${s}"` : "";
|
|
605
|
+
}
|
|
606
|
+
function checkFamily(f) {
|
|
607
|
+
if (!/^[\w .-]+$/.test(f))
|
|
608
|
+
throw new TableSpecError(`font family "${f}" may only hold letters, digits, spaces, "." "-" "_"`);
|
|
609
|
+
return f;
|
|
610
|
+
}
|
|
611
|
+
var checkLocation = (l, arg) => {
|
|
612
|
+
if (l !== "top" && l !== "bottom")
|
|
613
|
+
throw new TableSpecError(`${arg} location must be top or bottom, not ${JSON.stringify(l)}`);
|
|
614
|
+
return l;
|
|
615
|
+
};
|
|
616
|
+
var ALIGN = /* @__PURE__ */ new Set(["left", "center", "right"]);
|
|
617
|
+
var checkAlign = (a, arg) => {
|
|
618
|
+
if (!ALIGN.has(a)) throw new TableSpecError(`${arg} must be left, center or right, not "${a}"`);
|
|
619
|
+
return a;
|
|
620
|
+
};
|
|
621
|
+
var SCALE_NAMES = {
|
|
622
|
+
1e3: ["thousands", "(000s)"],
|
|
623
|
+
1e6: ["millions", "(millions)"],
|
|
624
|
+
1e9: ["billions", "(billions)"],
|
|
625
|
+
1e12: ["trillions", "(trillions)"]
|
|
626
|
+
};
|
|
627
|
+
function applyDecorations(spec, rows, ctx) {
|
|
628
|
+
const none = () => "";
|
|
629
|
+
const out = {
|
|
630
|
+
caption: "",
|
|
631
|
+
headRows: "",
|
|
632
|
+
foot: [],
|
|
633
|
+
before: "",
|
|
634
|
+
after: "",
|
|
635
|
+
css: [],
|
|
636
|
+
fonts: [],
|
|
637
|
+
hiddenColumns: /* @__PURE__ */ new Set(),
|
|
638
|
+
labelText: /* @__PURE__ */ new Map(),
|
|
639
|
+
label: (_c, t) => escapeHtml(t),
|
|
640
|
+
labelSuffix: none,
|
|
641
|
+
labelStyle: none,
|
|
642
|
+
rowClass: none,
|
|
643
|
+
rowStyle: none,
|
|
644
|
+
groupRowClass: none,
|
|
645
|
+
cellStyle: none,
|
|
646
|
+
cellSuffix: none
|
|
647
|
+
};
|
|
648
|
+
const title = spec.decorations.find(
|
|
649
|
+
(d) => d.type === "title"
|
|
650
|
+
);
|
|
651
|
+
const subtitle = spec.decorations.find(
|
|
652
|
+
(d) => d.type === "subtitle"
|
|
653
|
+
);
|
|
654
|
+
if (title || subtitle)
|
|
655
|
+
out.caption = `${title ? `<span class="sdvt-title">${escapeHtml(title.text)}</span>` : ""}${subtitle ? `<span class="sdvt-subtitle">${escapeHtml(subtitle.text)}</span>` : ""}`;
|
|
656
|
+
const rowStyles = /* @__PURE__ */ new Map();
|
|
657
|
+
const rowClasses = /* @__PURE__ */ new Map();
|
|
658
|
+
const cellStyles = /* @__PURE__ */ new Map();
|
|
659
|
+
const addRow = (i, decl) => {
|
|
660
|
+
rowStyles.set(i, { ...rowStyles.get(i), ...decl });
|
|
661
|
+
};
|
|
662
|
+
const addClass = (i, c) => {
|
|
663
|
+
rowClasses.set(i, [...rowClasses.get(i) ?? [], c]);
|
|
664
|
+
};
|
|
665
|
+
const addCell = (i, key, decl) => {
|
|
666
|
+
cellStyles.set(`${i}:${key}`, { ...cellStyles.get(`${i}:${key}`), ...decl });
|
|
667
|
+
};
|
|
668
|
+
const fillCells = (i, decl) => {
|
|
669
|
+
for (const c of ctx.columns) addCell(i, c.key, decl);
|
|
670
|
+
};
|
|
671
|
+
const suffixes = /* @__PURE__ */ new Map();
|
|
672
|
+
const labelStyles = /* @__PURE__ */ new Map();
|
|
673
|
+
let recordedKey;
|
|
674
|
+
let cutCount = 0;
|
|
675
|
+
const bg = ctx.theme.tokens.bg === "transparent" ? "#ffffff" : hex6(ctx.theme.tokens.bg);
|
|
676
|
+
const matchesNone = (sel) => selectRows(sel, ctx.domainRows).length === 0;
|
|
677
|
+
for (const c of spec.columns)
|
|
678
|
+
if (c.kind === "colorResults") {
|
|
679
|
+
const [win, loss] = c.resultType === "binary" ? [1, 0] : ["W", "L"];
|
|
680
|
+
const eq = (v, want) => v === want || typeof v === "boolean" && Number(v) === want;
|
|
681
|
+
rows.forEach((r, i) => {
|
|
682
|
+
const v = cellValue(r, c.key);
|
|
683
|
+
if (isBlank(v)) return;
|
|
684
|
+
if (eq(v, win)) fillCells(i, { "background-color": c.winColor, color: c.winTextColor });
|
|
685
|
+
else if (eq(v, loss)) fillCells(i, { "background-color": c.lossColor, color: c.lossTextColor });
|
|
686
|
+
else if (c.tieColor !== void 0 && eq(v, c.tieValue))
|
|
687
|
+
fillCells(i, { "background-color": c.tieColor, color: c.tieTextColor });
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
for (const d of spec.decorations)
|
|
691
|
+
switch (d.type) {
|
|
692
|
+
case "sourceNote":
|
|
693
|
+
out.foot.push(d.unsafe ? d.html : escapeHtml(d.html));
|
|
694
|
+
break;
|
|
695
|
+
case "wrapLabels": {
|
|
696
|
+
const prev = out.label;
|
|
697
|
+
out.label = (col, text) => !d.columns || d.columns.includes(col.key) ? wrapLabel(text, d.width, d.balance).map(escapeHtml).join("<br>") : prev(col, text);
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
// ---- Task 9
|
|
701
|
+
case "groupStripes": {
|
|
702
|
+
if (!ctx.groupKey) throw new TableSpecError("groupStripes needs .groupBy(key)");
|
|
703
|
+
const on = (gi) => gi % 2 === (d.start === 2 ? 1 : 0);
|
|
704
|
+
groupIndexOf(rows, ctx.groupKey).forEach((g, i) => {
|
|
705
|
+
if (on(g)) addClass(i, "sdvt-gstripe");
|
|
706
|
+
});
|
|
707
|
+
out.css.push(
|
|
708
|
+
`${ctx.sel} tr.sdvt-gstripe td{background-color:${cssValue(d.color, "groupStripes color")}}`
|
|
709
|
+
);
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
712
|
+
case "rowAccent": {
|
|
713
|
+
if (d.side !== "left" && d.side !== "right")
|
|
714
|
+
throw new TableSpecError(`rowAccent side must be left or right, not ${JSON.stringify(d.side)}`);
|
|
715
|
+
const keyOf = (r) => {
|
|
716
|
+
const v = cellValue(r, d.key);
|
|
717
|
+
return isBlank(v) ? null : String(v);
|
|
718
|
+
};
|
|
719
|
+
const keys = rows.map(keyOf);
|
|
720
|
+
const levels = [...new Set(ctx.domainRows.map(keyOf).filter((k) => k !== null))].sort();
|
|
721
|
+
const pal = d.palette;
|
|
722
|
+
const colors = keys.map(
|
|
723
|
+
(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
|
|
724
|
+
);
|
|
725
|
+
const keep = d.rows ? new Set(selectRows(d.rows, rows, ctx.domainRows)) : null;
|
|
726
|
+
if (d.rows && keep?.size === 0 && matchesNone(d.rows)) {
|
|
727
|
+
ctx.warn(`sdvtables:rowAccent:${ctx.id}`, "rows matched no rows; the table is unchanged");
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
const unsafe = /* @__PURE__ */ new Set();
|
|
731
|
+
colors.forEach((c, i) => {
|
|
732
|
+
const fill = c ?? d.naColor;
|
|
733
|
+
if (fill === "transparent" || keep && !keep.has(i)) return;
|
|
734
|
+
if (pal === void 0 && c !== void 0 && !isCssValue(c)) unsafe.add(c);
|
|
735
|
+
else addRow(i, { [`border-${d.side}`]: `${d.width}px solid ${fill}` });
|
|
736
|
+
});
|
|
737
|
+
if (unsafe.size > 0)
|
|
738
|
+
ctx.warn(
|
|
739
|
+
`sdvtables:rowAccent:${ctx.id}:css:${[...unsafe].join(",")}`,
|
|
740
|
+
`${[...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`
|
|
741
|
+
);
|
|
742
|
+
if (d.hide) out.hiddenColumns.add(d.key);
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
case "boldRows":
|
|
746
|
+
for (const i of selectRows(d.rows, rows, ctx.domainRows)) {
|
|
747
|
+
addRow(i, { "font-weight": "bold" });
|
|
748
|
+
fillCells(i, {
|
|
749
|
+
color: d.textColor,
|
|
750
|
+
...d.highlightColor ? { "background-color": d.highlightColor } : {}
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
break;
|
|
754
|
+
case "spotlight": {
|
|
755
|
+
const lit = new Set(selectRows(d.rows, rows, ctx.domainRows));
|
|
756
|
+
if (lit.size === 0 && matchesNone(d.rows)) {
|
|
757
|
+
ctx.warn(`sdvtables:spotlight:${ctx.id}`, "rows matched no rows, so the table is unchanged");
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
const dim = d.dimColor === "auto" ? secondaryOn(bg, onColor(bg)) : d.dimColor;
|
|
761
|
+
const fill = {
|
|
762
|
+
...d.fill ? { "background-color": d.fill } : {},
|
|
763
|
+
...d.textColor ? { color: d.textColor } : {}
|
|
764
|
+
};
|
|
765
|
+
const look = { ...fill, ...d.bold ? { "font-weight": "bold" } : {} };
|
|
766
|
+
rows.forEach((_, i) => {
|
|
767
|
+
if (!lit.has(i)) {
|
|
768
|
+
if (dim) addRow(i, { color: dim });
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
if (d.accentColor) addRow(i, { "box-shadow": `inset ${d.accentWidth}px 0 0 ${d.accentColor}` });
|
|
772
|
+
if (!d.columns) {
|
|
773
|
+
fillCells(i, fill);
|
|
774
|
+
if (d.bold) addRow(i, { "font-weight": "bold" });
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
for (const c of ctx.columns)
|
|
778
|
+
if (d.columns.includes(c.key)) addCell(i, c.key, look);
|
|
779
|
+
else if (dim) addCell(i, c.key, { color: dim });
|
|
780
|
+
});
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
case "cutline": {
|
|
784
|
+
if (!d.after.every((a) => Number.isInteger(a)))
|
|
785
|
+
throw new TableSpecError(`cutline after must be whole row numbers, got ${d.after.join(", ")}`);
|
|
786
|
+
if (d.gap.length < 1 || d.gap.length > 2 || d.gap.some((g) => !(g >= 0)))
|
|
787
|
+
throw new TableSpecError("cutline gap must be one or two non-negative numbers");
|
|
788
|
+
const n = rows.length;
|
|
789
|
+
const dropped = d.after.filter((a) => a < 0 || a >= n);
|
|
790
|
+
if (dropped.length > 0)
|
|
791
|
+
ctx.warn(
|
|
792
|
+
`sdvtables:cutline:${ctx.id}:${dropped.join(",")}`,
|
|
793
|
+
`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`
|
|
794
|
+
);
|
|
795
|
+
const color = cssValue(d.color, "cutline color");
|
|
796
|
+
const style = cssValue(d.style, "cutline style");
|
|
797
|
+
const weight = checkPx(d.weight, "cutline weight");
|
|
798
|
+
const labelSize = checkPx(d.labelSize, "cutline labelSize");
|
|
799
|
+
const cn = cutCount++;
|
|
800
|
+
const above = d.gap[0] ?? 0;
|
|
801
|
+
const below = d.gap[d.gap.length - 1] ?? 0;
|
|
802
|
+
d.after.forEach((a, j) => {
|
|
803
|
+
if (a < 0 || a >= n) return;
|
|
804
|
+
const label = d.label && d.label.length > 0 ? d.label[j % d.label.length] ?? null : null;
|
|
805
|
+
const top = d.labelPosition === "below" || a === 0;
|
|
806
|
+
const labelRow = top ? a : a - 1;
|
|
807
|
+
addClass(a, `sdvt-cut-${cn}-${j}`);
|
|
808
|
+
out.css.push(`${ctx.sel} tr.sdvt-cut-${cn}-${j} td{border-top:${weight}px ${style} ${color}}`);
|
|
809
|
+
if (above > 0 && a > 0 && !(label && labelRow === a - 1)) {
|
|
810
|
+
addClass(a - 1, `sdvt-cut-${cn}-${j}-above`);
|
|
811
|
+
out.css.push(`${ctx.sel} tr.sdvt-cut-${cn}-${j}-above td{padding-bottom:${above}px}`);
|
|
812
|
+
}
|
|
813
|
+
if (below > 0 && !(label && labelRow === a))
|
|
814
|
+
out.css.push(`${ctx.sel} tr.sdvt-cut-${cn}-${j} td{padding-top:${below}px}`);
|
|
815
|
+
if (label) {
|
|
816
|
+
addClass(labelRow, `sdvt-cut-${cn}-${j}-label`);
|
|
817
|
+
out.css.push(
|
|
818
|
+
`${ctx.sel} tr.sdvt-cut-${cn}-${j}-label td{padding-${top ? "top" : "bottom"}:${labelSize + 13 + (top ? below : above)}px;background-color:transparent}`,
|
|
819
|
+
`${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"}}`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
case "borderGrid": {
|
|
826
|
+
const color = cssValue(d.color, "borderGrid color");
|
|
827
|
+
const b = `border-right:${checkPx(d.weight, "borderGrid weight")}px solid ${color}`;
|
|
828
|
+
out.css.push(
|
|
829
|
+
`${ctx.sel} td.sdvt-cell:not(:last-child){${b}}`,
|
|
830
|
+
`${ctx.sel} td.sdvt-cell{border-top-color:${color}}`
|
|
831
|
+
);
|
|
832
|
+
if (d.includeLabels) out.css.push(`${ctx.sel} th.sdvt-label:not(:last-child){${b}}`);
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
// ---- Task 10
|
|
836
|
+
case "titleHeader": {
|
|
837
|
+
for (const ts of [d.kickerStyle, d.titleStyle, d.subtitleStyle, d.dateStyle])
|
|
838
|
+
if (ts?.font)
|
|
839
|
+
out.fonts.push({
|
|
840
|
+
family: checkFamily(ts.font),
|
|
841
|
+
weights: [typeof ts.weight === "number" ? ts.weight : 400]
|
|
842
|
+
});
|
|
843
|
+
out.css.push(
|
|
844
|
+
`${ctx.sel} .sdvt-kicker{font-size:0.75em;font-weight:700;color:#C84630;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:0.15em}`,
|
|
845
|
+
`${ctx.sel} .sdvt-date{font-size:0.85em;font-weight:400;color:#8A8A8A;margin-top:0.15em}`
|
|
846
|
+
);
|
|
847
|
+
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>` : ""}`;
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
case "caption538": {
|
|
851
|
+
const align = checkAlign(d.align, "caption538 align");
|
|
852
|
+
if (!d.top && !d.bottom)
|
|
853
|
+
throw new TableSpecError("caption538: nothing to caption; pass top, bottom, or both");
|
|
854
|
+
const rule = d.ruleColor ?? ctx.theme.tokens.text;
|
|
855
|
+
if (d.top)
|
|
856
|
+
out.foot.push(
|
|
857
|
+
`<div class="sdvt-cap538" style="${styleAttr({ "border-bottom": `${d.ruleWidth}px solid ${rule}`, "font-size": `${d.size}px` })}">${escapeHtml(d.top)}</div>`
|
|
858
|
+
);
|
|
859
|
+
if (d.bottom)
|
|
860
|
+
out.foot.push(
|
|
861
|
+
`<div class="sdvt-cap538" style="${styleAttr({ "text-align": align })}">${escapeHtml(d.bottom)}</div>`
|
|
862
|
+
);
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
case "socialTag": {
|
|
866
|
+
const align = checkAlign(d.align, "socialTag align");
|
|
867
|
+
const entries = Object.entries(d.accounts);
|
|
868
|
+
if (entries.length === 0)
|
|
869
|
+
throw new TableSpecError("socialTag accounts must be a non-empty {platform: handle} object");
|
|
870
|
+
const parts = entries.map(([k, handle]) => {
|
|
871
|
+
const svg = SOCIAL_ICONS[k.toLowerCase()];
|
|
872
|
+
if (!svg)
|
|
873
|
+
throw new TableSpecError(
|
|
874
|
+
`socialTag: unknown platform "${k}"; one of ${Object.keys(SOCIAL_ICONS).join(", ")}`
|
|
875
|
+
);
|
|
876
|
+
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>`;
|
|
877
|
+
});
|
|
878
|
+
out.css.push(
|
|
879
|
+
`${ctx.sel} .sdvt-handle{display:inline-flex;align-items:center;gap:0.3em;white-space:nowrap}`
|
|
880
|
+
);
|
|
881
|
+
if (d.caption)
|
|
882
|
+
out.foot.push(
|
|
883
|
+
`<div class="sdvt-cap538" style="${styleAttr({ "border-bottom": `1px solid ${ctx.theme.tokens.text}`, "font-size": "12px" })}">${escapeHtml(d.caption)}</div>`
|
|
884
|
+
);
|
|
885
|
+
out.foot.push(
|
|
886
|
+
`<div class="sdvt-social" style="text-align:${align}">${parts.join(d.stack ? "<br>" : escapeHtml(d.separator))}</div>`
|
|
887
|
+
);
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
case "scaleNote": {
|
|
891
|
+
if (!(Number.isFinite(d.divisor) && d.divisor !== 0))
|
|
892
|
+
throw new TableSpecError(`scaleNote divisor must be a single non-zero number, got ${d.divisor}`);
|
|
893
|
+
if (d.columns.length === 0) throw new TableSpecError("scaleNote columns matched no columns");
|
|
894
|
+
for (const k of d.columns) ctx.scaled.set(k, { divisor: d.divisor, decimals: d.decimals });
|
|
895
|
+
const named = SCALE_NAMES[d.divisor];
|
|
896
|
+
const shown = Number.isInteger(d.divisor) ? formatNumber(d.divisor, { digits: 0, big: true }) : naturalDigits(d.divisor, true);
|
|
897
|
+
const note = d.note ?? (named ? `Figures in ${named[0]}.` : `Figures divided by ${shown}.`);
|
|
898
|
+
const suffix = d.labelSuffix ?? (named ? named[1] : `(\xF7${shown})`);
|
|
899
|
+
if (d.where !== "label") out.foot.push(escapeHtml(note));
|
|
900
|
+
if (d.where !== "sourceNote") {
|
|
901
|
+
const prev = out.labelSuffix;
|
|
902
|
+
out.labelSuffix = (key) => d.columns.includes(key) ? ` ${suffix}` : prev(key);
|
|
903
|
+
}
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
case "borderBars": {
|
|
907
|
+
if (d.side !== "top" && d.side !== "bottom")
|
|
908
|
+
throw new TableSpecError(`borderBars side must be top or bottom, not ${JSON.stringify(d.side)}`);
|
|
909
|
+
checkAlign(d.barAlign, "borderBars barAlign");
|
|
910
|
+
const barWidth = escapeAttr(cssValue(d.barWidth, "borderBars barWidth"));
|
|
911
|
+
const colors = d.colors.map((c) => escapeAttr(cssValue(c, "borderBars colors")));
|
|
912
|
+
if (d.imgAlign !== "left" && d.imgAlign !== "right")
|
|
913
|
+
throw new TableSpecError(
|
|
914
|
+
`borderBars imgAlign must be left or right, not ${JSON.stringify(d.imgAlign)}`
|
|
915
|
+
);
|
|
916
|
+
const barHeight = Number(d.barHeight);
|
|
917
|
+
if (!Number.isFinite(barHeight))
|
|
918
|
+
throw new TableSpecError(
|
|
919
|
+
`borderBars barHeight must be a number, got ${JSON.stringify(d.barHeight)}`
|
|
920
|
+
);
|
|
921
|
+
if (d.textAlign !== "left" && d.textAlign !== "right")
|
|
922
|
+
throw new TableSpecError(`borderBars textAlign must be left or right, not "${d.textAlign}"`);
|
|
923
|
+
const margin = {
|
|
924
|
+
left: "margin-left:0;margin-right:auto",
|
|
925
|
+
center: "margin-left:auto;margin-right:auto",
|
|
926
|
+
right: "margin-left:auto;margin-right:0"
|
|
927
|
+
}[d.barAlign];
|
|
928
|
+
let block;
|
|
929
|
+
if (!d.text && !d.img) {
|
|
930
|
+
const bars = colors.map((c) => `<div class="sdvt-bar" style="height:${barHeight}px;background-color:${c}"></div>`).join("");
|
|
931
|
+
block = `<div class="sdvt-bars-box" style="background-color:transparent;width:${barWidth};${margin}">${bars}</div>`;
|
|
932
|
+
} else {
|
|
933
|
+
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>";
|
|
934
|
+
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` })}">` : "";
|
|
935
|
+
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>`;
|
|
936
|
+
}
|
|
937
|
+
const wrapped = `<div class="sdvt-bars sdvt-bars-${d.side}">${block}</div>`;
|
|
938
|
+
if (d.side === "top") out.before += wrapped;
|
|
939
|
+
else out.after += wrapped;
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
942
|
+
case "watermark": {
|
|
943
|
+
if (!d.text === !d.image) throw new TableSpecError("watermark: supply exactly one of text or image");
|
|
944
|
+
let url;
|
|
945
|
+
let tall = false;
|
|
946
|
+
let extra = "";
|
|
947
|
+
const opacity = checkPx(d.opacity, "watermark opacity");
|
|
948
|
+
if (d.image) {
|
|
949
|
+
url = cssStr(d.image);
|
|
950
|
+
extra = `;opacity:${opacity}`;
|
|
951
|
+
} else [url, tall] = watermarkSvg(d.text ?? "", d.color, opacity, d.angle, d.font);
|
|
952
|
+
const size = cssValue(d.size, "watermark size");
|
|
953
|
+
out.css.push(
|
|
954
|
+
`${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}}`
|
|
955
|
+
);
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
case "font": {
|
|
959
|
+
const stack = fontStack(checkFamily(d.family));
|
|
960
|
+
if (d.google)
|
|
961
|
+
out.fonts.push({ family: d.family, weights: [typeof d.weight === "number" ? d.weight : 400] });
|
|
962
|
+
out.css.push(
|
|
963
|
+
`${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")}` : ""}}`
|
|
964
|
+
);
|
|
965
|
+
break;
|
|
966
|
+
}
|
|
967
|
+
// ---- Task 11
|
|
968
|
+
case "legendContinuous": {
|
|
969
|
+
const rec = ctx.recorded;
|
|
970
|
+
const palette = (d.palette ?? rec?.palette ?? RANK_PALETTE).map((c) => hex6(c));
|
|
971
|
+
let domain = d.domain ?? rec?.domain;
|
|
972
|
+
if (!domain && d.columns && ctx.domainRows.length > 0)
|
|
973
|
+
domain = domainOf(d.columns.map((k) => ctx.domainRows.map((r2) => toNumber(cellValue(r2, k)))));
|
|
974
|
+
if (!domain && ctx.domainRows.length === 0 && (d.columns || ctx.columns.some(isScaled))) break;
|
|
975
|
+
if (!domain)
|
|
976
|
+
throw new TableSpecError(
|
|
977
|
+
"legendContinuous: no recorded scale (color a column with colorPills/colorRanks/percentileBar first) and no domain or columns given"
|
|
978
|
+
);
|
|
979
|
+
if (d.nBins < 1) throw new TableSpecError("legendContinuous nBins must be at least 1");
|
|
980
|
+
const loc = checkLocation(d.location, "legendContinuous");
|
|
981
|
+
const swW = checkPx(d.swatchWidth, "legendContinuous swatchWidth");
|
|
982
|
+
const swH = checkPx(d.swatchHeight, "legendContinuous swatchHeight");
|
|
983
|
+
const rev = d.reverse ?? rec?.reverse ?? false;
|
|
984
|
+
const r = ramp(rev ? [...palette].reverse() : palette, domain);
|
|
985
|
+
const [lo, hi] = domain;
|
|
986
|
+
const sw = Array.from(
|
|
987
|
+
{ length: d.nBins },
|
|
988
|
+
(_, 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>`
|
|
989
|
+
);
|
|
990
|
+
const labels = d.labels ?? [lo, hi].map((v) => formatValue(v, d.digits, "comma", ""));
|
|
991
|
+
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>`;
|
|
992
|
+
if (loc === "top") out.before += block;
|
|
993
|
+
else out.after += block;
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
case "legendDiscrete": {
|
|
997
|
+
const key = d.key === "recorded" ? recordedKey : d.key;
|
|
998
|
+
if (!key)
|
|
999
|
+
throw new TableSpecError(
|
|
1000
|
+
'legendDiscrete("recorded") needs a .tiers() decoration before it (no recorded key)'
|
|
1001
|
+
);
|
|
1002
|
+
const loc = checkLocation(d.location, "legendDiscrete");
|
|
1003
|
+
const size = checkPx(d.swatchSize, "legendDiscrete swatchSize");
|
|
1004
|
+
const gap = checkPx(d.gap, "legendDiscrete gap");
|
|
1005
|
+
const items = Object.entries(key).map(
|
|
1006
|
+
([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>`
|
|
1007
|
+
);
|
|
1008
|
+
const align = checkAlign(d.align, "legendDiscrete align");
|
|
1009
|
+
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>`;
|
|
1010
|
+
if (loc === "top") out.before += block;
|
|
1011
|
+
else out.after += block;
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
case "significance": {
|
|
1015
|
+
if (d.levels.length !== d.symbols.length)
|
|
1016
|
+
throw new TableSpecError("significance levels and symbols must be the same length");
|
|
1017
|
+
if (d.levels.some((x, j) => j > 0 && x < (d.levels[j - 1] ?? x)))
|
|
1018
|
+
throw new TableSpecError("significance levels must be in ascending order, strictest first");
|
|
1019
|
+
for (const pair of d.pairs) {
|
|
1020
|
+
rows.forEach((r, i) => {
|
|
1021
|
+
const p = toNumber(cellValue(r, pair.p));
|
|
1022
|
+
if (p === null) return;
|
|
1023
|
+
const s = d.symbols[d.levels.findIndex((x) => p < x)];
|
|
1024
|
+
if (s) {
|
|
1025
|
+
const k = `${i}:${pair.estimate}`;
|
|
1026
|
+
suffixes.set(
|
|
1027
|
+
k,
|
|
1028
|
+
`${suffixes.get(k) ?? ""}${d.superscript ? `<sup style="font-size:0.7em">${escapeHtml(s)}</sup>` : escapeHtml(s)}`
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
if (d.hideP) out.hiddenColumns.add(pair.p);
|
|
1033
|
+
}
|
|
1034
|
+
if (d.note)
|
|
1035
|
+
out.foot.push(
|
|
1036
|
+
escapeHtml(d.levels.map((x, j) => `${d.symbols[j] ?? ""} p < ${naturalDigits(x)}`).join(", "))
|
|
1037
|
+
);
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
case "outliers": {
|
|
1041
|
+
if (d.method === "bounds" && !d.bounds)
|
|
1042
|
+
throw new TableSpecError("outliers bounds must be [lower, upper] when method is 'bounds'");
|
|
1043
|
+
const t = d.threshold ?? (d.method === "sd" ? 3 : 1.5);
|
|
1044
|
+
const shown = d.fill === void 0 ? null : solid(d.fill, bg);
|
|
1045
|
+
const ink = d.color ?? (shown === null || contrast("#B3261E", shown) >= 4.5 ? "#B3261E" : onColor(shown));
|
|
1046
|
+
let flagged = false;
|
|
1047
|
+
for (const k of d.columns) {
|
|
1048
|
+
const xs = rows.map((r) => toNumber(cellValue(r, k)));
|
|
1049
|
+
const nums = ctx.domainRows.map((r) => toNumber(cellValue(r, k))).filter((v) => v !== null);
|
|
1050
|
+
let lo = Number.NEGATIVE_INFINITY;
|
|
1051
|
+
let hi = Number.POSITIVE_INFINITY;
|
|
1052
|
+
if (d.method === "bounds") {
|
|
1053
|
+
lo = d.bounds?.[0] ?? lo;
|
|
1054
|
+
hi = d.bounds?.[1] ?? hi;
|
|
1055
|
+
} else if (nums.length >= 2 && d.method === "sd") {
|
|
1056
|
+
const m = nums.reduce((a, b) => a + b, 0) / nums.length;
|
|
1057
|
+
const s = sampleSd(nums);
|
|
1058
|
+
if (s !== 0) {
|
|
1059
|
+
lo = m - t * s;
|
|
1060
|
+
hi = m + t * s;
|
|
1061
|
+
}
|
|
1062
|
+
} else if (nums.length >= 2) {
|
|
1063
|
+
const q1 = quantile7(nums, 0.25);
|
|
1064
|
+
const q3 = quantile7(nums, 0.75);
|
|
1065
|
+
if (q3 !== q1) {
|
|
1066
|
+
lo = q1 - t * (q3 - q1);
|
|
1067
|
+
hi = q3 + t * (q3 - q1);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
xs.forEach((v, i) => {
|
|
1071
|
+
if (v === null || !(v < lo && d.side !== "high" || v > hi && d.side !== "low")) return;
|
|
1072
|
+
flagged = true;
|
|
1073
|
+
addCell(i, k, {
|
|
1074
|
+
color: ink,
|
|
1075
|
+
"font-weight": d.bold ? "bold" : void 0,
|
|
1076
|
+
"background-color": d.fill
|
|
1077
|
+
});
|
|
1078
|
+
if (d.symbol)
|
|
1079
|
+
suffixes.set(`${i}:${k}`, `${suffixes.get(`${i}:${k}`) ?? ""}${escapeHtml(d.symbol)}`);
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
if (flagged && d.note) {
|
|
1083
|
+
const tail = { both: "", high: " (high side only)", low: " (low side only)" }[d.side];
|
|
1084
|
+
out.foot.push(
|
|
1085
|
+
escapeHtml(
|
|
1086
|
+
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}.`
|
|
1087
|
+
)
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
case "marginalia": {
|
|
1093
|
+
const ink = onColor(bg);
|
|
1094
|
+
const color = d.color ?? secondaryOn(bg, ink);
|
|
1095
|
+
const rule = d.ruleColor ?? mix(bg, ink, 0.18);
|
|
1096
|
+
const align = checkAlign(d.align, "marginalia align");
|
|
1097
|
+
for (const k of d.columns) {
|
|
1098
|
+
out.labelText.set(k, d.label);
|
|
1099
|
+
if (d.width !== void 0)
|
|
1100
|
+
labelStyles.set(
|
|
1101
|
+
k,
|
|
1102
|
+
`width:${typeof d.width === "number" ? `${d.width}px` : escapeAttr(cssValue(d.width, "marginalia width"))}`
|
|
1103
|
+
);
|
|
1104
|
+
rows.forEach(
|
|
1105
|
+
(_, i) => addCell(i, k, {
|
|
1106
|
+
"font-style": d.italic ? "italic" : "normal",
|
|
1107
|
+
"font-size": d.size,
|
|
1108
|
+
color,
|
|
1109
|
+
"border-left": d.rule ? `1px solid ${rule}` : void 0,
|
|
1110
|
+
"text-align": align
|
|
1111
|
+
})
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
case "tiers": {
|
|
1117
|
+
if (!d.colors)
|
|
1118
|
+
throw new TableSpecError("tiers colors is missing; pass levels and colors as two lists");
|
|
1119
|
+
if (d.colors.length !== d.levels.length)
|
|
1120
|
+
throw new TableSpecError(
|
|
1121
|
+
`tiers levels and colors must be the same length: got ${d.levels.length} levels and ${d.colors.length} colors`
|
|
1122
|
+
);
|
|
1123
|
+
const fills = d.colors.map((c) => hex6(c));
|
|
1124
|
+
const tierOf = (r) => {
|
|
1125
|
+
const v = cellValue(r, d.tierKey);
|
|
1126
|
+
return isBlank(v) ? null : String(v);
|
|
1127
|
+
};
|
|
1128
|
+
const held = [...new Set(ctx.domainRows.map(tierOf).filter((t) => t !== null))];
|
|
1129
|
+
const missing = d.levels.filter((l) => !held.includes(l));
|
|
1130
|
+
if (missing.length > 0)
|
|
1131
|
+
ctx.warn(
|
|
1132
|
+
`sdvtables:tiers:${ctx.id}`,
|
|
1133
|
+
`tier(s) ${missing.join(", ")} are not in "${d.tierKey}", so they get no rows; it holds ${held.join(", ")}`
|
|
1134
|
+
);
|
|
1135
|
+
rows.forEach((r, i) => {
|
|
1136
|
+
const t = tierOf(r);
|
|
1137
|
+
const f = t === null ? void 0 : fills[d.levels.indexOf(t)];
|
|
1138
|
+
if (f) addCell(i, d.tierKey, { "background-color": f, color: onColor(f), "font-weight": "bold" });
|
|
1139
|
+
});
|
|
1140
|
+
recordedKey = Object.fromEntries(d.levels.map((l, j) => [l, fills[j] ?? ""]));
|
|
1141
|
+
break;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
out.cellSuffix = (i, key) => suffixes.get(`${i}:${key}`) ?? "";
|
|
1145
|
+
out.labelStyle = (key) => labelStyles.get(key) ?? "";
|
|
1146
|
+
out.css = out.css.map((r) => r.replace(/</g, "\\3c "));
|
|
1147
|
+
out.rowClass = (i) => (rowClasses.get(i) ?? []).map((c) => ` ${c}`).join("");
|
|
1148
|
+
out.rowStyle = (i) => {
|
|
1149
|
+
const s = rowStyles.get(i);
|
|
1150
|
+
return s ? styleAttr(s) : "";
|
|
1151
|
+
};
|
|
1152
|
+
out.cellStyle = (i, key) => {
|
|
1153
|
+
const s = cellStyles.get(`${i}:${key}`);
|
|
1154
|
+
return s ? styleAttr(s) : "";
|
|
1155
|
+
};
|
|
1156
|
+
return out;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// src/html/layout.ts
|
|
1160
|
+
function expandTiers(spec) {
|
|
1161
|
+
const d = spec.decorations.find((x) => x.type === "tiers");
|
|
1162
|
+
if (!d) return spec;
|
|
1163
|
+
const columns = spec.columns.map((c) => {
|
|
1164
|
+
if (!d.imageColumns.includes(c.key)) return { ...c, label: "" };
|
|
1165
|
+
return {
|
|
1166
|
+
key: c.key,
|
|
1167
|
+
label: "",
|
|
1168
|
+
kind: "image",
|
|
1169
|
+
height: d.imgHeight,
|
|
1170
|
+
...c.align ? { align: c.align } : {},
|
|
1171
|
+
...c.width ? { width: c.width } : {}
|
|
1172
|
+
};
|
|
1173
|
+
});
|
|
1174
|
+
return {
|
|
1175
|
+
...spec,
|
|
1176
|
+
theme: { name: "tier", density: spec.theme.density, options: { style: d.style } },
|
|
1177
|
+
columns
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
function snakeLayout(d, rows, headCells, ncol, rowHtml, trOf, gapTh, gapTd) {
|
|
1181
|
+
const blocks = snakeAlign(
|
|
1182
|
+
rows.map((row, i) => ({ row, i })),
|
|
1183
|
+
{ nCols: d.nCols, ...d.rowsPerCol !== void 0 ? { rowsPerCol: d.rowsPerCol } : {} }
|
|
1184
|
+
);
|
|
1185
|
+
if (blocks.length < 2 || rows.length === 0) return null;
|
|
1186
|
+
const per = blocks[0]?.length ?? 0;
|
|
1187
|
+
const blank = `<td class="sdvt-cell sdvt-blank">${escapeHtml(d.fill)}</td>`.repeat(ncol);
|
|
1188
|
+
const body = Array.from(
|
|
1189
|
+
{ length: per },
|
|
1190
|
+
(_, k) => trOf(
|
|
1191
|
+
blocks[0]?.[k]?.i ?? k,
|
|
1192
|
+
blocks.map((b) => {
|
|
1193
|
+
const e = b[k];
|
|
1194
|
+
return e ? rowHtml(e.row, e.i) : blank;
|
|
1195
|
+
}).join(gapTd)
|
|
1196
|
+
)
|
|
1197
|
+
);
|
|
1198
|
+
return { head: blocks.map(() => headCells).join(gapTh), body };
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// src/html/parts.ts
|
|
1202
|
+
function checkKeys(spec, rows) {
|
|
1203
|
+
const first = rows[0];
|
|
1204
|
+
if (first === void 0) return;
|
|
1205
|
+
const keys = new Set(Object.keys(first));
|
|
1206
|
+
for (const col of spec.columns) {
|
|
1207
|
+
const need = [
|
|
1208
|
+
col.key,
|
|
1209
|
+
..."to" in col ? [col.to] : [],
|
|
1210
|
+
..."keys" in col ? col.keys : [],
|
|
1211
|
+
..."stack" in col ? [col.stack, col.team] : []
|
|
1212
|
+
];
|
|
1213
|
+
for (const k of need)
|
|
1214
|
+
if (!keys.has(k))
|
|
1215
|
+
throw new TableSpecError(
|
|
1216
|
+
`column "${k}" (kind ${col.kind}) is not a key of the first row; keys are ${[...keys].join(", ")}`
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
function alignOf(col) {
|
|
1221
|
+
if (col.align) return col.align;
|
|
1222
|
+
switch (col.kind) {
|
|
1223
|
+
case "num":
|
|
1224
|
+
case "int":
|
|
1225
|
+
case "pct":
|
|
1226
|
+
case "rank":
|
|
1227
|
+
case "delta":
|
|
1228
|
+
case "tally":
|
|
1229
|
+
case "colorPills":
|
|
1230
|
+
case "colorRanks":
|
|
1231
|
+
return "right";
|
|
1232
|
+
case "logo":
|
|
1233
|
+
case "wordmark":
|
|
1234
|
+
case "headshot":
|
|
1235
|
+
case "indicatorBox":
|
|
1236
|
+
case "image":
|
|
1237
|
+
return "center";
|
|
1238
|
+
default:
|
|
1239
|
+
return "left";
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
function labelOf(col) {
|
|
1243
|
+
return col.label ?? col.key.replace(/_/g, " ").replace(/\b\w/g, (m) => m.toUpperCase());
|
|
1244
|
+
}
|
|
1245
|
+
function themeKey(ref) {
|
|
1246
|
+
return `sdvt-t-${fnv1a32(JSON.stringify([ref.name, ref.density, Object.entries(ref.options ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)]))}`;
|
|
1247
|
+
}
|
|
1248
|
+
function styleSheet(spec, theme = resolveTheme(spec.theme)) {
|
|
1249
|
+
const sel = `.${themeKey(spec.theme)}`;
|
|
1250
|
+
return `${tokensCSS(sel, theme.tokens)}
|
|
1251
|
+
${BASE_CSS(sel)}
|
|
1252
|
+
${theme.rules(sel)}`;
|
|
1253
|
+
}
|
|
1254
|
+
function sortAria(sort, key) {
|
|
1255
|
+
if (!sort || sort.col !== key) return "none";
|
|
1256
|
+
return sort.dir === "asc" ? "ascending" : "descending";
|
|
1257
|
+
}
|
|
1258
|
+
function attrsText(attrs) {
|
|
1259
|
+
return Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeAttr(v)}"`).join("");
|
|
1260
|
+
}
|
|
1261
|
+
var styleTag = (css) => css ? `<style>${css}</style>` : "";
|
|
1262
|
+
function tableHTML(p) {
|
|
1263
|
+
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}`;
|
|
1264
|
+
}
|
|
1265
|
+
function assemble(p, inner = tableHTML(p)) {
|
|
1266
|
+
return `${p.link ? `${p.link}
|
|
1267
|
+
` : ""}<div${attrsText(p.wrapperAttrs)}>${styleTag(p.sheet)}${inner}</div>`;
|
|
1268
|
+
}
|
|
1269
|
+
function renderParts(input, rows, opts = {}) {
|
|
1270
|
+
checkKeys(input, rows);
|
|
1271
|
+
const id = tableId(input);
|
|
1272
|
+
const spec = expandTiers(input);
|
|
1273
|
+
const theme = resolveTheme(spec.theme);
|
|
1274
|
+
if (!/^[A-Za-z][\w-]*$/.test(id))
|
|
1275
|
+
throw new TableSpecError(
|
|
1276
|
+
`table id ${JSON.stringify(id)} must match /^[A-Za-z][\\w-]*$/ (it becomes a CSS selector)`
|
|
1277
|
+
);
|
|
1278
|
+
const sel = `#${id}`;
|
|
1279
|
+
const groupBy = spec.decorations.find(
|
|
1280
|
+
(d) => d.type === "groupBy"
|
|
1281
|
+
);
|
|
1282
|
+
const engineHidden = opts.hidden ?? [];
|
|
1283
|
+
const bad = engineHidden.filter((k) => !spec.columns.some((c) => c.key === k));
|
|
1284
|
+
if (bad.length > 0)
|
|
1285
|
+
throw new TableSpecError(
|
|
1286
|
+
`hidden names no column ${bad.map((k) => JSON.stringify(k)).join(", ")}; columns are ${spec.columns.map((c) => c.key).join(", ")}`
|
|
1287
|
+
);
|
|
1288
|
+
const domainRows = opts.domainRows ?? rows;
|
|
1289
|
+
const grid = opts.interactive === true ? opts.grid : void 0;
|
|
1290
|
+
const tabRow = grid === void 0 ? -1 : Math.min(grid.row, rows.length - 1);
|
|
1291
|
+
const scales = columnScales(spec, rows, warn, domainRows);
|
|
1292
|
+
const ctx = {
|
|
1293
|
+
spec,
|
|
1294
|
+
rows,
|
|
1295
|
+
domainRows,
|
|
1296
|
+
theme,
|
|
1297
|
+
id,
|
|
1298
|
+
sel,
|
|
1299
|
+
columns: spec.columns,
|
|
1300
|
+
groupKey: groupBy?.key,
|
|
1301
|
+
warn,
|
|
1302
|
+
teamIds: teamIdsOf(spec, rows),
|
|
1303
|
+
scales,
|
|
1304
|
+
recorded: scales.get(spec.columns.filter(isScaled).at(-1)?.key ?? ""),
|
|
1305
|
+
// none when the last one was skipped (zero rows)
|
|
1306
|
+
scaled: /* @__PURE__ */ new Map()
|
|
1307
|
+
};
|
|
1308
|
+
const deco = applyDecorations(spec, rows, ctx);
|
|
1309
|
+
const visible = spec.columns.filter((c) => !deco.hiddenColumns.has(c.key) && !engineHidden.includes(c.key));
|
|
1310
|
+
const ncol = visible.length;
|
|
1311
|
+
const span = Math.max(1, ncol);
|
|
1312
|
+
const labels = new Map(
|
|
1313
|
+
visible.map((c) => [c.key, (deco.labelText.get(c.key) ?? labelOf(c)) + deco.labelSuffix(c.key)])
|
|
1314
|
+
);
|
|
1315
|
+
let head = visible.map((c) => {
|
|
1316
|
+
const label = `${deco.label(c, labels.get(c.key))}${c.subheader ? `<span class="sdvt-subheader">${escapeHtml(c.subheader)}</span>` : ""}`;
|
|
1317
|
+
const sortable = opts.interactive === true && c.sortable !== false;
|
|
1318
|
+
const dir = sortAria(opts.sort, c.key);
|
|
1319
|
+
const aria = sortable && dir !== "none" ? ` aria-sort="${dir}"` : "";
|
|
1320
|
+
const inner = sortable ? `<button type="button" class="sdvt-sort" data-sdv-sort="${escapeAttr(c.key)}">${label}</button>` : label;
|
|
1321
|
+
const isCurrent = grid?.col === c.key;
|
|
1322
|
+
const current = isCurrent ? " sdvt-col-current" : "";
|
|
1323
|
+
const currentAria = isCurrent ? ' aria-current="true"' : "";
|
|
1324
|
+
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>`;
|
|
1325
|
+
}).join("");
|
|
1326
|
+
const cellsOf = (row, i) => visible.map(
|
|
1327
|
+
(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)])}>${renderCell(c, row, i, ctx)}${deco.cellSuffix(i, c.key)}</td>`
|
|
1328
|
+
).join("");
|
|
1329
|
+
const striped = theme.tokens.stripe !== "transparent";
|
|
1330
|
+
let shown = 0;
|
|
1331
|
+
const gridRow = (i) => grid === void 0 ? "" : ` tabindex="${i === tabRow ? 0 : -1}" aria-selected="${opts.selected?.has(i) === true}"`;
|
|
1332
|
+
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>`;
|
|
1333
|
+
let body = [];
|
|
1334
|
+
const snake = spec.decorations.find(
|
|
1335
|
+
(d) => d.type === "snake"
|
|
1336
|
+
);
|
|
1337
|
+
if (snake && ctx.groupKey !== void 0) throw new TableSpecError("snake cannot be combined with groupBy");
|
|
1338
|
+
if (snake && opts.interactive === true)
|
|
1339
|
+
throw new TableSpecError(
|
|
1340
|
+
"snake cannot be combined with an interactive table (renderHTML(table), hydrate, SdvTable); render it statically with renderHTML(spec, rows)"
|
|
1341
|
+
);
|
|
1342
|
+
const gap = snake && snake.gap > 0 ? `style="width:${snake.gap}px;border:none"` : "";
|
|
1343
|
+
const snaked = snake ? snakeLayout(
|
|
1344
|
+
snake,
|
|
1345
|
+
rows,
|
|
1346
|
+
head,
|
|
1347
|
+
ncol,
|
|
1348
|
+
cellsOf,
|
|
1349
|
+
trOf,
|
|
1350
|
+
gap ? `<th class="sdvt-gap" ${gap}></th>` : "",
|
|
1351
|
+
gap ? `<td class="sdvt-gap" ${gap}></td>` : ""
|
|
1352
|
+
) : null;
|
|
1353
|
+
if (snake && snaked) {
|
|
1354
|
+
head = snaked.head;
|
|
1355
|
+
body = snaked.body;
|
|
1356
|
+
if (snake.cleanGaps)
|
|
1357
|
+
deco.css.push(
|
|
1358
|
+
`${sel} .sdvt-gap,${sel} td.sdvt-blank{border:none!important;background:transparent!important;box-shadow:none!important}`
|
|
1359
|
+
);
|
|
1360
|
+
} else {
|
|
1361
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1362
|
+
if (ctx.groupKey === void 0)
|
|
1363
|
+
groups.set(
|
|
1364
|
+
void 0,
|
|
1365
|
+
rows.map((_, i) => i)
|
|
1366
|
+
);
|
|
1367
|
+
else
|
|
1368
|
+
rows.forEach((row, i) => {
|
|
1369
|
+
const g = row[ctx.groupKey];
|
|
1370
|
+
const list = groups.get(g);
|
|
1371
|
+
if (list) list.push(i);
|
|
1372
|
+
else groups.set(g, [i]);
|
|
1373
|
+
});
|
|
1374
|
+
for (const [g, idxs] of groups) {
|
|
1375
|
+
if (ctx.groupKey !== void 0)
|
|
1376
|
+
body.push(
|
|
1377
|
+
`<tr class="sdvt-group-row${deco.groupRowClass()}"><th scope="rowgroup" colspan="${span}" class="sdvt-group">${escapeHtml(g)}</th></tr>`
|
|
1378
|
+
);
|
|
1379
|
+
for (const i of idxs) body.push(trOf(i, cellsOf(rows[i], i)));
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
const fonts = [...theme.fonts, ...deco.fonts];
|
|
1383
|
+
const name = spec.theme.name;
|
|
1384
|
+
return {
|
|
1385
|
+
id,
|
|
1386
|
+
link: opts.fonts === false ? "" : fontsLink(fonts),
|
|
1387
|
+
// css:"none" drops only the shared theme sheet (A98, A100)
|
|
1388
|
+
wrapperAttrs: {
|
|
1389
|
+
class: `sdvt sdvt-theme-${name} ${themeKey(spec.theme)}`,
|
|
1390
|
+
id,
|
|
1391
|
+
"data-sdvt-theme": name,
|
|
1392
|
+
"data-sdvt-density": spec.theme.density
|
|
1393
|
+
},
|
|
1394
|
+
sheet: opts.css === "none" ? "" : styleSheet(spec, theme),
|
|
1395
|
+
rules: deco.css.join("\n"),
|
|
1396
|
+
before: deco.before,
|
|
1397
|
+
caption: deco.caption ? `<caption>${deco.caption}</caption>` : "",
|
|
1398
|
+
headRows: deco.headRows,
|
|
1399
|
+
head,
|
|
1400
|
+
labels,
|
|
1401
|
+
rows: body.join(""),
|
|
1402
|
+
foot: deco.foot.length > 0 ? `<tfoot>${deco.foot.map((f) => `<tr><td colspan="${span}">${f}</td></tr>`).join("")}</tfoot>` : "",
|
|
1403
|
+
after: deco.after,
|
|
1404
|
+
tableAttrs: grid === void 0 ? "" : ' role="grid" aria-multiselectable="true"'
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
var fontsLinkFor = (spec) => renderParts(spec, []).link;
|
|
1408
|
+
|
|
1409
|
+
// src/html/interactive.ts
|
|
1410
|
+
function tableRenderOptions(table) {
|
|
1411
|
+
const sel = table.getSelection();
|
|
1412
|
+
return {
|
|
1413
|
+
interactive: true,
|
|
1414
|
+
sort: table.state.sort,
|
|
1415
|
+
hidden: table.state.hidden,
|
|
1416
|
+
selected: new Set(table.rows.flatMap((r, i) => sel.has(table.rowId(r)) ? [i] : [])),
|
|
1417
|
+
// A49: always the full source rows, so colours, legends, outlier limits and row-accent palettes hold still
|
|
1418
|
+
// while the user pages, types, sorts or brushes, and index row selectors mean source rows
|
|
1419
|
+
domainRows: table.allRows,
|
|
1420
|
+
// Task 10 (A48): a selectable grid whose one tab stop is the cursor row
|
|
1421
|
+
grid: table.state.cursor
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
function pagerLabel(table) {
|
|
1425
|
+
return `Page ${table.state.page + 1} of ${table.pageCount}`;
|
|
1426
|
+
}
|
|
1427
|
+
function filterInputs(table, labels) {
|
|
1428
|
+
return table.columns.flatMap((c) => {
|
|
1429
|
+
const label = labels.get(c.key);
|
|
1430
|
+
const f = table.state.filters[c.key];
|
|
1431
|
+
return c.filterable === true && label !== void 0 ? [{ key: c.key, label, value: typeof f === "string" ? f : "" }] : [];
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
var SR_ONLY = "position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0";
|
|
1435
|
+
var searchInput = (id, label, placeholder, cls, data, value) => `<label for="${escapeAttr(id)}" style="${SR_ONLY}">${label}</label><input type="search" id="${escapeAttr(id)}" class="${cls}" ${data} placeholder="${placeholder}" value="${escapeAttr(value)}"/>`;
|
|
1436
|
+
function renderToolbar(table, labels = renderParts(table.spec, table.rows, tableRenderOptions(table)).labels) {
|
|
1437
|
+
const id = tableId(table.spec);
|
|
1438
|
+
const global = searchInput(
|
|
1439
|
+
`${id}-search`,
|
|
1440
|
+
"Search all columns",
|
|
1441
|
+
"Search",
|
|
1442
|
+
"sdvt-global-filter",
|
|
1443
|
+
'data-sdv-global-filter=""',
|
|
1444
|
+
table.state.globalFilter
|
|
1445
|
+
);
|
|
1446
|
+
const perColumn = filterInputs(table, labels).map((f, j) => {
|
|
1447
|
+
const label = `Filter ${escapeHtml(f.label)}`;
|
|
1448
|
+
return searchInput(
|
|
1449
|
+
`${id}-filter-${j}`,
|
|
1450
|
+
label,
|
|
1451
|
+
label,
|
|
1452
|
+
"sdvt-filter",
|
|
1453
|
+
`data-sdv-filter="${escapeAttr(f.key)}"`,
|
|
1454
|
+
f.value
|
|
1455
|
+
);
|
|
1456
|
+
}).join("");
|
|
1457
|
+
return `<div class="sdvt-toolbar">${global}${perColumn}</div>`;
|
|
1458
|
+
}
|
|
1459
|
+
function renderPager(table) {
|
|
1460
|
+
const prev = table.state.page === 0 ? ' aria-disabled="true"' : "";
|
|
1461
|
+
const next = table.state.page >= table.pageCount - 1 ? ' aria-disabled="true"' : "";
|
|
1462
|
+
return `<nav class="sdvt-pager" data-sdv-pager="" aria-label="Pagination"><button type="button" class="sdvt-page" data-sdv-page="prev" aria-label="Previous page"${prev}>\u2039</button><span class="sdvt-page-label" data-sdv-page-label="" aria-live="polite">${pagerLabel(table)}</span><button type="button" class="sdvt-page" data-sdv-page="next" aria-label="Next page"${next}>\u203A</button></nav>`;
|
|
1463
|
+
}
|
|
1464
|
+
function renderInteractive(table, opts = {}) {
|
|
1465
|
+
const p = renderParts(table.spec, table.rows, { ...opts, ...tableRenderOptions(table) });
|
|
1466
|
+
const pager = table.state.pageSize === Number.POSITIVE_INFINITY ? "" : renderPager(table);
|
|
1467
|
+
return assemble(
|
|
1468
|
+
p,
|
|
1469
|
+
`${renderToolbar(table, p.labels)}<div class="sdvt-body" data-sdv-body="">${tableHTML(p)}</div>${pager}`
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// src/html/controls.ts
|
|
1474
|
+
var BODY_ROW = "[data-sdv-body] tr.sdvt-row[data-row]";
|
|
1475
|
+
function rowIdAt(table, target) {
|
|
1476
|
+
const tr = target instanceof Element ? target.closest(BODY_ROW) : null;
|
|
1477
|
+
const row = tr === null ? void 0 : table.rows[Number(tr.getAttribute("data-row"))];
|
|
1478
|
+
const id = row === void 0 ? "" : table.rowId(row);
|
|
1479
|
+
return id === "" ? null : id;
|
|
1480
|
+
}
|
|
1481
|
+
function applyHover(root, table, id) {
|
|
1482
|
+
for (const tr of Array.from(root.querySelectorAll(BODY_ROW))) {
|
|
1483
|
+
const row = id === null ? void 0 : table.rows[Number(tr.getAttribute("data-row"))];
|
|
1484
|
+
tr.classList.toggle("sdvt-hover", row !== void 0 && table.rowId(row) === id);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
function handleClick(table, target) {
|
|
1488
|
+
const el = target instanceof Element ? target.closest("[data-sdv-sort],[data-sdv-page]") : null;
|
|
1489
|
+
if (!el) {
|
|
1490
|
+
const id = rowIdAt(table, target);
|
|
1491
|
+
if (id === null) return;
|
|
1492
|
+
const next = new Set(table.getSelection());
|
|
1493
|
+
if (!next.delete(id)) next.add(id);
|
|
1494
|
+
table.setSelection(next);
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
if (el.getAttribute("aria-disabled") === "true") return;
|
|
1498
|
+
const sortCol = el.getAttribute("data-sdv-sort");
|
|
1499
|
+
const page = el.getAttribute("data-sdv-page");
|
|
1500
|
+
if (sortCol !== null) table.setSort(sortCol, nextSortDir(table.state.sort, sortCol));
|
|
1501
|
+
else if (page === "prev") table.setPage(table.state.page - 1);
|
|
1502
|
+
else if (page === "next") table.setPage(table.state.page + 1);
|
|
1503
|
+
}
|
|
1504
|
+
function handleInput(table, target) {
|
|
1505
|
+
if (!(target instanceof HTMLInputElement)) return;
|
|
1506
|
+
const col = target.getAttribute("data-sdv-filter");
|
|
1507
|
+
if (col !== null) table.setFilter(col, target.value === "" ? null : target.value);
|
|
1508
|
+
else if (target.hasAttribute("data-sdv-global-filter")) table.setGlobalFilter(target.value);
|
|
1509
|
+
}
|
|
1510
|
+
function handleHover(table, target) {
|
|
1511
|
+
table.setHover(rowIdAt(table, target));
|
|
1512
|
+
}
|
|
1513
|
+
var CONTROLS = ["data-sdv-sort", "data-sdv-filter", "data-sdv-global-filter", "data-sdv-page"];
|
|
1514
|
+
var FOCUSABLE = "a[href],button,input,select,textarea,[tabindex]";
|
|
1515
|
+
function captureFocus(el) {
|
|
1516
|
+
const root = el.getRootNode();
|
|
1517
|
+
const focused = root.activeElement ?? el.ownerDocument.activeElement;
|
|
1518
|
+
if (focused === null || !el.contains(focused)) return () => {
|
|
1519
|
+
};
|
|
1520
|
+
const name = CONTROLS.find((a) => focused.hasAttribute(a));
|
|
1521
|
+
const value = name === void 0 ? null : focused.getAttribute(name);
|
|
1522
|
+
const caret = focused instanceof HTMLInputElement ? { start: focused.selectionStart, end: focused.selectionEnd } : void 0;
|
|
1523
|
+
const td = name === void 0 ? focused.closest("[data-sdv-body] tr[data-row] td[data-col]") : null;
|
|
1524
|
+
const cellAt = td ? {
|
|
1525
|
+
row: td.parentElement?.getAttribute("data-row"),
|
|
1526
|
+
col: td.getAttribute("data-col"),
|
|
1527
|
+
n: Array.from(td.querySelectorAll(FOCUSABLE)).indexOf(focused)
|
|
1528
|
+
} : void 0;
|
|
1529
|
+
const row = name === void 0 && focused.matches(BODY_ROW) ? focused.getAttribute("data-row") : null;
|
|
1530
|
+
return () => {
|
|
1531
|
+
if (el.contains(focused)) return;
|
|
1532
|
+
if (name !== void 0) {
|
|
1533
|
+
const next = Array.from(el.querySelectorAll(`[${name}]`)).find(
|
|
1534
|
+
(n) => n.getAttribute(name) === value
|
|
1535
|
+
);
|
|
1536
|
+
next?.focus();
|
|
1537
|
+
if (next instanceof HTMLInputElement && caret?.start != null && caret.end != null)
|
|
1538
|
+
next.setSelectionRange(caret.start, caret.end);
|
|
1539
|
+
} else if (row !== null) {
|
|
1540
|
+
Array.from(el.querySelectorAll(BODY_ROW)).find((r) => r.getAttribute("data-row") === row)?.focus();
|
|
1541
|
+
} else if (cellAt && cellAt.n >= 0) {
|
|
1542
|
+
const tds = Array.from(el.querySelectorAll("[data-sdv-body] tr[data-row]")).find((r) => r.getAttribute("data-row") === cellAt.row)?.querySelectorAll("td[data-col]");
|
|
1543
|
+
const cell = Array.from(tds ?? []).find((c) => c.getAttribute("data-col") === cellAt.col);
|
|
1544
|
+
cell?.querySelectorAll(FOCUSABLE)[cellAt.n]?.focus();
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
var KEYS = /* @__PURE__ */ new Map([
|
|
1549
|
+
["j", "down"],
|
|
1550
|
+
["k", "up"],
|
|
1551
|
+
["h", "left"],
|
|
1552
|
+
["l", "right"],
|
|
1553
|
+
["s", "sort"],
|
|
1554
|
+
["/", "search"],
|
|
1555
|
+
["ArrowDown", "down"],
|
|
1556
|
+
["ArrowUp", "up"],
|
|
1557
|
+
["ArrowLeft", "left"],
|
|
1558
|
+
["ArrowRight", "right"],
|
|
1559
|
+
["Enter", "toggle"],
|
|
1560
|
+
[" ", "toggle"]
|
|
1561
|
+
]);
|
|
1562
|
+
var LETTERS = /* @__PURE__ */ new Set(["j", "k", "h", "l", "s", "/"]);
|
|
1563
|
+
var clamp = (i, n) => Math.max(0, Math.min(i, n - 1));
|
|
1564
|
+
function keyAction(key, k) {
|
|
1565
|
+
const what = KEYS.get(key);
|
|
1566
|
+
if (what === void 0 || !k.hotkeys && LETTERS.has(key)) return null;
|
|
1567
|
+
const shown = (c) => c !== null && k.cols.includes(c);
|
|
1568
|
+
const col = shown(k.cursor.col) ? k.cursor.col : shown(k.sorted) ? k.sorted : k.cols[0] ?? null;
|
|
1569
|
+
const row = k.focused ?? k.cursor.row;
|
|
1570
|
+
switch (what) {
|
|
1571
|
+
case "down":
|
|
1572
|
+
case "up": {
|
|
1573
|
+
const at = k.order.indexOf(row);
|
|
1574
|
+
if (at < 0) return null;
|
|
1575
|
+
const next = k.focused === null ? row : k.order[clamp(at + (what === "down" ? 1 : -1), k.order.length)];
|
|
1576
|
+
return next === void 0 ? null : { type: "cursor", row: next, col: k.cursor.col };
|
|
1577
|
+
}
|
|
1578
|
+
case "left":
|
|
1579
|
+
case "right": {
|
|
1580
|
+
if (k.focused !== null && !k.onRow) return null;
|
|
1581
|
+
const step = what === "right" ? 1 : -1;
|
|
1582
|
+
const next = col === null ? void 0 : k.cols[clamp(k.cols.indexOf(col) + step, k.cols.length)];
|
|
1583
|
+
return next === void 0 ? null : { type: "cursor", row, col: next };
|
|
1584
|
+
}
|
|
1585
|
+
case "sort":
|
|
1586
|
+
return col === null ? null : { type: "sort", col };
|
|
1587
|
+
case "search":
|
|
1588
|
+
return { type: "search" };
|
|
1589
|
+
case "toggle":
|
|
1590
|
+
return k.onRow && k.focused !== null ? { type: "toggle", row: k.focused } : null;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
var TEXT_FIELD = 'input,textarea,select,[contenteditable]:not([contenteditable="false"])';
|
|
1594
|
+
function handleKeydown(table, root, e) {
|
|
1595
|
+
const t = e.target;
|
|
1596
|
+
if (e.defaultPrevented || e.isComposing || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
1597
|
+
if (!(t instanceof Element) || t.closest(TEXT_FIELD) !== null) return;
|
|
1598
|
+
const rows = Array.from(root.querySelectorAll(BODY_ROW));
|
|
1599
|
+
const sorts = Array.from(root.querySelectorAll("[data-sdv-body] [data-sdv-sort]"));
|
|
1600
|
+
const near = t.closest(BODY_ROW);
|
|
1601
|
+
const rowEl = near !== null && root.contains(near) ? near : null;
|
|
1602
|
+
const s = table.state;
|
|
1603
|
+
const action = keyAction(e.key, {
|
|
1604
|
+
focused: rowEl === null ? null : Number(rowEl.getAttribute("data-row")),
|
|
1605
|
+
onRow: rowEl === t,
|
|
1606
|
+
cursor: s.cursor,
|
|
1607
|
+
sorted: s.sort?.col ?? null,
|
|
1608
|
+
cols: sorts.map((b) => b.getAttribute("data-sdv-sort") ?? ""),
|
|
1609
|
+
order: rows.map((r) => Number(r.getAttribute("data-row"))),
|
|
1610
|
+
hotkeys: table.spec.interactive?.hotkeys !== false
|
|
1611
|
+
});
|
|
1612
|
+
const box = action?.type === "search" ? root.querySelector("[data-sdv-global-filter]") : null;
|
|
1613
|
+
if (action === null || action.type === "search" && box === null) return;
|
|
1614
|
+
e.preventDefault();
|
|
1615
|
+
if (action.type === "search") box?.focus();
|
|
1616
|
+
else if (action.type === "sort")
|
|
1617
|
+
handleClick(table, sorts.find((b) => b.getAttribute("data-sdv-sort") === action.col) ?? null);
|
|
1618
|
+
else if (action.type === "toggle") {
|
|
1619
|
+
if (!e.repeat) handleClick(table, rowEl);
|
|
1620
|
+
} else {
|
|
1621
|
+
rows.find((r) => r.getAttribute("data-row") === String(action.row))?.focus();
|
|
1622
|
+
table.setCursor(action.row, action.col);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
export { SR_ONLY, applyHover, assemble, attrsText, captureFocus, escapeAttr, escapeHtml, filterInputs, fontsLink, fontsLinkFor, handleClick, handleHover, handleInput, handleKeydown, keyAction, labelOf, loadTeamNames, pagerLabel, renderInteractive, renderPager, renderParts, renderToolbar, rowIdAt, sortAria, styleSheet, tableHTML, tableRenderOptions, themeKey };
|
|
1627
|
+
//# sourceMappingURL=chunk-UCRLJGDL.js.map
|
|
1628
|
+
//# sourceMappingURL=chunk-UCRLJGDL.js.map
|