@sportsdataverse/sdvtables 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +70 -112
  2. package/data/table_themes.json +12136 -0
  3. package/data/table_themes.json.sha256 +1 -0
  4. package/dist/aggrid.d.ts +104 -0
  5. package/dist/aggrid.js +230 -0
  6. package/dist/aggrid.js.map +1 -0
  7. package/dist/chunk-3TJR7TKS.js +223 -0
  8. package/dist/chunk-3TJR7TKS.js.map +1 -0
  9. package/dist/{chunk-UCRLJGDL.js → chunk-R375P3MN.js} +963 -276
  10. package/dist/chunk-R375P3MN.js.map +1 -0
  11. package/dist/chunk-UTOVFIFT.js +15176 -0
  12. package/dist/chunk-UTOVFIFT.js.map +1 -0
  13. package/dist/{chunk-U6W6MWKW.js → chunk-VLF2S6VW.js} +7 -5
  14. package/dist/chunk-VLF2S6VW.js.map +1 -0
  15. package/dist/chunk-WBTFIGYK.js +189 -0
  16. package/dist/chunk-WBTFIGYK.js.map +1 -0
  17. package/dist/{engine-CkfxYXAL.d.ts → engine-BFIPJUlC.d.ts} +1 -1
  18. package/dist/export.d.ts +1 -1
  19. package/dist/export.js +4 -3
  20. package/dist/export.js.map +1 -1
  21. package/dist/html.d.ts +8 -5
  22. package/dist/html.js +4 -3
  23. package/dist/index.d.ts +121 -8
  24. package/dist/index.js +51 -3
  25. package/dist/index.js.map +1 -1
  26. package/dist/mui.d.ts +134 -0
  27. package/dist/mui.js +193 -0
  28. package/dist/mui.js.map +1 -0
  29. package/dist/react.d.ts +2 -2
  30. package/dist/react.js +3 -2
  31. package/dist/react.js.map +1 -1
  32. package/dist/spec-A88vJX5A.d.ts +748 -0
  33. package/dist/tanstack-table.d.ts +113 -0
  34. package/dist/tanstack-table.js +98 -0
  35. package/dist/tanstack-table.js.map +1 -0
  36. package/dist/{tokens-CpYFsFtf.d.ts → tokens-CHwBt9gs.d.ts} +1 -1
  37. package/package.json +40 -3
  38. package/dist/chunk-4VKYHOHL.js +0 -2081
  39. package/dist/chunk-4VKYHOHL.js.map +0 -1
  40. package/dist/chunk-U6W6MWKW.js.map +0 -1
  41. package/dist/chunk-UCRLJGDL.js.map +0 -1
  42. package/dist/spec-BEhLDtj_.d.ts +0 -430
@@ -1,4 +1,4 @@
1
- import { resolveTheme, TableSpecError, nextSortDir, TOKEN_KEYS, toNumber, isBlank, secondaryOn, naturalDigits, RANK_PALETTE, formatValue, fontStack, formatNumber, selectRows, snakeAlign, matches, ordinal } from './chunk-4VKYHOHL.js';
1
+ import { TableSpecError, formatNumber, naturalDigits, resolveTheme, TOKEN_KEYS, toNumber, checkChart, isBlank, secondaryOn, RANK_PALETTE, formatValue, fontStack, selectRows, snakeAlign, matches, ordinal, scaledNumber } from './chunk-UTOVFIFT.js';
2
2
  import { loadLeague, hex6, warn, resolveSync, onColor, mix, solid, contrast, teamColorsSync, headshotUrl, logoUrlSync } from '@sportsdataverse/sdvplot';
3
3
 
4
4
  var NAMES = /* @__PURE__ */ new Map();
@@ -23,13 +23,13 @@ var MAP = {
23
23
  };
24
24
  var escapeHtml = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => MAP[c]);
25
25
  var escapeAttr = escapeHtml;
26
- var CSS_UNSAFE = /[;{}<>"\\\n\r]/;
26
+ var CSS_UNSAFE = /[;{}<>"\\\n\r]|url\(/i;
27
27
  var isCssValue = (v) => !CSS_UNSAFE.test(String(v));
28
28
  function cssValue(v, field) {
29
29
  const s = String(v);
30
30
  if (CSS_UNSAFE.test(s))
31
31
  throw new TableSpecError(
32
- `${field} ${JSON.stringify(s)} is not a CSS value: it may not contain ; { } < > " \\ or a line break`
32
+ `${field} ${JSON.stringify(s)} is not a CSS value: it may not contain ; { } < > " \\ url( or a line break`
33
33
  );
34
34
  return s;
35
35
  }
@@ -47,6 +47,31 @@ function styleOf(decls) {
47
47
  return s ? ` style="${s}"` : "";
48
48
  }
49
49
 
50
+ // src/html/icons.ts
51
+ var PATHS = {
52
+ circle: "M8 1a7 7 0 1 0 0 14A7 7 0 1 0 8 1z",
53
+ square: "M2 2h12v12H2z",
54
+ diamond: "M8 0.5 15.5 8 8 15.5 0.5 8z",
55
+ star: "M8 0.8l2.2 4.6 5 .6-3.7 3.5.9 5L8 12l-4.4 2.5.9-5L.8 6l5-.6z",
56
+ "triangle-up": "M8 1.5 15 14.5H1z",
57
+ "triangle-down": "M1 1.5h14L8 14.5z",
58
+ "arrow-up": "M8 1 14 8H10v7H6V8H2z",
59
+ "arrow-down": "M6 1h4v7h4l-6 7-6-7h4z",
60
+ "arrow-right": "M1 6h7V2l7 6-7 6v-4H1z",
61
+ check: "M1.5 8.5 3.6 6.4 6.2 9 12.4 2.8 14.5 4.9 6.2 13.2z",
62
+ xmark: "M3.1 1 8 5.9 12.9 1 15 3.1 10.1 8 15 12.9 12.9 15 8 10.1 3.1 15 1 12.9 5.9 8 1 3.1z",
63
+ minus: "M1 6.5h14v3H1z",
64
+ plus: "M6.5 1h3v5.5H15v3H9.5V15h-3V9.5H1v-3h5.5z",
65
+ heart: "M8 14.5 1.6 8.2A3.9 3.9 0 0 1 8 3.4a3.9 3.9 0 0 1 6.4 4.8z"
66
+ };
67
+ var ICON_NAMES = Object.keys(PATHS);
68
+ function iconSvg(name, color, size) {
69
+ const d = Object.hasOwn(PATHS, name) ? PATHS[name] : void 0;
70
+ if (d === void 0)
71
+ throw new TableSpecError(`unknown icon ${JSON.stringify(name)}; icons are ${ICON_NAMES.join(", ")}`);
72
+ return `<svg class="sdvt-icon" viewBox="0 0 16 16" width="${size}" height="${size}" fill="${escapeAttr(color)}" aria-hidden="true" style="vertical-align:middle"><path d="${d}"/></svg>`;
73
+ }
74
+
50
75
  // src/html/fonts.ts
51
76
  function fontsLink(fonts) {
52
77
  const byFamily = /* @__PURE__ */ new Map();
@@ -147,16 +172,43 @@ function BASE_CSS(sel) {
147
172
  // Task 10: the column `s` sorts
148
173
  ].join("\n");
149
174
  }
150
- function ramp(palette, [lo, hi]) {
175
+ function ramp(palette, [lo, hi], bias = 1) {
151
176
  const k = palette.length - 1;
177
+ if (!(bias > 0)) throw new TableSpecError(`bias must be a positive number, got ${String(bias)}`);
178
+ const stops = palette.map((_, i) => k === 0 ? 0 : (i / k) ** bias);
152
179
  return (v) => {
153
180
  if (!(lo <= v && v <= hi)) return null;
154
181
  if (k === 0) return palette[0] ?? null;
155
182
  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);
183
+ if (bias === 1) {
184
+ const i2 = Math.min(Math.floor(t * k), k - 1);
185
+ return mix(palette[i2], palette[i2 + 1], t * k - i2);
186
+ }
187
+ let i = 0;
188
+ while (i < k - 1 && t > stops[i + 1]) i++;
189
+ const a = stops[i];
190
+ const b = stops[i + 1];
191
+ return mix(palette[i], palette[i + 1], Math.min(1, Math.max(0, (t - a) / (b - a))));
158
192
  };
159
193
  }
194
+ function ntile(values, n) {
195
+ if (!(Number.isInteger(n) && n > 0))
196
+ throw new TableSpecError(`buckets must be a positive integer, got ${String(n)}`);
197
+ const order = values.map((v, i) => [v, i]).filter((p) => p[0] !== null).sort((a, b) => a[0] - b[0] || a[1] - b[1]);
198
+ const len = order.length;
199
+ const out = values.map(() => null);
200
+ const nLarger = len % n;
201
+ const larger = Math.ceil(len / n);
202
+ const smaller = Math.floor(len / n);
203
+ const threshold = larger * nLarger;
204
+ order.forEach(([, i], j) => {
205
+ const x = j + 1;
206
+ out[i] = Math.floor(
207
+ x <= threshold ? (x + larger - 1) / larger : (x - threshold + smaller - 1) / smaller + nLarger
208
+ );
209
+ });
210
+ return out;
211
+ }
160
212
  function averageRanks(values, desc) {
161
213
  const idx = values.map((v, i) => [v, i]).filter((p) => p[0] !== null).sort((a, b) => a[0] - b[0] || a[1] - b[1]);
162
214
  const out = values.map(() => null);
@@ -195,6 +247,394 @@ function sampleSd(xs) {
195
247
  return Math.sqrt(xs.reduce((a, b) => a + (b - m) ** 2, 0) / (xs.length - 1));
196
248
  }
197
249
 
250
+ // src/html/named-colors.ts
251
+ var NAMED = new Map(
252
+ Object.entries({
253
+ aliceblue: "f0f8ff",
254
+ antiquewhite: "faebd7",
255
+ aqua: "00ffff",
256
+ aquamarine: "7fffd4",
257
+ azure: "f0ffff",
258
+ beige: "f5f5dc",
259
+ bisque: "ffe4c4",
260
+ black: "000000",
261
+ blanchedalmond: "ffebcd",
262
+ blue: "0000ff",
263
+ blueviolet: "8a2be2",
264
+ brown: "a52a2a",
265
+ burlywood: "deb887",
266
+ cadetblue: "5f9ea0",
267
+ chartreuse: "7fff00",
268
+ chocolate: "d2691e",
269
+ coral: "ff7f50",
270
+ cornflowerblue: "6495ed",
271
+ cornsilk: "fff8dc",
272
+ crimson: "dc143c",
273
+ cyan: "00ffff",
274
+ darkblue: "00008b",
275
+ darkcyan: "008b8b",
276
+ darkgoldenrod: "b8860b",
277
+ darkgray: "a9a9a9",
278
+ darkgreen: "006400",
279
+ darkgrey: "a9a9a9",
280
+ darkkhaki: "bdb76b",
281
+ darkmagenta: "8b008b",
282
+ darkolivegreen: "556b2f",
283
+ darkorange: "ff8c00",
284
+ darkorchid: "9932cc",
285
+ darkred: "8b0000",
286
+ darksalmon: "e9967a",
287
+ darkseagreen: "8fbc8f",
288
+ darkslateblue: "483d8b",
289
+ darkslategray: "2f4f4f",
290
+ darkslategrey: "2f4f4f",
291
+ darkturquoise: "00ced1",
292
+ darkviolet: "9400d3",
293
+ deeppink: "ff1493",
294
+ deepskyblue: "00bfff",
295
+ dimgray: "696969",
296
+ dimgrey: "696969",
297
+ dodgerblue: "1e90ff",
298
+ firebrick: "b22222",
299
+ floralwhite: "fffaf0",
300
+ forestgreen: "228b22",
301
+ fuchsia: "ff00ff",
302
+ gainsboro: "dcdcdc",
303
+ ghostwhite: "f8f8ff",
304
+ gold: "ffd700",
305
+ goldenrod: "daa520",
306
+ gray: "808080",
307
+ green: "008000",
308
+ greenyellow: "adff2f",
309
+ grey: "808080",
310
+ honeydew: "f0fff0",
311
+ hotpink: "ff69b4",
312
+ indianred: "cd5c5c",
313
+ indigo: "4b0082",
314
+ ivory: "fffff0",
315
+ khaki: "f0e68c",
316
+ lavender: "e6e6fa",
317
+ lavenderblush: "fff0f5",
318
+ lawngreen: "7cfc00",
319
+ lemonchiffon: "fffacd",
320
+ lightblue: "add8e6",
321
+ lightcoral: "f08080",
322
+ lightcyan: "e0ffff",
323
+ lightgoldenrodyellow: "fafad2",
324
+ lightgray: "d3d3d3",
325
+ lightgreen: "90ee90",
326
+ lightgrey: "d3d3d3",
327
+ lightpink: "ffb6c1",
328
+ lightsalmon: "ffa07a",
329
+ lightseagreen: "20b2aa",
330
+ lightskyblue: "87cefa",
331
+ lightslategray: "778899",
332
+ lightslategrey: "778899",
333
+ lightsteelblue: "b0c4de",
334
+ lightyellow: "ffffe0",
335
+ lime: "00ff00",
336
+ limegreen: "32cd32",
337
+ linen: "faf0e6",
338
+ magenta: "ff00ff",
339
+ maroon: "800000",
340
+ mediumaquamarine: "66cdaa",
341
+ mediumblue: "0000cd",
342
+ mediumorchid: "ba55d3",
343
+ mediumpurple: "9370db",
344
+ mediumseagreen: "3cb371",
345
+ mediumslateblue: "7b68ee",
346
+ mediumspringgreen: "00fa9a",
347
+ mediumturquoise: "48d1cc",
348
+ mediumvioletred: "c71585",
349
+ midnightblue: "191970",
350
+ mintcream: "f5fffa",
351
+ mistyrose: "ffe4e1",
352
+ moccasin: "ffe4b5",
353
+ navajowhite: "ffdead",
354
+ navy: "000080",
355
+ oldlace: "fdf5e6",
356
+ olive: "808000",
357
+ olivedrab: "6b8e23",
358
+ orange: "ffa500",
359
+ orangered: "ff4500",
360
+ orchid: "da70d6",
361
+ palegoldenrod: "eee8aa",
362
+ palegreen: "98fb98",
363
+ paleturquoise: "afeeee",
364
+ palevioletred: "db7093",
365
+ papayawhip: "ffefd5",
366
+ peachpuff: "ffdab9",
367
+ peru: "cd853f",
368
+ pink: "ffc0cb",
369
+ plum: "dda0dd",
370
+ powderblue: "b0e0e6",
371
+ purple: "800080",
372
+ rebeccapurple: "663399",
373
+ red: "ff0000",
374
+ rosybrown: "bc8f8f",
375
+ royalblue: "4169e1",
376
+ saddlebrown: "8b4513",
377
+ salmon: "fa8072",
378
+ sandybrown: "f4a460",
379
+ seagreen: "2e8b57",
380
+ seashell: "fff5ee",
381
+ sienna: "a0522d",
382
+ silver: "c0c0c0",
383
+ skyblue: "87ceeb",
384
+ slateblue: "6a5acd",
385
+ slategray: "708090",
386
+ slategrey: "708090",
387
+ snow: "fffafa",
388
+ springgreen: "00ff7f",
389
+ steelblue: "4682b4",
390
+ tan: "d2b48c",
391
+ teal: "008080",
392
+ thistle: "d8bfd8",
393
+ tomato: "ff6347",
394
+ turquoise: "40e0d0",
395
+ violet: "ee82ee",
396
+ wheat: "f5deb3",
397
+ white: "ffffff",
398
+ whitesmoke: "f5f5f5",
399
+ yellow: "ffff00",
400
+ yellowgreen: "9acd32"
401
+ }).map(([k, v]) => [k, `#${v}`])
402
+ );
403
+ function namedColor(s) {
404
+ return NAMED.get(s.trim().toLowerCase());
405
+ }
406
+
407
+ // src/html/svg.ts
408
+ var halfEven = (x) => {
409
+ const r = Math.round(x);
410
+ return String(Math.abs(x % 1) === 0.5 && r % 2 !== 0 ? r - 1 : r);
411
+ };
412
+ function cutlineSvg(text, color, size) {
413
+ const t = text.toUpperCase();
414
+ const tracking = 1.1;
415
+ const esc = t.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
416
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${halfEven(t.length * (size * 0.8 + tracking) + 4)}" height="${halfEven(size + 4)}"><text x="0" y="${(size + 0.5).toFixed(1)}" font-family="Helvetica,Arial,sans-serif" font-size="${size}" font-weight="700" letter-spacing="${tracking}" fill="${color}">${esc}</text></svg>`;
417
+ return `data:image/svg+xml;charset=utf-8,${pyQuote(svg)}`;
418
+ }
419
+ var pyQuote = (x) => encodeURIComponent(x).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
420
+ function watermarkSvg(text, color, opacity, angle, font) {
421
+ const label = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
422
+ const size = 100;
423
+ const textW = Math.max(label.length * size * 0.62, size);
424
+ const textH = size * 1.3;
425
+ const rad = Math.abs(angle) * Math.PI / 180;
426
+ const w = Math.ceil(textW * Math.cos(rad) + textH * Math.sin(rad)) + 4;
427
+ const h = Math.ceil(textW * Math.sin(rad) + textH * Math.cos(rad)) + 4;
428
+ const rot = angle !== 0 ? ` transform="rotate(${angle} ${w / 2} ${h / 2})"` : "";
429
+ const attr = (v) => v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
430
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}"><text x="50%" y="50%" text-anchor="middle" dominant-baseline="central" font-family="${attr(font)}" font-size="${size}" font-weight="700" fill="${attr(color)}" fill-opacity="${opacity}"${rot}>${label}</text></svg>`;
431
+ return [`data:image/svg+xml,${pyQuote(svg)}`, h > w];
432
+ }
433
+ var cssStr = (v) => v.replace(/[\\"\n\r<]/g, (c) => `\\${c.charCodeAt(0).toString(16)} `);
434
+ var NAMED2 = ["first", "last", "min", "max"];
435
+ var n2 = (v) => String(Math.round(v * 100) / 100 + 0);
436
+ function plainNumber(v) {
437
+ if (Number.isInteger(v)) return String(v);
438
+ const places = Math.max(4, 3 - Math.floor(Math.log10(Math.abs(v))));
439
+ return v.toFixed(places).replace(/0+$/, "").replace(/\.$/, "");
440
+ }
441
+ var fixed = (v, d) => {
442
+ const s = v.toFixed(d);
443
+ return /^-0(.0*)?$/.test(s) ? s.slice(1) : s;
444
+ };
445
+ var att = (v, field) => escapeAttr(cssValue(v, field));
446
+ function quantile(xs, p) {
447
+ const s = [...xs].sort((a2, b) => a2 - b);
448
+ const h = (s.length - 1) * p;
449
+ const lo = Math.floor(h);
450
+ const a = s[lo];
451
+ return a + (h - lo) * (s[Math.min(lo + 1, s.length - 1)] - a);
452
+ }
453
+ var STAT = {
454
+ mean: (xs) => xs.reduce((s, x) => s + x, 0) / xs.length,
455
+ median: (xs) => quantile(xs, 0.5),
456
+ min: (xs) => Math.min(...xs),
457
+ max: (xs) => Math.max(...xs)
458
+ };
459
+ function margins(labels, statline, bars) {
460
+ const any = labels.length > 0;
461
+ const endsOnly = any && !labels.some((n) => n === "min" || n === "max" || n === "all");
462
+ if (bars) {
463
+ if (statline) return any ? [12, 26, 3, 26, 22] : [4, 28, 3, 13, 22];
464
+ return any ? [12, 26, 0, 26, 22] : [3, 13, 0, 13, 22];
465
+ }
466
+ if (statline) return !any ? [4, 28, 3, 13, 22] : endsOnly ? [5, 28, 3, 24, 22] : [14, 28, 10, 13, 28];
467
+ return !any ? [3, 13, 2, 13, 22] : endsOnly ? [5, 24, 3, 24, 22] : [14, 13, 10, 13, 30];
468
+ }
469
+ function frameOf(col, present2, bars, row) {
470
+ const [t = 0, r = 0, b = 0, l = 0, grown = 22] = margins(col.labels, col.statline !== void 0, bars);
471
+ const h = col.chartHeight ?? grown;
472
+ if (col.chartWidth <= l + r || h <= t + b)
473
+ throw new TableSpecError(
474
+ `column "${col.key}": ${col.kind} chartWidth x chartHeight ${plainNumber(col.chartWidth)} x ${plainNumber(h)} leaves no room inside its margins (${l + r} x ${t + b} px with these labels/statline); make the chart larger`
475
+ );
476
+ const lo = col.minValue ?? Math.min(...present2, ...bars ? [0] : []);
477
+ const hi = col.maxValue ?? Math.max(...present2, ...bars ? [0] : []);
478
+ if (lo > hi)
479
+ throw new TableSpecError(
480
+ `column "${col.key}" row ${row}: minValue/maxValue cross the row's data (bottom ${plainNumber(lo)} is above top ${plainNumber(hi)}); give both bounds, or one the data stays on the right side of`
481
+ );
482
+ const bottom = h - b;
483
+ const y = (v) => hi === lo ? (t + bottom) / 2 : t + Math.min(Math.max((hi - v) / (hi - lo), 0), 1) * (bottom - t);
484
+ return { h, top: t, right: col.chartWidth - r, bottom, left: l, lo, hi, y };
485
+ }
486
+ function statlineSvg(col, f, present2) {
487
+ if (col.statline === void 0) return "";
488
+ const v = STAT[col.statline](present2);
489
+ const y = n2(f.y(v));
490
+ const c = att(col.statlineColor, `${col.kind} statlineColor`);
491
+ const size = att(col.statlineLabelSize, `${col.kind} statlineLabelSize`);
492
+ return `<line class="sdvt-statline" x1="${n2(f.left)}" x2="${n2(f.right)}" y1="${y}" y2="${y}" stroke="${c}" stroke-width="1" stroke-dasharray="2,2" stroke-opacity="0.75"/><text class="sdvt-statline-label" x="${n2(f.right + 5)}" y="${y}" dominant-baseline="middle" fill="${c}" font-weight="bold" font-size="${size}">${fixed(v, col.decimals)}</text>`;
493
+ }
494
+ function bandlineSvg(col, f, present2) {
495
+ if (col.bandline === void 0) return "";
496
+ const range = col.bandline === "range" || present2.length < 2;
497
+ const lo = range ? Math.min(...present2) : quantile(present2, 0.25);
498
+ const hi = range ? Math.max(...present2) : quantile(present2, 0.75);
499
+ const y1 = f.y(hi);
500
+ return `<rect class="sdvt-bandline" x="${n2(f.left)}" y="${n2(y1)}" width="${n2(f.right - f.left)}" height="${n2(f.y(lo) - y1)}" fill="${att(col.bandlineColor, `${col.kind} bandlineColor`)}" fill-opacity="${col.bandlineOpacity}"/>`;
501
+ }
502
+ function summary(name, col, present2) {
503
+ const n = present2.length;
504
+ let out = `${name} of ${n} value${n === 1 ? "" : "s"}: min ${plainNumber(Math.min(...present2))}, max ${plainNumber(Math.max(...present2))}, last ${plainNumber(present2[n - 1])}`;
505
+ if (col.statline) out += `; ${col.statline} line at ${fixed(STAT[col.statline](present2), col.decimals)}`;
506
+ if (col.bandline)
507
+ out += `; band over the ${col.bandline === "innerquartiles" ? "inner quartiles" : "range"}`;
508
+ return out;
509
+ }
510
+ function pointsAt(values, idx) {
511
+ const v = (i) => values[i];
512
+ let min = idx[0];
513
+ let max = min;
514
+ for (const i of idx) {
515
+ if (v(i) < v(min)) min = i;
516
+ if (v(i) > v(max)) max = i;
517
+ }
518
+ return { first: idx[0], last: idx.at(-1), min, max };
519
+ }
520
+ function markColors(h, at, idx, field) {
521
+ const named = h === void 0 ? {} : Array.isArray(h) ? Object.fromEntries(h.map((n) => [n, "red"])) : h;
522
+ const out = /* @__PURE__ */ new Map();
523
+ const all = named.all;
524
+ if (all !== void 0) for (const i of idx) out.set(i, att(all, field));
525
+ for (const n of NAMED2) {
526
+ const c = named[n];
527
+ if (c !== void 0) out.set(at[n], att(c, field));
528
+ }
529
+ return new Map([...out].sort((a, b) => a[0] - b[0]));
530
+ }
531
+ var labelled = (labels, at, idx) => labels.includes("all") ? [...idx] : [...new Set(labels.map((n) => at[n]))].sort((a, b) => a - b);
532
+ function chartSvg(kind, w, h, label, body) {
533
+ return `<svg class="sdvt-${kind}" xmlns="http://www.w3.org/2000/svg" width="${n2(w)}" height="${n2(h)}" viewBox="0 0 ${n2(w)} ${n2(h)}" role="img" aria-label="${escapeAttr(label)}" style="display:inline-block;vertical-align:middle;overflow:visible"><title>${escapeHtml(label)}</title>${body}</svg>`;
534
+ }
535
+ var present = (values) => values.filter((v) => v !== null);
536
+ var presentIdx = (values) => values.flatMap((v, i) => v === null ? [] : [i]);
537
+ function sparklineSvg(col, values, color, row) {
538
+ const xs = present(values);
539
+ const idx = presentIdx(values);
540
+ const f = frameOf(col, xs, false, row);
541
+ const n = values.length;
542
+ const step = n > 1 ? (f.right - f.left) / (n - 1) : 0;
543
+ const pts = new Map(
544
+ idx.map((i) => [i, [n > 1 ? f.left + i * step : (f.left + f.right) / 2, f.y(values[i])]])
545
+ );
546
+ const path = [];
547
+ for (let i = 0; i < n; i++) {
548
+ const p = pts.get(i);
549
+ if (!p) continue;
550
+ const down = pts.has(i - 1);
551
+ path.push(`${down ? "L" : "M"}${n2(p[0])},${n2(p[1])}`);
552
+ if (!down && !pts.has(i + 1)) path.push("h0");
553
+ }
554
+ const stroke = att(color, "sparkline lineColor");
555
+ let body = bandlineSvg(col, f, xs);
556
+ if (col.showArea) {
557
+ const runs = [];
558
+ for (const i of idx) {
559
+ if (pts.has(i - 1)) runs.at(-1)?.push(pts.get(i));
560
+ else runs.push([pts.get(i)]);
561
+ }
562
+ const b = n2(f.bottom);
563
+ const d = runs.map(
564
+ (r) => `M${n2(r[0][0])},${b} ${r.map(([x, y]) => `L${n2(x)},${n2(y)}`).join(" ")} L${n2(r.at(-1)[0])},${b} Z`
565
+ ).join(" ");
566
+ const fill = col.areaColor === void 0 ? stroke : att(col.areaColor, "sparkline areaColor");
567
+ body += `<path class="sdvt-area" d="${d}" fill="${fill}" fill-opacity="${col.areaOpacity}"/>`;
568
+ }
569
+ body += `<path class="sdvt-line" d="${path.join(" ")}" fill="none" stroke="${stroke}" stroke-width="${col.lineWidth}" stroke-linejoin="round" stroke-linecap="round"/>`;
570
+ body += statlineSvg(col, f, xs);
571
+ const at = pointsAt(values, idx);
572
+ for (const [i, c] of markColors(col.highlightPoints, at, idx, "sparkline highlightPoints")) {
573
+ const [x, y] = pts.get(i);
574
+ body += `<circle class="sdvt-point" cx="${n2(x)}" cy="${n2(y)}" r="${col.pointSize}" fill="${c}" stroke="${c}"/>`;
575
+ }
576
+ const endsOnly = col.labels.length > 0 && !col.labels.some((l) => l === "min" || l === "max" || l === "all");
577
+ const size = att(col.labelSize, "sparkline labelSize");
578
+ for (const i of labelled(col.labels, at, idx)) {
579
+ const [x, y] = pts.get(i);
580
+ const pos = endsOnly && i === at.first ? `x="${n2(x - 6)}" y="${n2(y)}" text-anchor="end" dominant-baseline="middle"` : endsOnly && i === at.last ? `x="${n2(x + 6)}" y="${n2(y)}" text-anchor="start" dominant-baseline="middle"` : `x="${n2(x)}" y="${n2(y - 7)}" text-anchor="middle"`;
581
+ body += `<text class="sdvt-label" ${pos} fill="currentColor" font-size="${size}">${fixed(values[i], col.decimals)}</text>`;
582
+ }
583
+ return chartSvg("sparkline", col.chartWidth, f.h, summary("Sparkline", col, xs), body);
584
+ }
585
+ function sparkbarSvg(col, values, color, row) {
586
+ const xs = present(values);
587
+ const idx = presentIdx(values);
588
+ const f = frameOf(col, xs, true, row);
589
+ const band = (f.right - f.left) / values.length;
590
+ const barW = band * 0.8;
591
+ const base = f.y(Math.min(Math.max(0, f.lo), f.hi));
592
+ const at = pointsAt(values, idx);
593
+ const marks = markColors(col.highlightBars, at, idx, "sparkbar highlightBars");
594
+ const fill = att(color, "sparkbar fillColor");
595
+ let body = bandlineSvg(col, f, xs);
596
+ const tops = /* @__PURE__ */ new Map();
597
+ for (const i of idx) {
598
+ const x = f.left + i * band + (band - barW) / 2;
599
+ const y = f.y(values[i]);
600
+ tops.set(i, [x + barW / 2, Math.min(y, base)]);
601
+ body += `<rect class="sdvt-bar" x="${n2(x)}" y="${n2(Math.min(y, base))}" width="${n2(barW)}" height="${n2(Math.abs(base - y))}" fill="${marks.get(i) ?? fill}" fill-opacity="${col.fillOpacity}"/>`;
602
+ }
603
+ body += statlineSvg(col, f, xs);
604
+ const size = att(col.labelSize, "sparkbar labelSize");
605
+ for (const i of labelled(col.labels, at, idx)) {
606
+ const [x, y] = tops.get(i);
607
+ body += `<text class="sdvt-label" x="${n2(x)}" y="${n2(y - 3)}" text-anchor="middle" fill="currentColor" font-size="${size}">${fixed(values[i], col.decimals)}</text>`;
608
+ }
609
+ return chartSvg("sparkbar", col.chartWidth, f.h, summary("Bar chart", col, xs), body);
610
+ }
611
+ function arc(c, outer, inner, frac) {
612
+ const t = Math.PI * (1 - frac);
613
+ const cos = Math.cos(t);
614
+ const sin = Math.sin(t);
615
+ return `M${n2(c - outer)},${n2(c)} A${n2(outer)},${n2(outer)} 0 0 1 ${n2(c + outer * cos)},${n2(c - outer * sin)} L${n2(c + inner * cos)},${n2(c - inner * sin)} A${n2(inner)},${n2(inner)} 0 0 0 ${n2(c - inner)},${n2(c)} Z`;
616
+ }
617
+ function gaugeSvg(col, v, lo, hi, color, fmt) {
618
+ const w = 50 * col.size;
619
+ const r = w / 2;
620
+ const inner = r * 0.75;
621
+ const frac = hi === lo ? 0 : Math.min(Math.max((Math.abs(v) - lo) / (hi - lo), 0), 1);
622
+ const ink = col.textColor === void 0 ? "currentColor" : att(col.textColor, "gauge textColor");
623
+ const weight = col.boldText ? ' font-weight="bold"' : "";
624
+ const font = col.textSize === void 0 ? "" : ` font-size="${att(col.textSize, "gauge textSize")}"`;
625
+ let body = `<path class="sdvt-track" d="${arc(r, r, inner, 1)}" fill="${att(col.background, "gauge background")}"/>`;
626
+ if (frac > 0)
627
+ body += `<path class="sdvt-fill" d="${arc(r, r, inner, frac)}" fill="${color}" fill-opacity="${col.opacity}"/>`;
628
+ body += `<text class="sdvt-value" x="${n2(r)}" y="${n2(r - 2)}" text-anchor="middle" fill="${ink}"${weight}${font}>${escapeHtml(fmt(v))}</text>`;
629
+ if (col.showMinMax) {
630
+ const y = n2(r + 10 * col.size);
631
+ body += `<text class="sdvt-min" x="0" y="${y}" text-anchor="start" fill="${ink}" font-size="0.7em">${escapeHtml(fmt(lo))}</text><text class="sdvt-max" x="${n2(w)}" y="${y}" text-anchor="end" fill="${ink}" font-size="0.7em">${escapeHtml(fmt(hi))}</text>`;
632
+ }
633
+ const gauged = v < 0 ? ` (its absolute value, ${fmt(Math.abs(v))}, is gauged)` : "";
634
+ const label = `Gauge: ${fmt(v)}${gauged} on a scale from ${fmt(lo)} to ${fmt(hi)}`;
635
+ return chartSvg("gauge", w, r + (col.showMinMax ? 12 * col.size : 0), label, body);
636
+ }
637
+
198
638
  // src/html/cells.ts
199
639
  var cellValue = (row, key) => row[key];
200
640
  var blankOr = (v) => isBlank(v) ? "" : escapeHtml(v);
@@ -219,9 +659,14 @@ function checkHeight(h, kind) {
219
659
  function teamIdsOf(spec, rows) {
220
660
  const out = /* @__PURE__ */ new Map();
221
661
  for (const c of spec.columns) {
222
- if (c.kind !== "logo" && c.kind !== "wordmark" && c.kind !== "mergeStackTeamColor" && c.kind !== "teamColorBar" && c.kind !== "teamColorBg")
662
+ checkRefs(c);
663
+ checkChart(c);
664
+ const optIn = "team" in c && c.kind !== "mergeStackTeamColor" && c.team !== void 0;
665
+ if (!optIn && c.kind !== "logo" && c.kind !== "wordmark" && c.kind !== "mergeStackTeamColor" && c.kind !== "teamColorBar" && c.kind !== "teamColorBg")
223
666
  continue;
224
- const from = c.kind === "mergeStackTeamColor" ? c.team : c.key;
667
+ if (c.league === void 0)
668
+ throw new TableSpecError(`column "${c.key}" names a team column, so it needs a league`);
669
+ const from = "team" in c && c.team !== void 0 ? c.team : c.key;
225
670
  const values = rows.map((r) => {
226
671
  const v = cellValue(r, from);
227
672
  return isBlank(v) ? null : String(v).trim();
@@ -244,6 +689,339 @@ var pillLabel = (col, v) => formatValue(
244
689
  col.suffix
245
690
  );
246
691
  var isNa = (col, v) => isBlank(v) || col.naStrings.some((n) => col.ignoreCase ? n.toLowerCase() === String(v).toLowerCase() : n === String(v));
692
+ var fmtNum = (n, f) => `${f?.prefix ?? ""}${formatValue(n * (f?.scale ?? 1), f?.digits, f?.type ?? "number", f?.suffix ?? "")}`;
693
+ var clamp01 = (x) => x < 0 ? 0 : x > 1 ? 1 : x;
694
+ function withAlpha(color, opacity, field) {
695
+ if (!(opacity >= 0 && opacity <= 1))
696
+ throw new TableSpecError(`${field} must be between 0 and 1, got ${opacity}`);
697
+ if (opacity === 1) return cssValue(color, field);
698
+ const a = Math.round(opacity * 255).toString(16).padStart(2, "0");
699
+ return `${hex6(color, { dropAlpha: true })}${a}`;
700
+ }
701
+ var STATS = /* @__PURE__ */ new WeakMap();
702
+ function statsOf(ctx, key) {
703
+ let byKey = STATS.get(ctx.domainRows);
704
+ if (!byKey) {
705
+ byKey = /* @__PURE__ */ new Map();
706
+ STATS.set(ctx.domainRows, byKey);
707
+ }
708
+ const hit = byKey.get(key);
709
+ if (hit) return hit;
710
+ const nums = ctx.domainRows.map((r) => toNumber(cellValue(r, key)));
711
+ const present2 = nums.filter((n) => n !== null);
712
+ const fin = present2.filter(Number.isFinite);
713
+ const st = {
714
+ nums,
715
+ min: present2.length ? Math.min(...present2) : Number.NaN,
716
+ max: present2.length ? Math.max(...present2) : Number.NaN,
717
+ maxAbs: present2.length ? Math.max(...present2.map(Math.abs)) : Number.NaN,
718
+ finite: fin.length ? [Math.min(...fin), Math.max(...fin), Math.max(...fin.map(Math.abs))] : [Number.NaN, Number.NaN, Number.NaN],
719
+ index: new Map(ctx.domainRows.map((r, j) => [r, j])),
720
+ tiles: /* @__PURE__ */ new Map()
721
+ };
722
+ byKey.set(key, st);
723
+ return st;
724
+ }
725
+ function tileOf(ctx, key, row, i, n) {
726
+ const st = statsOf(ctx, key);
727
+ const j = ctx.domainRows === ctx.rows ? i : st.index.get(row);
728
+ if (j === void 0) return ntile([...st.nums, toNumber(cellValue(row, key))], n).at(-1) ?? null;
729
+ let t = st.tiles.get(n);
730
+ if (!t) {
731
+ t = ntile(st.nums, n);
732
+ st.tiles.set(n, t);
733
+ }
734
+ return t[j] ?? null;
735
+ }
736
+ function sizeOf(v, st, minValue, maxValue) {
737
+ const top = maxValue ?? st.maxAbs;
738
+ const r = minValue === void 0 ? Math.abs(v) / top : (Math.abs(v) - minValue) / (top - minValue);
739
+ return Number.isFinite(r) ? clamp01(r) : 0;
740
+ }
741
+ var normalized = (v, lo, hi) => hi === lo ? 1 : clamp01((v - lo) / (hi - lo));
742
+ function dataColor(v) {
743
+ if (isBlank(v)) return void 0;
744
+ const s = String(v).trim();
745
+ const named = namedColor(s);
746
+ if (named !== void 0) return named;
747
+ try {
748
+ hex6(s, { dropAlpha: true });
749
+ return s;
750
+ } catch {
751
+ return void 0;
752
+ }
753
+ }
754
+ function refColor(col, row, i, ctx) {
755
+ if (col.colorRef !== void 0) {
756
+ const c = dataColor(cellValue(row, col.colorRef));
757
+ if (c !== void 0) return c;
758
+ }
759
+ if (col.team === void 0 || col.league === void 0) return void 0;
760
+ const id = ctx.teamIds.get(col.key)?.[i];
761
+ return id === void 0 ? void 0 : teamColorsSync(col.league, id, { which: col.which ?? "primary", idSystem: "team_id" });
762
+ }
763
+ function checkRefs(c) {
764
+ if (!("mark" in c) || c.mark === void 0) return;
765
+ if ((c.mark === "logo" || c.mark === "wordmark") && (c.team === void 0 || c.league === void 0))
766
+ throw new TableSpecError(
767
+ `column "${c.key}": mark "${c.mark}" reads the team column, so set team and league`
768
+ );
769
+ if (c.mark === "headshot" && (c.markKey === void 0 || c.league === void 0))
770
+ throw new TableSpecError(
771
+ `column "${c.key}": mark "headshot" reads an ESPN athlete id, so set markKey and league`
772
+ );
773
+ }
774
+ function markOf(col, row, i, ctx) {
775
+ if (col.mark === void 0 || col.league === void 0) return "";
776
+ const h = checkHeight(col.markHeight ?? 20, `${col.kind} mark`);
777
+ if (col.mark === "headshot") {
778
+ const raw = col.markKey === void 0 ? null : cellValue(row, col.markKey);
779
+ if (isBlank(raw)) return "";
780
+ const url2 = headshotUrl(String(raw).trim(), col.league, { idSystem: "espn" });
781
+ return url2 ? markImg(url2, String(raw).trim(), h) : "";
782
+ }
783
+ const id = ctx.teamIds.get(col.key)?.[i];
784
+ if (id === void 0) return "";
785
+ const url = logoUrlSync(id, col.league, {
786
+ ...col.season !== void 0 ? { season: col.season } : {},
787
+ markType: col.mark,
788
+ idSystem: "team_id"
789
+ });
790
+ return url ? markImg(url, teamNameSync(col.league, id) ?? id, h, id) : "";
791
+ }
792
+ function dataBar(col, row, i, ctx) {
793
+ const raw = cellValue(row, col.key);
794
+ const v = toNumber(raw);
795
+ if (v === null) return blankOr(raw);
796
+ if (col.fill.length === 0) throw new TableSpecError("dataBar fill needs at least one colour");
797
+ if (col.gradient && col.fill.length < 2)
798
+ throw new TableSpecError("dataBar gradient needs at least two colours in fill");
799
+ const st = statsOf(ctx, col.key);
800
+ const pos = col.textPosition;
801
+ const h = col.barHeight !== void 0 ? checkPx(col.barHeight, "dataBar barHeight") : pos === "above" ? 5 : 18;
802
+ const radius = col.roundEdges ? h / 2 : 0;
803
+ const posNeg = col.posNeg ?? st.min < 0;
804
+ const ref = refColor(col, row, i, ctx);
805
+ const fill = ref ?? (posNeg && col.fill.length === 2 ? col.fill[v < 0 ? 0 : 1] : col.fill.length === 1 ? col.fill[0] : ramp(
806
+ col.fill.map((c) => hex6(c)),
807
+ [0, 1],
808
+ col.bias
809
+ )(normalized(v, st.min, st.max)));
810
+ const gradient = col.gradient ? `linear-gradient(to right, ${col.fill.join(", ")})` : void 0;
811
+ const inside = pos === "inside-end" || pos === "inside-base" || pos === "center";
812
+ const inkOn = col.gradient ? col.fill.at(v < 0 ? 0 : -1) : fill;
813
+ const ink = pos === "none" ? "transparent" : col.textColor ?? (inside ? onColor(solid(inkOn)) : void 0);
814
+ const label = escapeHtml(fmtNum(v, col.numberFormat));
815
+ const m = markOf(col, row, i, ctx);
816
+ const mark = m ? `<span style="display:inline-flex;margin:0 4px">${m}</span>` : "";
817
+ const right = col.align === "right";
818
+ const width = Number((sizeOf(v, st, col.minValue, col.maxValue) * 100).toFixed(4));
819
+ const bar = (dir2, text, gap) => `<div style="${styleAttr({
820
+ display: "flex",
821
+ "align-items": "center",
822
+ "flex-direction": dir2,
823
+ "justify-content": pos === "center" ? "center" : pos === "inside-base" ? "flex-start" : "flex-end",
824
+ background: gradient ? void 0 : fill,
825
+ "background-image": gradient,
826
+ "border-radius": `${radius}px`,
827
+ color: text ? ink : void 0,
828
+ width: `${width}%`,
829
+ height: `${h}px`,
830
+ "white-space": "nowrap",
831
+ [dir2 === "row" ? "margin-right" : "margin-left"]: gap ? "6px" : void 0
832
+ })}">${text ? `<span>${label}</span>` : ""}</div>`;
833
+ const textSpan = `<span${ink ? ` style="color:${escapeAttr(cssValue(ink, "dataBar textColor"))}"` : ""}>${label}</span>`;
834
+ const group = (dir2) => inside || pos === "none" ? `<div style="display:flex;align-items:center;flex-direction:${dir2};width:100%">${bar(dir2, true, false)}${mark}</div>` : `<div style="display:flex;align-items:center;flex-direction:${dir2};width:100%">${bar(dir2, false, true)}${mark}${textSpan}</div>`;
835
+ if (posNeg) {
836
+ const half = (side) => `<div style="flex:1 1 0;display:flex;align-items:center${side === "neg" ? ";justify-content:flex-end" : ""}">${v < 0 === (side === "neg") ? group(side === "neg" ? "row-reverse" : "row") : ""}</div>`;
837
+ return `<div class="sdvt-databar" style="display:flex;align-items:stretch;min-height:${h}px">${half("neg")}<div style="width:1px;background:#666666"></div>${half("pos")}</div>`;
838
+ }
839
+ const dir = right ? "row-reverse" : "row";
840
+ const track = (inner, gap = false) => `<div style="${styleAttr({ "flex-grow": 1, "border-radius": `${radius}px`, background: col.background, [right ? "margin-right" : "margin-left"]: gap ? "6px" : void 0 })}">${inner}</div>`;
841
+ if (pos === "above")
842
+ return `<div class="sdvt-databar"><div style="text-align:${right ? "right" : "left"}${ink ? `;color:${escapeAttr(cssValue(ink, "dataBar textColor"))}` : ""}">${label}</div>${track(`<div style="display:flex;align-items:center;flex-direction:${dir}">${bar(dir, false, false)}${mark}</div>`)}</div>`;
843
+ if (pos === "outside-base") {
844
+ const chars = Math.max(
845
+ 1,
846
+ ...st.nums.filter((n) => n !== null).map((n) => fmtNum(n, col.numberFormat).length)
847
+ );
848
+ return `<div class="sdvt-databar" style="display:flex;align-items:center;flex-direction:${dir}"><span style="${styleAttr({ "min-width": `${chars}ch`, "text-align": right ? "left" : "right", "font-variant-numeric": "tabular-nums", color: ink })}">${label}</span>${track(`<div style="display:flex;align-items:center;flex-direction:${dir}">${bar(dir, false, false)}${mark}</div>`, true)}</div>`;
849
+ }
850
+ return `<div class="sdvt-databar" style="display:flex">${track(group(dir))}</div>`;
851
+ }
852
+ function iconSet(col, row, i, ctx) {
853
+ const raw = cellValue(row, col.key);
854
+ const v = toNumber(raw);
855
+ if (v === null) return blankOr(raw);
856
+ const named = col.iconRef === void 0 ? void 0 : cellValue(row, col.iconRef);
857
+ const icon = !isBlank(named) && ICON_NAMES.includes(String(named).trim()) ? String(named).trim() : col.icons[(tileOf(ctx, col.key, row, i, col.icons.length) ?? 1) - 1];
858
+ const color = (col.iconColorRef === void 0 ? void 0 : dataColor(cellValue(row, col.iconColorRef))) ?? col.colors[(tileOf(ctx, col.key, row, i, col.colors.length) ?? 1) - 1];
859
+ const svg = iconSvg(
860
+ icon,
861
+ withAlpha(color, col.opacity, "iconSet colors"),
862
+ checkPx(col.iconSize, "iconSet iconSize")
863
+ );
864
+ const label = escapeHtml(fmtNum(v, col.numberFormat));
865
+ switch (col.iconPosition) {
866
+ case "left":
867
+ return `<span style="display:inline-block;margin-right:8px">${svg}</span>${label}`;
868
+ case "above":
869
+ return `<div>${svg}</div><div>${label}</div>`;
870
+ case "below":
871
+ return `<div>${label}</div><div>${svg}</div>`;
872
+ case "over":
873
+ return `${svg}<span style="font-size:0;color:transparent">${label}</span>`;
874
+ default:
875
+ return `${label}<span style="display:inline-block;margin-left:8px">${svg}</span>`;
876
+ }
877
+ }
878
+ function iconAssign(col, row, i, ctx) {
879
+ const raw = cellValue(row, col.key);
880
+ const v = toNumber(raw);
881
+ if (v === null) return blankOr(raw);
882
+ const size = checkPx(col.iconSize, "iconAssign iconSize");
883
+ const on = withAlpha(col.fill, col.fillOpacity, "iconAssign fill");
884
+ const off = withAlpha(col.empty, col.emptyOpacity, "iconAssign empty");
885
+ const half = (x) => Math.floor(x + 0.5);
886
+ let steps;
887
+ let filled;
888
+ let title;
889
+ if (col.buckets !== void 0) {
890
+ const b = tileOf(ctx, col.key, row, i, col.buckets) ?? 0;
891
+ steps = Array.from({ length: col.buckets }, (_, k) => k + 1);
892
+ filled = (s) => s <= half(b);
893
+ title = `${b} out of ${col.buckets}`;
894
+ } else {
895
+ if (!(col.seqBy > 0)) throw new TableSpecError(`iconAssign seqBy must be positive, got ${col.seqBy}`);
896
+ const top = half(statsOf(ctx, col.key).max);
897
+ const n = half(v);
898
+ steps = [];
899
+ if (top === 0) steps.push(0);
900
+ else for (let s = 1; s <= top; s += col.seqBy) steps.push(s);
901
+ if (steps.length > 1e3)
902
+ throw new TableSpecError(`iconAssign would draw ${steps.length} icons per cell; set buckets or seqBy`);
903
+ filled = top === 0 ? (s) => s < n : (s) => s <= n;
904
+ title = `${naturalDigits(v)} out of ${top}`;
905
+ }
906
+ const icons = steps.map((s) => iconSvg(col.icon, filled(s) ? on : off, size)).join("");
907
+ const box = `<span title="${escapeAttr(title)}" aria-label="${escapeAttr(title)}" role="img">${icons}</span>`;
908
+ const label = escapeHtml(fmtNum(v, col.numberFormat));
909
+ switch (col.showValues) {
910
+ case "right":
911
+ return `${box}<span style="margin-left:6px">${label}</span>`;
912
+ case "left":
913
+ return `<span style="margin-right:6px">${label}</span>${box}`;
914
+ case "above":
915
+ return `<div>${label}</div><div>${box}</div>`;
916
+ case "below":
917
+ return `<div>${box}</div><div>${label}</div>`;
918
+ default:
919
+ return box;
920
+ }
921
+ }
922
+ function mergeStack(col, row, i, ctx) {
923
+ const main = `font-size:${checkPx(col.size, "mergeStack size")}px;${styleAttr({ color: col.color, "font-weight": col.weight, "font-style": col.style })}`;
924
+ const sub = `font-size:${checkPx(col.stackSize, "mergeStack stackSize")}px;${styleAttr({ color: col.stackColor, "font-weight": col.stackWeight, "font-style": col.stackStyle })}`;
925
+ const gap = checkPx(col.spacing, "mergeStack spacing") + 3;
926
+ const a = blankOr(cellValue(row, col.key));
927
+ const b = blankOr(cellValue(row, col.stack));
928
+ const body = col.position === "below" ? `<div><div style="${main};margin-bottom:${gap}px">${a}</div><div style="${sub}">${b}</div></div>` : col.position === "above" ? `<div><div style="${sub};margin-bottom:${gap}px">${b}</div><div style="${main}">${a}</div></div>` : col.position === "left" ? `<span style="${sub};margin-right:${gap}px">${b}</span><span style="${main}">${a}</span>` : `<span style="${main};margin-right:${gap}px">${a}</span><span style="${sub}">${b}</span>`;
929
+ const m = markOf(col, row, i, ctx);
930
+ const stacked = col.position === "below" || col.position === "above";
931
+ return m ? `<div style="display:flex;align-items:center;gap:6px">${m}${stacked ? body : `<div>${body}</div>`}</div>` : body;
932
+ }
933
+ function bubble(col, row, i, ctx) {
934
+ const raw = cellValue(row, col.key);
935
+ const v = toNumber(raw);
936
+ if (v === null) return blankOr(raw);
937
+ if (col.colors.length === 0) throw new TableSpecError("bubble colors needs at least one colour");
938
+ const st = statsOf(ctx, col.key);
939
+ const px = Number(
940
+ (sizeOf(v, st, col.minValue, col.maxValue) * checkPx(col.maxSize, "bubble maxSize")).toFixed(2)
941
+ );
942
+ const base = refColor(col, row, i, ctx) ?? ramp(
943
+ col.colors.map((c) => hex6(c)),
944
+ [0, 1],
945
+ col.bias
946
+ )(normalized(v, col.minValue ?? st.min, col.maxValue ?? st.max));
947
+ const fill = withAlpha(base, col.opacity, "bubble opacity");
948
+ const ink = col.showText ? col.textColor ?? onColor(solid(fill, tableBg(ctx))) : "transparent";
949
+ return `<div style="${styleAttr({
950
+ background: fill,
951
+ color: ink,
952
+ display: "inline-flex",
953
+ "justify-content": "center",
954
+ "align-items": "center",
955
+ "text-align": "center",
956
+ height: `${px}px`,
957
+ width: `${px}px`,
958
+ "border-radius": col.shape === "circles" ? "50%" : void 0
959
+ })}">${escapeHtml(fmtNum(v, col.numberFormat))}</div>`;
960
+ }
961
+ function chartColor(col, row, i, ctx) {
962
+ if (col.colorRef !== void 0) {
963
+ const c = dataColor(cellValue(row, col.colorRef));
964
+ if (c !== void 0) return c;
965
+ }
966
+ const id = col.team === void 0 ? void 0 : ctx.teamIds.get(col.key)?.[i];
967
+ if (id === void 0 || col.league === void 0) return void 0;
968
+ const which = col.which ?? "primary";
969
+ const own = teamColorsSync(col.league, id, { which, idSystem: "team_id" });
970
+ const other = teamColorsSync(col.league, id, {
971
+ which: which === "primary" ? "secondary" : "primary",
972
+ idSystem: "team_id"
973
+ });
974
+ const first = own ?? other;
975
+ return first === void 0 ? void 0 : readableInk(first, own === void 0 ? void 0 : other, tableBg(ctx), 3);
976
+ }
977
+ function seriesOf(v) {
978
+ if (!Array.isArray(v)) return null;
979
+ return v.map((x) => {
980
+ const n = typeof x === "number" || typeof x === "string" ? toNumber(x) : null;
981
+ return n !== null && Number.isFinite(n) ? n : null;
982
+ });
983
+ }
984
+ function seriesCell(col, row, i, ctx) {
985
+ const values = seriesOf(cellValue(row, col.key));
986
+ if (values === null || values.every((v) => v === null)) return "";
987
+ const color = chartColor(col, row, i, ctx);
988
+ return col.kind === "sparkline" ? sparklineSvg(col, values, color ?? col.lineColor, i) : sparkbarSvg(col, values, color ?? col.fillColor, i);
989
+ }
990
+ function gauge(col, row, i, ctx) {
991
+ const raw = cellValue(row, col.key);
992
+ const v = toNumber(raw);
993
+ if (v === null) return blankOr(raw);
994
+ if (!Number.isFinite(v)) return "";
995
+ const [cmin, cmax, cabs] = statsOf(ctx, col.key).finite;
996
+ const lo = col.minValue ?? 0;
997
+ const hi = col.maxValue ?? (Number.isFinite(cabs) ? cabs : Math.abs(v));
998
+ if (lo > hi)
999
+ throw new TableSpecError(
1000
+ `column "${col.key}" row ${i}: gauge minValue ${naturalDigits(lo)} is above the column's largest absolute value ${naturalDigits(hi)}; give maxValue too`
1001
+ );
1002
+ const color = chartColor(col, row, i, ctx) ?? (col.fill.length === 1 ? col.fill[0] : ramp(
1003
+ col.fill.map((c) => hex6(c)),
1004
+ [0, 1],
1005
+ col.bias
1006
+ )(normalized(v, Number.isFinite(cmin) ? cmin : v, Number.isFinite(cmax) ? cmax : v)));
1007
+ return gaugeSvg(
1008
+ col,
1009
+ v,
1010
+ lo,
1011
+ hi,
1012
+ escapeAttr(cssValue(color, "gauge fill")),
1013
+ (n) => fmtNum(n, col.numberFormat)
1014
+ );
1015
+ }
1016
+ function extremeOf(col, row, ctx) {
1017
+ if (col.extreme === void 0) return col.when !== void 0 && matches(col.when, row) ? "max" : null;
1018
+ const v = toNumber(cellValue(row, col.key));
1019
+ if (v === null) return null;
1020
+ const st = statsOf(ctx, col.key);
1021
+ if (col.extreme !== "max" && v === st.min) return "min";
1022
+ if (col.extreme !== "min" && v === st.max) return "max";
1023
+ return null;
1024
+ }
247
1025
  var isScaled = (col) => col.kind === "colorPills" || col.kind === "colorRanks" || col.kind === "percentileBar";
248
1026
  function columnScales(spec, rows, warnFn, domainRows = rows) {
249
1027
  const at = domainRows === rows ? null : new Map(domainRows.map((r, j) => [r, j]));
@@ -260,19 +1038,31 @@ function columnScales(spec, rows, warnFn, domainRows = rows) {
260
1038
  const nums = domainRows.map((r) => toNumber(cellValue(r, col.key)));
261
1039
  let values = nums;
262
1040
  let own = (n) => n;
1041
+ let spanned;
1042
+ let buckets;
263
1043
  if (col.kind === "colorPills" && col.fillType === "rank") {
264
1044
  values = averageRanks(nums, col.rankOrder === "desc");
265
1045
  own = () => null;
266
1046
  }
1047
+ if (col.kind === "colorRanks" && col.evenBreaks) {
1048
+ buckets = col.palette.map((c) => hex6(c));
1049
+ values = ntile(nums, buckets.length);
1050
+ own = () => null;
1051
+ } else if (col.kind === "colorRanks" && col.span && col.domain === void 0) {
1052
+ const keys = col.span === true ? [...new Set(domainRows.flatMap((r) => Object.keys(r)))].filter(
1053
+ (k) => domainRows.some((r) => typeof cellValue(r, k) === "number")
1054
+ ) : col.span;
1055
+ spanned = domainOf(keys.map((k) => domainRows.map((r) => toNumber(cellValue(r, k)))));
1056
+ }
267
1057
  if (col.kind === "percentileBar") {
268
- const present = nums.filter((n) => n !== null);
1058
+ const present2 = nums.filter((n) => n !== null);
269
1059
  const [lo, hi] = col.domain;
270
1060
  const scale = col.scale;
271
- const proportion = scale === "auto" && present.length > 0 && present.every((n) => n >= 0 && n <= 1) && hi > 1;
1061
+ const proportion = scale === "auto" && present2.length > 0 && present2.every((n) => n >= 0 && n <= 1) && hi > 1;
272
1062
  own = (n) => n === null ? null : typeof scale === "number" ? n * scale : proportion ? lo + n * (hi - lo) : n;
273
1063
  values = nums.map(own);
274
1064
  }
275
- const domain = col.domain ?? domainOf([values]);
1065
+ const domain = buckets ? [1, buckets.length] : col.domain ?? spanned ?? domainOf([values]);
276
1066
  const fmt = () => `(${naturalDigits(domain[0])} to ${naturalDigits(domain[1])})`;
277
1067
  if (col.kind === "colorPills" && col.domain === void 0)
278
1068
  warnFn(
@@ -280,7 +1070,8 @@ function columnScales(spec, rows, warnFn, domainRows = rows) {
280
1070
  `no domain given, so the colors span the observed range ${fmt()}; set domain to compare colors across tables or columns`
281
1071
  );
282
1072
  const palette = col.palette.map((c) => hex6(c));
283
- const color = ramp(col.reverse ? [...palette].reverse() : palette, domain);
1073
+ const ordered = col.reverse ? [...palette].reverse() : palette;
1074
+ const color = buckets ? (b) => ordered[b - 1] ?? null : ramp(ordered, domain, col.kind === "colorRanks" ? col.bias ?? 1 : 1);
284
1075
  const outside = values.filter((v) => v !== null && color(v) === null).length;
285
1076
  if (col.kind === "colorPills" && outside > 0)
286
1077
  warnFn(
@@ -308,7 +1099,7 @@ function renderCell(col, row, i, ctx) {
308
1099
  const n = toNumber(cellValue(row, col.key));
309
1100
  if (n === null) return "";
310
1101
  const sc = ctx.scaled.get(col.key);
311
- if (sc) return escapeHtml(formatNumber(n / sc.divisor, { digits: sc.decimals, big: true }));
1102
+ if (sc) return escapeHtml(scaledNumber(n, sc));
312
1103
  return escapeHtml(
313
1104
  col.kind === "int" ? formatNumber(n, { digits: 0, big: true }) : formatNumber(n, {
314
1105
  ...col.digits !== void 0 ? { digits: col.digits } : {},
@@ -407,7 +1198,7 @@ function renderCell(col, row, i, ctx) {
407
1198
  fill = col.naColor;
408
1199
  text = "";
409
1200
  } else {
410
- fill = sc.color(s) ?? "#808080";
1201
+ fill = refColor(col, row, i, ctx) ?? sc.color(s) ?? "#808080";
411
1202
  text = pillLabel(col, toNumber(cellValue(row, col.key)));
412
1203
  }
413
1204
  const ink = col.textColor ?? onColor(solid(fill, tableBg(ctx)));
@@ -458,6 +1249,22 @@ function renderCell(col, row, i, ctx) {
458
1249
  checkHeight(Number.parseFloat(col.height), "image")
459
1250
  );
460
1251
  }
1252
+ // ---- reactablefmtr-style kinds
1253
+ case "dataBar":
1254
+ return dataBar(col, row, i, ctx);
1255
+ case "iconSet":
1256
+ return iconSet(col, row, i, ctx);
1257
+ case "iconAssign":
1258
+ return iconAssign(col, row, i, ctx);
1259
+ case "mergeStack":
1260
+ return mergeStack(col, row, i, ctx);
1261
+ case "bubble":
1262
+ return bubble(col, row, i, ctx);
1263
+ case "sparkline":
1264
+ case "sparkbar":
1265
+ return seriesCell(col, row, i, ctx);
1266
+ case "gauge":
1267
+ return gauge(col, row, i, ctx);
461
1268
  default: {
462
1269
  const bad = col;
463
1270
  throw new TableSpecError(`unknown column kind ${String(bad.kind)}`);
@@ -478,15 +1285,21 @@ function kindCellStyle(col, row, i, ctx) {
478
1285
  case "colorRanks": {
479
1286
  const sc = ctx.scales.get(col.key);
480
1287
  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))) });
1288
+ const ref = s === null ? void 0 : refColor(col, row, i, ctx);
1289
+ const fill = ref ?? (s === null ? null : sc?.color(s)) ?? "#ffffff";
1290
+ return styleAttr({
1291
+ "background-color": fill,
1292
+ color: col.showText === false ? "transparent" : onColor(solid(fill, tableBg(ctx)))
1293
+ });
483
1294
  }
484
- case "highlight":
485
- return matches(col.when, row) ? styleAttr({
1295
+ case "highlight": {
1296
+ const hit = extremeOf(col, row, ctx);
1297
+ return hit ? styleAttr({
486
1298
  "background-color": col.fill,
487
- color: col.textColor,
1299
+ color: col.textColor ?? (col.extreme === void 0 ? void 0 : hit === "min" ? "red" : "green"),
488
1300
  "font-weight": col.bold ? "bold" : void 0
489
1301
  }) : "";
1302
+ }
490
1303
  case "highlightNa":
491
1304
  return isNa(col, cellValue(row, col.key)) ? styleAttr({
492
1305
  "background-color": col.fill,
@@ -499,6 +1312,99 @@ function kindCellStyle(col, row, i, ctx) {
499
1312
  }
500
1313
  }
501
1314
 
1315
+ // src/html/legend-icon.ts
1316
+ var LEGEND_ICON_SETS = {
1317
+ "ski rating": [
1318
+ ["circle", "#39b54a", "Easy"],
1319
+ ["square", "#0f75bc", "Moderate"],
1320
+ ["diamond", "#000000", "Difficult"],
1321
+ // Font Awesome "square" turned 45 degrees
1322
+ ["double-diamond", "#000000", "Most Difficult"]
1323
+ ],
1324
+ medals: [
1325
+ ["medal", "#A77044", "Bronze"],
1326
+ ["medal", "#D7D7D7", "Silver"],
1327
+ ["medal", "#D6AF36", "Gold"]
1328
+ ],
1329
+ batteries: [
1330
+ ["battery-quarter", "#d7191c", "0-25%"],
1331
+ ["battery-half", "#fdae61", "25-50%"],
1332
+ ["battery-three-quarters", "#a6d96a", "50-75%"],
1333
+ ["battery-full", "#1a9641", "75-100%"]
1334
+ ]
1335
+ };
1336
+ var battery = (level) => `<rect x="0.75" y="4.25" width="12.5" height="7.5" rx="1" fill="none" stroke="currentColor" stroke-width="1.5"/><rect x="13.75" y="6.5" width="1.75" height="3" fill="currentColor"/><rect x="2.5" y="6" width="${9 * level}" height="4" fill="currentColor"/>`;
1337
+ var SHAPES = {
1338
+ circle: [16, `<circle cx="8" cy="8" r="7" fill="currentColor"/>`],
1339
+ square: [16, `<rect x="1" y="1" width="14" height="14" fill="currentColor"/>`],
1340
+ diamond: [16, `<path d="M8 .5 15.5 8 8 15.5 .5 8Z" fill="currentColor"/>`],
1341
+ "double-diamond": [
1342
+ 30,
1343
+ `<path d="M8 .5 15.5 8 8 15.5 .5 8ZM22 .5 29.5 8 22 15.5 14.5 8Z" fill="currentColor"/>`
1344
+ ],
1345
+ "triangle-up": [16, `<path d="M8 1.5 15 14.5H1Z" fill="currentColor"/>`],
1346
+ "triangle-down": [16, `<path d="M1 1.5H15L8 14.5Z" fill="currentColor"/>`],
1347
+ medal: [
1348
+ 16,
1349
+ `<path d="M3 0h3.5l2 5.5H5ZM13 0H9.5l-2 5.5H11Z" fill="currentColor"/><circle cx="8" cy="10.5" r="5" fill="currentColor"/>`
1350
+ ],
1351
+ "battery-quarter": [16, battery(0.25)],
1352
+ "battery-half": [16, battery(0.5)],
1353
+ "battery-three-quarters": [16, battery(0.75)],
1354
+ "battery-full": [16, battery(1)]
1355
+ };
1356
+ function legendIconSvg(shape, color, size) {
1357
+ const s = Object.hasOwn(SHAPES, shape) ? SHAPES[shape] : void 0;
1358
+ if (!s)
1359
+ throw new TableSpecError(
1360
+ `legendIcon icon must be one of ${Object.keys(SHAPES).join(", ")}, not "${shape}"`
1361
+ );
1362
+ const [w, body] = s;
1363
+ return `<svg class="sdvt-legend-icon" viewBox="0 0 ${w} 16" width="${size * w / 16}" height="${size}" aria-hidden="true" focusable="false" style="color:${escapeAttr(cssValue(color, "legendIcon colors"))};vertical-align:middle">${body}</svg>`;
1364
+ }
1365
+ function legendIconBlock(d) {
1366
+ if (d.align !== "left" && d.align !== "right")
1367
+ throw new TableSpecError(`legendIcon align must be left or right, not "${String(d.align)}"`);
1368
+ if (d.location !== "top" && d.location !== "bottom")
1369
+ throw new TableSpecError(`legendIcon location must be top or bottom, not "${String(d.location)}"`);
1370
+ const size = checkPx(d.iconSize, "legendIcon iconSize");
1371
+ if (size <= 0) throw new TableSpecError(`legendIcon iconSize must be positive, got ${size}`);
1372
+ let items;
1373
+ if (d.iconSet !== void 0 && d.icons !== void 0)
1374
+ throw new TableSpecError("legendIcon takes an iconSet or icons, not both");
1375
+ if (d.iconSet !== void 0) {
1376
+ const set = Object.hasOwn(LEGEND_ICON_SETS, d.iconSet) ? LEGEND_ICON_SETS[d.iconSet] : void 0;
1377
+ if (!set)
1378
+ throw new TableSpecError(
1379
+ `legendIcon iconSet must be "ski rating", "medals" or "batteries", not "${d.iconSet}"`
1380
+ );
1381
+ if (d.labels && d.labels.length !== set.length)
1382
+ throw new TableSpecError(
1383
+ `legendIcon iconSet "${d.iconSet}" needs ${set.length} labels, got ${d.labels.length}`
1384
+ );
1385
+ items = set.map(([shape, color, label], i) => [shape, color, d.labels?.[i] ?? label]);
1386
+ } else {
1387
+ if (!d.icons || d.icons.length === 0) throw new TableSpecError("legendIcon needs an iconSet or icons");
1388
+ const colors = d.colors && d.colors.length > 0 ? d.colors : ["currentColor"];
1389
+ if (d.labels && d.labels.length !== d.icons.length)
1390
+ throw new TableSpecError(
1391
+ `legendIcon needs one label per icon: ${d.icons.length} icons, ${d.labels.length} labels`
1392
+ );
1393
+ if (!d.labels && d.showLabels)
1394
+ throw new TableSpecError("legendIcon icons need labels when showLabels is true");
1395
+ items = d.icons.map((shape, i) => [
1396
+ shape,
1397
+ colors[i % colors.length],
1398
+ d.labels?.[i] ?? shape.replace(/-/g, " ")
1399
+ ]);
1400
+ }
1401
+ const icons = items.map(
1402
+ ([shape, color, label]) => d.showLabels ? `<span class="sdvt-legend-item" style="display:inline-flex;align-items:center;gap:4px">${legendIconSvg(shape, color, size)}${escapeHtml(label)}</span>` : `<span class="sdvt-legend-item" role="img" aria-label="${escapeAttr(label)}">${legendIconSvg(shape, color, size)}</span>`
1403
+ ).join("");
1404
+ const justify = d.align === "left" ? "flex-start" : "flex-end";
1405
+ return `<div class="sdvt-legend sdvt-legend-icon sdvt-legend-${d.location}" style="text-align:${d.align}">${d.title ? `<div class="sdvt-legend-title" style="margin-bottom:8px;font-weight:bold;font-size:90%">${escapeHtml(d.title)}</div>` : ""}<div style="display:flex;flex-wrap:wrap;justify-content:${justify};align-items:center;gap:4px 10px;font-size:13px;word-spacing:1px">${icons}</div>${d.footer ? `<div class="sdvt-legend-sub" style="font-size:70%;color:var(--sdvt-muted)">${escapeHtml(d.footer)}</div>` : ""}</div>`;
1406
+ }
1407
+
502
1408
  // src/html/social-icons.ts
503
1409
  var X = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/></svg>';
504
1410
  var IG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z"/></svg>';
@@ -523,34 +1429,6 @@ var SOCIAL_ICONS = {
523
1429
  mail: EMAIL
524
1430
  };
525
1431
 
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
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
1432
  // src/html/decorations.ts
555
1433
  function strwrap(words, width) {
556
1434
  const w = Math.max(width - 1, 1);
@@ -624,6 +1502,17 @@ var SCALE_NAMES = {
624
1502
  1e9: ["billions", "(billions)"],
625
1503
  1e12: ["trillions", "(trillions)"]
626
1504
  };
1505
+ function scaleNoteOf(d) {
1506
+ if (!(Number.isFinite(d.divisor) && d.divisor !== 0))
1507
+ throw new TableSpecError(`scaleNote divisor must be a single non-zero number, got ${d.divisor}`);
1508
+ if (d.columns.length === 0) throw new TableSpecError("scaleNote columns matched no columns");
1509
+ const named = SCALE_NAMES[d.divisor];
1510
+ const shown = Number.isInteger(d.divisor) ? formatNumber(d.divisor, { digits: 0, big: true }) : naturalDigits(d.divisor, true);
1511
+ return {
1512
+ note: d.note ?? (named ? `Figures in ${named[0]}.` : `Figures divided by ${shown}.`),
1513
+ suffix: d.labelSuffix ?? (named ? named[1] : `(\xF7${shown})`)
1514
+ };
1515
+ }
627
1516
  function applyDecorations(spec, rows, ctx) {
628
1517
  const none = () => "";
629
1518
  const out = {
@@ -888,14 +1777,8 @@ function applyDecorations(spec, rows, ctx) {
888
1777
  break;
889
1778
  }
890
1779
  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");
1780
+ const { note, suffix } = scaleNoteOf(d);
894
1781
  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
1782
  if (d.where !== "label") out.foot.push(escapeHtml(note));
900
1783
  if (d.where !== "sourceNote") {
901
1784
  const prev = out.labelSuffix;
@@ -1011,6 +1894,12 @@ function applyDecorations(spec, rows, ctx) {
1011
1894
  else out.after += block;
1012
1895
  break;
1013
1896
  }
1897
+ case "legendIcon": {
1898
+ const block = legendIconBlock(d);
1899
+ if (d.location === "top") out.before += block;
1900
+ else out.after += block;
1901
+ break;
1902
+ }
1014
1903
  case "significance": {
1015
1904
  if (d.levels.length !== d.symbols.length)
1016
1905
  throw new TableSpecError("significance levels and symbols must be the same length");
@@ -1208,7 +2097,10 @@ function checkKeys(spec, rows) {
1208
2097
  col.key,
1209
2098
  ..."to" in col ? [col.to] : [],
1210
2099
  ..."keys" in col ? col.keys : [],
1211
- ..."stack" in col ? [col.stack, col.team] : []
2100
+ ...["stack", "team", "colorRef", "markKey", "iconRef", "iconColorRef"].flatMap((f) => {
2101
+ const k = col[f];
2102
+ return typeof k === "string" ? [k] : [];
2103
+ })
1212
2104
  ];
1213
2105
  for (const k of need)
1214
2106
  if (!keys.has(k))
@@ -1228,7 +2120,11 @@ function alignOf(col) {
1228
2120
  case "tally":
1229
2121
  case "colorPills":
1230
2122
  case "colorRanks":
2123
+ case "iconSet":
1231
2124
  return "right";
2125
+ case "bubble":
2126
+ case "gauge":
2127
+ return "center";
1232
2128
  case "logo":
1233
2129
  case "wordmark":
1234
2130
  case "headshot":
@@ -1323,8 +2219,16 @@ function renderParts(input, rows, opts = {}) {
1323
2219
  const currentAria = isCurrent ? ' aria-current="true"' : "";
1324
2220
  return `<th scope="col" class="sdvt-label sdvt-${escapeAttr(alignOf(c))}${current}" data-col="${escapeAttr(c.key)}" data-kind="${escapeAttr(c.kind)}"${currentAria}${aria}${styleOf([c.width ? `width:${escapeAttr(cssValue(c.width, `column ${c.key} width`))}` : "", deco.labelStyle(c.key)])}>${inner}</th>`;
1325
2221
  }).join("");
2222
+ const cellOf = (c, row, i) => {
2223
+ const html = renderCell(c, row, i, ctx);
2224
+ if (c.tooltip !== true) return `>${html}`;
2225
+ const text = html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
2226
+ const raw = cellValue(row, c.key);
2227
+ const title = text || (raw === null || raw === void 0 ? "" : escapeAttr(raw));
2228
+ return title ? ` title="${title}">${html}` : `>${html}`;
2229
+ };
1326
2230
  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>`
2231
+ (c) => `<td class="sdvt-cell sdvt-kind-${escapeAttr(c.kind)} sdvt-${escapeAttr(alignOf(c))}" data-col="${escapeAttr(c.key)}"${styleOf([kindCellStyle(c, row, i, ctx), deco.cellStyle(i, c.key)])}${cellOf(c, row, i)}${deco.cellSuffix(i, c.key)}</td>`
1328
2232
  ).join("");
1329
2233
  const striped = theme.tokens.stripe !== "transparent";
1330
2234
  let shown = 0;
@@ -1406,223 +2310,6 @@ function renderParts(input, rows, opts = {}) {
1406
2310
  }
1407
2311
  var fontsLinkFor = (spec) => renderParts(spec, []).link;
1408
2312
 
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
2313
+ export { ICON_NAMES, alignOf, assemble, attrsText, escapeAttr, escapeHtml, fontsLink, fontsLinkFor, labelOf, loadTeamNames, renderParts, scaleNoteOf, sortAria, styleSheet, tableHTML, tableId, teamNameSync, themeKey };
2314
+ //# sourceMappingURL=chunk-R375P3MN.js.map
2315
+ //# sourceMappingURL=chunk-R375P3MN.js.map