@sportsdataverse/sdvplot 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +109 -0
- package/README.md +984 -2
- package/dist/_web-D7Aa5Rd8.d.ts +69 -0
- package/dist/aaf-GJ3SH34T.js +13 -0
- package/dist/aaf-HCYN5T6Y.js +37 -0
- package/dist/aaf-ZAE5KS2Y.js +13 -0
- package/dist/ahl-E66MDVCU.js +66 -0
- package/dist/ahl-NPQQWZEV.js +249 -0
- package/dist/ahl-QRDU73XX.js +309 -0
- package/dist/axis-CraWQRUJ.d.ts +49 -0
- package/dist/bins.d.ts +174 -0
- package/dist/bins.js +4 -0
- package/dist/bins.js.map +1 -0
- package/dist/cfb-F42SVS4X.js +5000 -0
- package/dist/cfb-P3SHEYRZ.js +692 -0
- package/dist/cfb-WXRZANLP.js +10226 -0
- package/dist/chartjs-surface.d.ts +42 -0
- package/dist/chartjs-surface.js +65 -0
- package/dist/chartjs-surface.js.map +1 -0
- package/dist/chartjs.d.ts +162 -0
- package/dist/chartjs.js +342 -0
- package/dist/chartjs.js.map +1 -0
- package/dist/chunk-4C3GX6QO.js +92 -0
- package/dist/chunk-4C3GX6QO.js.map +1 -0
- package/dist/chunk-57WFSI4Q.js +128 -0
- package/dist/chunk-57WFSI4Q.js.map +1 -0
- package/dist/chunk-7ULOCCFL.js +313 -0
- package/dist/chunk-7ULOCCFL.js.map +1 -0
- package/dist/chunk-AMXASQHT.js +120 -0
- package/dist/chunk-AMXASQHT.js.map +1 -0
- package/dist/chunk-B2MP37SR.js +175 -0
- package/dist/chunk-B2MP37SR.js.map +1 -0
- package/dist/chunk-C2CDY5OG.js +137 -0
- package/dist/chunk-C2CDY5OG.js.map +1 -0
- package/dist/chunk-DYMUVE6K.js +127 -0
- package/dist/chunk-DYMUVE6K.js.map +1 -0
- package/dist/chunk-E7G7VSLV.js +43 -0
- package/dist/chunk-E7G7VSLV.js.map +1 -0
- package/dist/chunk-H2C3FMMA.js +300 -0
- package/dist/chunk-H2C3FMMA.js.map +1 -0
- package/dist/chunk-J4P4HDBF.js +175 -0
- package/dist/chunk-J4P4HDBF.js.map +1 -0
- package/dist/chunk-J6R2LEJ7.js +58 -0
- package/dist/chunk-J6R2LEJ7.js.map +1 -0
- package/dist/chunk-M3XD2OSN.js +132 -0
- package/dist/chunk-M3XD2OSN.js.map +1 -0
- package/dist/chunk-OIILARXI.js +30 -0
- package/dist/chunk-OIILARXI.js.map +1 -0
- package/dist/chunk-RPVVJY74.js +170 -0
- package/dist/chunk-RPVVJY74.js.map +1 -0
- package/dist/chunk-UL5M6KDE.js +205 -0
- package/dist/chunk-UL5M6KDE.js.map +1 -0
- package/dist/chunk-WQT4IVLX.js +10 -0
- package/dist/chunk-WQT4IVLX.js.map +1 -0
- package/dist/chunk-ZAEQXFJM.js +556 -0
- package/dist/chunk-ZAEQXFJM.js.map +1 -0
- package/dist/cricket-KPCYG2HL.js +493 -0
- package/dist/cricket-NDXLE25B.js +116 -0
- package/dist/cricket-YUUF7P6M.js +160 -0
- package/dist/cytoscape.d.ts +188 -0
- package/dist/cytoscape.js +219 -0
- package/dist/cytoscape.js.map +1 -0
- package/dist/d3.d.ts +234 -0
- package/dist/d3.js +198 -0
- package/dist/d3.js.map +1 -0
- package/dist/deckgl.d.ts +232 -0
- package/dist/deckgl.js +197 -0
- package/dist/deckgl.js.map +1 -0
- package/dist/echarts.d.ts +121 -0
- package/dist/echarts.js +228 -0
- package/dist/echarts.js.map +1 -0
- package/dist/echl-3CR4RGFC.js +241 -0
- package/dist/echl-NPC32C23.js +107 -0
- package/dist/echl-PI33DGJC.js +64 -0
- package/dist/export.d.ts +130 -0
- package/dist/export.js +258 -0
- package/dist/export.js.map +1 -0
- package/dist/g2.d.ts +251 -0
- package/dist/g2.js +102 -0
- package/dist/g2.js.map +1 -0
- package/dist/headshots-B2J_LOO8.d.ts +40 -0
- package/dist/index.d.ts +183 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/interact.d.ts +567 -0
- package/dist/interact.js +809 -0
- package/dist/interact.js.map +1 -0
- package/dist/lightweight-charts.d.ts +217 -0
- package/dist/lightweight-charts.js +155 -0
- package/dist/lightweight-charts.js.map +1 -0
- package/dist/maplibre.d.ts +271 -0
- package/dist/maplibre.js +175 -0
- package/dist/maplibre.js.map +1 -0
- package/dist/marks-DH-tQIP-.d.ts +75 -0
- package/dist/mbb-3TRLVW3H.js +3619 -0
- package/dist/mbb-DQUHCHZP.js +5111 -0
- package/dist/mbb-PRAIO2P5.js +371 -0
- package/dist/milb-6IQ6NCEW.js +291 -0
- package/dist/milb-EYY5MD4T.js +1752 -0
- package/dist/milb-EYZ4SZLI.js +797 -0
- package/dist/mlb-4PBUJSVQ.js +35 -0
- package/dist/mlb-FHPY3GQJ.js +701 -0
- package/dist/mlb-REVGL3N2.js +665 -0
- package/dist/mui.d.ts +338 -0
- package/dist/mui.js +184 -0
- package/dist/mui.js.map +1 -0
- package/dist/nba-ATT775GS.js +485 -0
- package/dist/nba-LKM4UHVU.js +482 -0
- package/dist/nba-YX7PXM3R.js +35 -0
- package/dist/nbagl-3WGZCZ4S.js +37 -0
- package/dist/nbagl-LP4XJRAL.js +229 -0
- package/dist/nbagl-VTTOU7LN.js +499 -0
- package/dist/ncaa_baseball-UV75LNVR.js +2670 -0
- package/dist/ncaa_baseball-VD5J5C72.js +419 -0
- package/dist/ncaa_baseball-YRBS32D6.js +4592 -0
- package/dist/ncaa_mhockey-IMYHS7QS.js +1033 -0
- package/dist/ncaa_mhockey-TXTKWHE3.js +115 -0
- package/dist/ncaa_mhockey-U3MFW5VT.js +663 -0
- package/dist/ncaa_softball-6WR6KS64.js +420 -0
- package/dist/ncaa_softball-RZCTIE56.js +2654 -0
- package/dist/ncaa_softball-VOD4PVGY.js +4599 -0
- package/dist/ncaa_whockey-6XXGUTBK.js +607 -0
- package/dist/ncaa_whockey-AKKYKY67.js +53 -0
- package/dist/ncaa_whockey-KBEKXFBB.js +293 -0
- package/dist/nfl-BDB2HYAR.js +625 -0
- package/dist/nfl-EHYHDLJD.js +37 -0
- package/dist/nfl-VF4NJCHY.js +514 -0
- package/dist/nfl_gsis-UY64JO37.js +4 -0
- package/dist/nhl-6WPHME7H.js +511 -0
- package/dist/nhl-EMIJFZHY.js +884 -0
- package/dist/nhl-ISFUKRFU.js +37 -0
- package/dist/nivo.d.ts +193 -0
- package/dist/nivo.js +8 -0
- package/dist/nivo.js.map +1 -0
- package/dist/ohl-2FEM554G.js +113 -0
- package/dist/ohl-C5Q4W6JB.js +32 -0
- package/dist/ohl-TQ4J46VC.js +66 -0
- package/dist/perspective.d.ts +198 -0
- package/dist/perspective.js +171 -0
- package/dist/perspective.js.map +1 -0
- package/dist/phf-H7POU2KW.js +32 -0
- package/dist/phf-K5IFMZUM.js +13 -0
- package/dist/phf-TWJI3BOW.js +20 -0
- package/dist/placement-UWkqUzpa.d.ts +32 -0
- package/dist/plot.d.ts +451 -0
- package/dist/plot.js +712 -0
- package/dist/plot.js.map +1 -0
- package/dist/plotly.d.ts +129 -0
- package/dist/plotly.js +360 -0
- package/dist/plotly.js.map +1 -0
- package/dist/pwhl-OX3BO4JI.js +53 -0
- package/dist/pwhl-W44OC3C6.js +17 -0
- package/dist/pwhl-ZJGG3H7W.js +23 -0
- package/dist/qmjhl-5HISEHLL.js +35 -0
- package/dist/qmjhl-DA3UHKE5.js +125 -0
- package/dist/qmjhl-YG4BOQPF.js +614 -0
- package/dist/react.d.ts +377 -0
- package/dist/react.js +522 -0
- package/dist/react.js.map +1 -0
- package/dist/recharts.d.ts +162 -0
- package/dist/recharts.js +8 -0
- package/dist/recharts.js.map +1 -0
- package/dist/resolve-CQW-jzvr.d.ts +28 -0
- package/dist/selection-D4T3PkRt.d.ts +140 -0
- package/dist/shadcn.d.ts +83 -0
- package/dist/shadcn.js +88 -0
- package/dist/shadcn.js.map +1 -0
- package/dist/shots.d.ts +364 -0
- package/dist/shots.js +86 -0
- package/dist/shots.js.map +1 -0
- package/dist/soccer-7BH3ZLVL.js +7898 -0
- package/dist/soccer-MRU25OGE.js +2636 -0
- package/dist/soccer-PF4IPPVA.js +12342 -0
- package/dist/surface-TR7YFJW3.js +4 -0
- package/dist/surface-TR7YFJW3.js.map +1 -0
- package/dist/surfaces-MIG7HOFU.js +6 -0
- package/dist/surfaces-MIG7HOFU.js.map +1 -0
- package/dist/surfaces-W-odWLpT.d.ts +47 -0
- package/dist/svelte/HeadshotMark.svelte +25 -0
- package/dist/svelte/HeadshotMark.svelte.d.ts +10 -0
- package/dist/svelte/HeadshotMark.svelte.d.ts.map +1 -0
- package/dist/svelte/LogoMark.svelte +25 -0
- package/dist/svelte/LogoMark.svelte.d.ts +10 -0
- package/dist/svelte/LogoMark.svelte.d.ts.map +1 -0
- package/dist/svelte/Marks.svelte +25 -0
- package/dist/svelte/Marks.svelte.d.ts +10 -0
- package/dist/svelte/Marks.svelte.d.ts.map +1 -0
- package/dist/svelte/Surface.svelte +29 -0
- package/dist/svelte/Surface.svelte.d.ts +9 -0
- package/dist/svelte/Surface.svelte.d.ts.map +1 -0
- package/dist/svelte/SurfaceBackdrop.svelte +26 -0
- package/dist/svelte/SurfaceBackdrop.svelte.d.ts +11 -0
- package/dist/svelte/SurfaceBackdrop.svelte.d.ts.map +1 -0
- package/dist/svelte/index.d.ts +13 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +11 -0
- package/dist/svelte-core.d.ts +139 -0
- package/dist/svelte-core.js +123 -0
- package/dist/svelte-core.js.map +1 -0
- package/dist/tanstack.d.ts +240 -0
- package/dist/tanstack.js +110 -0
- package/dist/tanstack.js.map +1 -0
- package/dist/testing.d.ts +75 -0
- package/dist/testing.js +239 -0
- package/dist/testing.js.map +1 -0
- package/dist/three.d.ts +223 -0
- package/dist/three.js +50 -0
- package/dist/three.js.map +1 -0
- package/dist/tiers-BN4md-GB.d.ts +65 -0
- package/dist/types-C7n6gJF3.d.ts +62 -0
- package/dist/ufl-7VLZTLTT.js +143 -0
- package/dist/ufl-CCMBLPSW.js +100 -0
- package/dist/ufl-F2BJRXME.js +16 -0
- package/dist/unovis.d.ts +127 -0
- package/dist/unovis.js +108 -0
- package/dist/unovis.js.map +1 -0
- package/dist/uplot.d.ts +120 -0
- package/dist/uplot.js +110 -0
- package/dist/uplot.js.map +1 -0
- package/dist/usfl-4CR5RMZJ.js +32 -0
- package/dist/usfl-LD7HP2YK.js +14 -0
- package/dist/usfl-VU4YSJQD.js +19 -0
- package/dist/ushl-CPG2LB4L.js +73 -0
- package/dist/ushl-PQL6HJAD.js +88 -0
- package/dist/ushl-ZT2ATPHK.js +22 -0
- package/dist/vega.d.ts +85 -0
- package/dist/vega.js +362 -0
- package/dist/vega.js.map +1 -0
- package/dist/victory.d.ts +107 -0
- package/dist/victory.js +38 -0
- package/dist/victory.js.map +1 -0
- package/dist/wbb-3KHCDYTK.js +5087 -0
- package/dist/wbb-BMX53Q3Q.js +369 -0
- package/dist/wbb-QNRN2CJJ.js +3620 -0
- package/dist/whl-4ATYZWUB.js +97 -0
- package/dist/whl-RVFPL6MH.js +79 -0
- package/dist/whl-Y7ASKYMM.js +28 -0
- package/dist/wnba-FWX5SFVI.js +232 -0
- package/dist/wnba-GX6HZR4B.js +220 -0
- package/dist/wnba-NTMWIPP6.js +20 -0
- package/dist/xfl-43R2JMJK.js +30 -0
- package/dist/xfl-H3BXQPZZ.js +16 -0
- package/dist/xfl-XON7A7FI.js +69 -0
- package/package.json +366 -3
package/dist/testing.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import './chunk-J4P4HDBF.js';
|
|
2
|
+
import './chunk-DYMUVE6K.js';
|
|
3
|
+
import './chunk-4C3GX6QO.js';
|
|
4
|
+
import { logoUrlSync } from './chunk-7ULOCCFL.js';
|
|
5
|
+
import { resolveSync, ESPN_HEADSHOT_LEAGUES, headshotUrl } from './chunk-ZAEQXFJM.js';
|
|
6
|
+
import './chunk-J6R2LEJ7.js';
|
|
7
|
+
import { setWarningHandler, UnsupportedTargetError, resetWarnings, InputError } from './chunk-E7G7VSLV.js';
|
|
8
|
+
|
|
9
|
+
// src/testing/index.ts
|
|
10
|
+
var num = (s) => s === null || s === "" ? null : Number.isNaN(Number(s)) ? s : Number(s);
|
|
11
|
+
function drawnMarks(node) {
|
|
12
|
+
return Array.from(node.querySelectorAll("image[data-sdv-id]")).map((img) => ({
|
|
13
|
+
id: img.getAttribute("data-sdv-id") ?? "",
|
|
14
|
+
x: num(img.getAttribute("data-sdv-x")),
|
|
15
|
+
y: num(img.getAttribute("data-sdv-y")),
|
|
16
|
+
height: Number(img.getAttribute("height")) / Number(img.getAttribute("data-sdv-frame")),
|
|
17
|
+
url: img.getAttribute("href") ?? "",
|
|
18
|
+
kind: img.getAttribute("data-sdv-kind") ?? ""
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
var ContractError = class extends Error {
|
|
22
|
+
constructor(rule, msg) {
|
|
23
|
+
super(`${rule}: ${msg}`);
|
|
24
|
+
this.rule = rule;
|
|
25
|
+
this.name = "ContractError";
|
|
26
|
+
}
|
|
27
|
+
rule;
|
|
28
|
+
};
|
|
29
|
+
var fail = (rule, msg) => {
|
|
30
|
+
throw new ContractError(rule, msg);
|
|
31
|
+
};
|
|
32
|
+
var xyz = (m) => m.map((d) => [d.id, d.x, d.y]);
|
|
33
|
+
var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
34
|
+
async function counted(fn) {
|
|
35
|
+
let n = 0;
|
|
36
|
+
resetWarnings();
|
|
37
|
+
setWarningHandler(() => {
|
|
38
|
+
n++;
|
|
39
|
+
});
|
|
40
|
+
try {
|
|
41
|
+
return [await fn(), n];
|
|
42
|
+
} finally {
|
|
43
|
+
setWarningHandler(null);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
var plain = (v) => Array.isArray(v) || typeof v === "object" && v !== null && [Object.prototype, null].includes(Object.getPrototypeOf(v));
|
|
47
|
+
function deepFreeze(v) {
|
|
48
|
+
if (plain(v) && !Object.isFrozen(v)) {
|
|
49
|
+
Object.freeze(v);
|
|
50
|
+
for (const x of Object.values(v)) deepFreeze(x);
|
|
51
|
+
}
|
|
52
|
+
return v;
|
|
53
|
+
}
|
|
54
|
+
var snapshot = (v) => JSON.stringify(v, (_k, x) => typeof x === "object" && x !== null && !plain(x) ? "[object]" : x);
|
|
55
|
+
async function notInputError(fn) {
|
|
56
|
+
try {
|
|
57
|
+
await fn();
|
|
58
|
+
} catch (e) {
|
|
59
|
+
if (e instanceof InputError) return null;
|
|
60
|
+
return `threw ${e instanceof Error ? `${e.name}: ${e.message}` : String(e)}`;
|
|
61
|
+
}
|
|
62
|
+
return "did not throw";
|
|
63
|
+
}
|
|
64
|
+
async function checkAdapterContract(a, o) {
|
|
65
|
+
const league = o.league ?? "nfl";
|
|
66
|
+
const [ka, kb] = o.known ?? ["LV", "LAR"];
|
|
67
|
+
const [wa, wb] = o.knownWordmarks ?? ["LV", "LAC"];
|
|
68
|
+
const [p, q] = o.players ?? ["3139477", "4241479"];
|
|
69
|
+
const xs = [10, 20];
|
|
70
|
+
const ys = [-3, -7];
|
|
71
|
+
for (const k of [
|
|
72
|
+
"addLogos",
|
|
73
|
+
"addWordmarks",
|
|
74
|
+
"addHeadshots",
|
|
75
|
+
"axisLogos",
|
|
76
|
+
"drawnMarks",
|
|
77
|
+
"drawnAxisMarks",
|
|
78
|
+
"visibleAxisLabels"
|
|
79
|
+
])
|
|
80
|
+
if (typeof a[k] !== "function") fail("rule 0 (shape)", `${a.name} lacks ${k}`);
|
|
81
|
+
if (typeof a.supportsAxisLogos !== "boolean") fail("rule 0 (shape)", "supportsAxisLogos must be boolean");
|
|
82
|
+
const checkVerb = async (verb, vals, ids, urls, r) => {
|
|
83
|
+
const want = [
|
|
84
|
+
[ids[0], 10, -3],
|
|
85
|
+
[ids[1], 20, -7]
|
|
86
|
+
];
|
|
87
|
+
const [t1, n1] = await counted(() => verb(o.makeTarget(), xs, ys, vals, {}));
|
|
88
|
+
const m1 = a.drawnMarks(t1);
|
|
89
|
+
if (!same(xyz(m1), want)) fail(r[0], `expected ${JSON.stringify(want)}, drew ${JSON.stringify(xyz(m1))}`);
|
|
90
|
+
if (n1 !== 0) fail(r[0], `known values warned ${n1} time(s)`);
|
|
91
|
+
m1.forEach((d, i) => {
|
|
92
|
+
const u = urls[i];
|
|
93
|
+
if (u && d.url !== u) fail(r[0], `${d.id} drew ${d.url}, its mark is ${u}`);
|
|
94
|
+
});
|
|
95
|
+
const [t2, n2] = await counted(() => verb(o.makeTarget(), xs, ys, ["XXX", vals[1]], {}));
|
|
96
|
+
if (!same(xyz(a.drawnMarks(t2)), [want[1]]))
|
|
97
|
+
fail(r[1], "an unknown value must be skipped with its own x/y");
|
|
98
|
+
if (n2 !== 1) fail(r[1], `one unknown value must warn exactly once, warned ${n2}`);
|
|
99
|
+
const [, n3] = await counted(
|
|
100
|
+
() => verb(o.makeTarget(), [1, 2, 3], [1, 2, 3], ["XXX", "YYY", vals[0]], {})
|
|
101
|
+
);
|
|
102
|
+
if (n3 !== 1)
|
|
103
|
+
fail(
|
|
104
|
+
r[1],
|
|
105
|
+
`two distinct unknown values must warn exactly once per call (one message listing both), warned ${n3}`
|
|
106
|
+
);
|
|
107
|
+
for (const h of [0.1, 0.25, 1]) {
|
|
108
|
+
const t = await verb(o.makeTarget(), xs, ys, vals, { height: h });
|
|
109
|
+
for (const d of a.drawnMarks(t))
|
|
110
|
+
if (Math.abs(d.height - h) / h > 0.01) fail(r[2], `asked height ${h}, drew ${d.height}`);
|
|
111
|
+
}
|
|
112
|
+
for (const bad of [0, 1.5, 40]) {
|
|
113
|
+
const why = await notInputError(() => verb(o.makeTarget(), xs, ys, vals, { height: bad }));
|
|
114
|
+
if (why) fail(r[2], `height ${bad} must throw InputError when the verb is called, ${why}`);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const [ia = "", ib = ""] = resolveSync([ka, kb], league);
|
|
118
|
+
await checkVerb(
|
|
119
|
+
(t, x, y, v, opts) => a.addLogos(t, x, y, v, { league, ...opts }),
|
|
120
|
+
[ka, kb],
|
|
121
|
+
[ia, ib],
|
|
122
|
+
[logoUrlSync(ka, league), logoUrlSync(kb, league)],
|
|
123
|
+
["rule 1 (resolution)", "rule 2 (unknown team: warn and skip)", "rule 4 (height semantics)"]
|
|
124
|
+
);
|
|
125
|
+
const tA = await a.addLogos(o.makeTarget(), xs, ys, [ka, kb], { league });
|
|
126
|
+
const tB = await a.addLogos(o.makeTarget(), new Float64Array(xs), new Float64Array(ys), [ka, kb], {
|
|
127
|
+
league
|
|
128
|
+
});
|
|
129
|
+
if (!same(xyz(a.drawnMarks(tA)), xyz(a.drawnMarks(tB))))
|
|
130
|
+
fail("rule 3 (columnar parity)", "typed-array positions drew different marks");
|
|
131
|
+
const [wia = "", wib = ""] = resolveSync([wa, wb], league);
|
|
132
|
+
const wm = (t) => logoUrlSync(t, league, { markType: "wordmark" });
|
|
133
|
+
await checkVerb(
|
|
134
|
+
(t, x, y, v, opts) => a.addWordmarks(t, x, y, v, { league, ...opts }),
|
|
135
|
+
[wa, wb],
|
|
136
|
+
[wia, wib],
|
|
137
|
+
[wm(wa), wm(wb)],
|
|
138
|
+
["rule 5 (wordmarks: resolution)", "rule 5 (wordmarks: warn and skip)", "rule 5 (wordmarks: height)"]
|
|
139
|
+
);
|
|
140
|
+
if (!Object.hasOwn(ESPN_HEADSHOT_LEAGUES, league))
|
|
141
|
+
fail(
|
|
142
|
+
"rule 6 (headshots)",
|
|
143
|
+
`league ${JSON.stringify(league)} has no ESPN headshots; pass an ESPN headshot league`
|
|
144
|
+
);
|
|
145
|
+
const hl = league;
|
|
146
|
+
await checkVerb(
|
|
147
|
+
(t, x, y, v, opts) => a.addHeadshots(t, x, y, v, { league: hl, ...opts }),
|
|
148
|
+
[p, q],
|
|
149
|
+
[p, q],
|
|
150
|
+
[headshotUrl(p, hl), headshotUrl(q, hl)],
|
|
151
|
+
["rule 6 (headshots)", "rule 6 (headshots: unknown id)", "rule 6 (headshots: height)"]
|
|
152
|
+
);
|
|
153
|
+
const r7 = "rule 7 (axis logos)";
|
|
154
|
+
if (a.supportsAxisLogos) {
|
|
155
|
+
const mk2 = o.makeAxisTarget;
|
|
156
|
+
if (!mk2) return fail(r7, "makeAxisTarget is required for an adapter that supports axis logos");
|
|
157
|
+
const [[marks, shown], n] = await counted(async () => {
|
|
158
|
+
const t = await a.axisLogos(mk2([ka, "XXX", kb]), "x", { league });
|
|
159
|
+
return [a.drawnAxisMarks(t, "x"), a.visibleAxisLabels(t, "x")];
|
|
160
|
+
});
|
|
161
|
+
if (!same(
|
|
162
|
+
marks.map((m) => m.id),
|
|
163
|
+
[ia, ib]
|
|
164
|
+
))
|
|
165
|
+
fail(r7, `expected axis images for [${ia}, ${ib}] in tick order, drew ${JSON.stringify(marks)}`);
|
|
166
|
+
if (!((marks[0]?.tick ?? 0) < (marks[1]?.tick ?? 0)))
|
|
167
|
+
fail(r7, "axis images must sit at increasing tick positions");
|
|
168
|
+
if (n !== 1) fail(r7, `an unknown axis category must warn exactly once, warned ${n}`);
|
|
169
|
+
if (!shown.includes("XXX") || shown.includes(ka) || shown.includes(kb))
|
|
170
|
+
fail(r7, `only the unknown category may stay as text, visible labels are ${JSON.stringify(shown)}`);
|
|
171
|
+
for (const h of [0.1, 0.25])
|
|
172
|
+
for (const m of a.drawnAxisMarks(await a.axisLogos(mk2([ka, kb]), "x", { league, height: h }), "x"))
|
|
173
|
+
if (Math.abs(m.height - h) / h > 0.01) fail(`${r7} (height)`, `asked ${h}, drew ${m.height}`);
|
|
174
|
+
} else {
|
|
175
|
+
let threw = false;
|
|
176
|
+
try {
|
|
177
|
+
await a.axisLogos(o.makeTarget(), "x", { league });
|
|
178
|
+
} catch (e) {
|
|
179
|
+
threw = e instanceof UnsupportedTargetError;
|
|
180
|
+
}
|
|
181
|
+
if (!threw)
|
|
182
|
+
fail(r7, "an adapter without axis-logo support must throw UnsupportedTargetError from axisLogos");
|
|
183
|
+
}
|
|
184
|
+
const alphaVerbs = [
|
|
185
|
+
["addLogos", (alpha) => a.addLogos(o.makeTarget(), [10], [-3], [ka], { league, alpha })],
|
|
186
|
+
["addWordmarks", (alpha) => a.addWordmarks(o.makeTarget(), [10], [-3], [wa], { league, alpha })],
|
|
187
|
+
["addHeadshots", (alpha) => a.addHeadshots(o.makeTarget(), [10], [-3], [p], { league: hl, alpha })]
|
|
188
|
+
];
|
|
189
|
+
for (const [name, call] of alphaVerbs)
|
|
190
|
+
for (const bad of [-0.1, 1.1]) {
|
|
191
|
+
const why = await notInputError(() => call(bad));
|
|
192
|
+
if (why) fail("rule 8 (alpha)", `${name} with alpha ${bad} must throw InputError, ${why}`);
|
|
193
|
+
}
|
|
194
|
+
const frozen = (v) => deepFreeze(structuredClone(v));
|
|
195
|
+
const inputVerbs = [
|
|
196
|
+
["addLogos", o.makeTarget, (t) => a.addLogos(t, frozen(xs), frozen(ys), frozen([ka, kb]), { league })],
|
|
197
|
+
[
|
|
198
|
+
"addWordmarks",
|
|
199
|
+
o.makeTarget,
|
|
200
|
+
(t) => a.addWordmarks(t, frozen(xs), frozen(ys), frozen([wa, wb]), { league })
|
|
201
|
+
],
|
|
202
|
+
[
|
|
203
|
+
"addHeadshots",
|
|
204
|
+
o.makeTarget,
|
|
205
|
+
(t) => a.addHeadshots(t, frozen(xs), frozen(ys), frozen([p, q]), { league: hl })
|
|
206
|
+
]
|
|
207
|
+
];
|
|
208
|
+
const mk = o.makeAxisTarget;
|
|
209
|
+
if (a.supportsAxisLogos && mk)
|
|
210
|
+
inputVerbs.push(["axisLogos", () => mk([ka, kb]), (t) => a.axisLogos(t, "x", { league })]);
|
|
211
|
+
for (const [name, make, call] of inputVerbs) {
|
|
212
|
+
const t = deepFreeze(make());
|
|
213
|
+
const before = snapshot(t);
|
|
214
|
+
try {
|
|
215
|
+
await call(t);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
fail(
|
|
218
|
+
"rule 9 (no input mutation)",
|
|
219
|
+
`${name} threw on a frozen input: ${e instanceof Error ? e.message : String(e)}`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (snapshot(t) !== before) fail("rule 9 (no input mutation)", `${name} changed its input target`);
|
|
223
|
+
}
|
|
224
|
+
setWarningHandler(null);
|
|
225
|
+
}
|
|
226
|
+
function drawnAxisMarks(node, axis) {
|
|
227
|
+
return Array.from(node.querySelectorAll(`image[data-sdv-axis="${axis}"]`)).map((img) => ({
|
|
228
|
+
id: img.getAttribute("data-sdv-id") ?? "",
|
|
229
|
+
tick: Number(img.getAttribute("data-sdv-tick")),
|
|
230
|
+
height: Number(img.getAttribute("height")) / Number(img.getAttribute("data-sdv-frame"))
|
|
231
|
+
})).sort((a, b) => a.tick - b.tick);
|
|
232
|
+
}
|
|
233
|
+
function visibleAxisLabels(node, axis) {
|
|
234
|
+
return Array.from(node.querySelectorAll(`[aria-label="${axis}-axis tick label"] text`)).map((t) => t.textContent ?? "").filter((s) => s !== "");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export { ContractError, checkAdapterContract, drawnAxisMarks, drawnMarks, visibleAxisLabels };
|
|
238
|
+
//# sourceMappingURL=testing.js.map
|
|
239
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/testing/index.ts"],"names":["mk"],"mappings":"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","file":"testing.js","sourcesContent":["import {\n ESPN_HEADSHOT_LEAGUES,\n type EspnHeadshotLeague,\n InputError,\n type League,\n UnsupportedTargetError,\n headshotUrl,\n logoUrlSync,\n resetWarnings,\n resolveSync,\n setWarningHandler,\n} from \"../index.js\";\nimport type { Axis } from \"../plot/axis.js\";\nimport type { Value } from \"../resolve.js\";\n\nexport interface DrawnMark {\n id: string;\n x: Value;\n y: Value;\n /** Drawn image height as a fraction of the frame height the render saw. */\n height: number;\n url: string;\n kind: string;\n}\nconst num = (s: string | null): Value =>\n s === null || s === \"\" ? null : Number.isNaN(Number(s)) ? s : Number(s);\n\n/** Every `<image data-sdv-id>` under `node`, in document order. */\nexport function drawnMarks(node: ParentNode): DrawnMark[] {\n return Array.from(node.querySelectorAll(\"image[data-sdv-id]\")).map((img) => ({\n id: img.getAttribute(\"data-sdv-id\") ?? \"\",\n x: num(img.getAttribute(\"data-sdv-x\")),\n y: num(img.getAttribute(\"data-sdv-y\")),\n height: Number(img.getAttribute(\"height\")) / Number(img.getAttribute(\"data-sdv-frame\")),\n url: img.getAttribute(\"href\") ?? \"\",\n kind: img.getAttribute(\"data-sdv-kind\") ?? \"\",\n }));\n}\n\nexport interface ContractMarkOptions {\n league: League;\n height?: number;\n alpha?: number;\n}\n/** Adapters must accept typed arrays (e.g. `Float64Array`) for the x and y positions: rule 3 passes them. */\nexport interface ContractAdapter<T> {\n name: string;\n addLogos(\n target: T,\n x: ArrayLike<Value>,\n y: ArrayLike<Value>,\n teams: readonly Value[],\n o: ContractMarkOptions,\n ): T | Promise<T>;\n addWordmarks(\n target: T,\n x: ArrayLike<Value>,\n y: ArrayLike<Value>,\n teams: readonly Value[],\n o: ContractMarkOptions,\n ): T | Promise<T>;\n /** `league` is narrowed to the ESPN-headshot leagues: the contract checks that before calling, so adapters need no cast. */\n addHeadshots(\n target: T,\n x: ArrayLike<Value>,\n y: ArrayLike<Value>,\n players: readonly Value[],\n o: Omit<ContractMarkOptions, \"league\"> & { league: EspnHeadshotLeague },\n ): T | Promise<T>;\n /** Throws `UnsupportedTargetError` when `supportsAxisLogos` is false. */\n axisLogos(target: T, axis: \"x\" | \"y\", o: { league: League; height?: number }): T | Promise<T>;\n supportsAxisLogos: boolean;\n drawnMarks(target: T): readonly DrawnMark[];\n drawnAxisMarks(target: T, axis: \"x\" | \"y\"): readonly { id: string; tick: number; height: number }[];\n visibleAxisLabels(target: T, axis: \"x\" | \"y\"): readonly string[];\n}\n\n/** `message` starts with \"rule N (...): ...\" so a test can match `/^rule N/`. */\nexport class ContractError extends Error {\n constructor(\n readonly rule: string,\n msg: string,\n ) {\n super(`${rule}: ${msg}`);\n this.name = \"ContractError\";\n }\n}\nconst fail = (rule: string, msg: string): never => {\n throw new ContractError(rule, msg);\n};\nconst xyz = (m: readonly DrawnMark[]): unknown[][] => m.map((d) => [d.id, d.x, d.y]);\nconst same = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.stringify(b);\n\n// There is no getter for the current warning handler, so a previous one cannot be restored:\n// this leaves the default handler (null) installed.\nasync function counted<R>(fn: () => R | Promise<R>): Promise<[R, number]> {\n let n = 0;\n resetWarnings();\n setWarningHandler(() => {\n n++;\n });\n try {\n return [await fn(), n];\n } finally {\n setWarningHandler(null);\n }\n}\n/** Plain data: an array, or an object whose prototype is Object.prototype or null (not a class instance or DOM node). */\nconst plain = (v: unknown): v is object =>\n Array.isArray(v) ||\n (typeof v === \"object\" && v !== null && [Object.prototype, null].includes(Object.getPrototypeOf(v)));\n/** Freezes the plain-data part of `v` (plain objects and arrays, recursively); class instances are left alone. */\nfunction deepFreeze<V>(v: V): V {\n if (plain(v) && !Object.isFrozen(v)) {\n Object.freeze(v);\n for (const x of Object.values(v)) deepFreeze(x);\n }\n return v;\n}\n/** The plain-data part of `v` as JSON, class instances as \"[object]\". */\nconst snapshot = (v: unknown): string =>\n JSON.stringify(v, (_k, x: unknown) => (typeof x === \"object\" && x !== null && !plain(x) ? \"[object]\" : x));\n\n/** null when `fn` threw InputError; otherwise what went wrong, for the failure message. */\nasync function notInputError(fn: () => unknown): Promise<string | null> {\n try {\n await fn();\n } catch (e) {\n if (e instanceof InputError) return null;\n return `threw ${e instanceof Error ? `${e.name}: ${e.message}` : String(e)}`;\n }\n return \"did not throw\";\n}\n\nexport interface ContractOptions<T> {\n makeTarget: () => T;\n makeAxisTarget?: (categories: readonly string[]) => T;\n league?: League;\n known?: readonly [string, string];\n knownWordmarks?: readonly [string, string];\n players?: readonly [string, string];\n}\n\n/**\n * Port of Python `sdvplot.testing.check_adapter_contract` (rules 0-8), plus rule 9: no verb mutates its input (the\n * target's plain-data part and the position and value arrays, deep-frozen). Rejects with a `ContractError`\n * naming the first rule broken. Warnings are once per call (one message listing every unresolved value).\n * Resets the warning handler to null (the default) when done: the previous handler cannot be restored.\n */\nexport async function checkAdapterContract<T>(a: ContractAdapter<T>, o: ContractOptions<T>): Promise<void> {\n const league = o.league ?? \"nfl\";\n const [ka, kb] = o.known ?? [\"LV\", \"LAR\"];\n const [wa, wb] = o.knownWordmarks ?? [\"LV\", \"LAC\"];\n const [p, q] = o.players ?? [\"3139477\", \"4241479\"];\n const xs = [10, 20];\n const ys = [-3, -7];\n // rule 0: shape (JS has no dispatch front door)\n for (const k of [\n \"addLogos\",\n \"addWordmarks\",\n \"addHeadshots\",\n \"axisLogos\",\n \"drawnMarks\",\n \"drawnAxisMarks\",\n \"visibleAxisLabels\",\n ] as const)\n if (typeof a[k] !== \"function\") fail(\"rule 0 (shape)\", `${a.name} lacks ${k}`);\n if (typeof a.supportsAxisLogos !== \"boolean\") fail(\"rule 0 (shape)\", \"supportsAxisLogos must be boolean\");\n\n type Verb = (\n t: T,\n x: ArrayLike<Value>,\n y: ArrayLike<Value>,\n v: readonly Value[],\n opts: { height?: number; alpha?: number },\n ) => T | Promise<T>;\n // rules 1, 2, 4 for one image verb; wordmarks (5) and headshots (6) reuse it with their own rule labels\n const checkVerb = async (\n verb: Verb,\n vals: readonly [string, string],\n ids: readonly [string, string],\n urls: readonly [string | undefined, string | undefined],\n r: readonly [string, string, string],\n ): Promise<void> => {\n const want = [\n [ids[0], 10, -3],\n [ids[1], 20, -7],\n ];\n const [t1, n1] = await counted(() => verb(o.makeTarget(), xs, ys, vals, {}));\n const m1 = a.drawnMarks(t1);\n if (!same(xyz(m1), want)) fail(r[0], `expected ${JSON.stringify(want)}, drew ${JSON.stringify(xyz(m1))}`);\n if (n1 !== 0) fail(r[0], `known values warned ${n1} time(s)`);\n m1.forEach((d, i) => {\n const u = urls[i];\n if (u && d.url !== u) fail(r[0], `${d.id} drew ${d.url}, its mark is ${u}`);\n });\n const [t2, n2] = await counted(() => verb(o.makeTarget(), xs, ys, [\"XXX\", vals[1]], {}));\n if (!same(xyz(a.drawnMarks(t2)), [want[1]]))\n fail(r[1], \"an unknown value must be skipped with its own x/y\");\n if (n2 !== 1) fail(r[1], `one unknown value must warn exactly once, warned ${n2}`);\n const [, n3] = await counted(() =>\n verb(o.makeTarget(), [1, 2, 3], [1, 2, 3], [\"XXX\", \"YYY\", vals[0]], {}),\n );\n if (n3 !== 1)\n fail(\n r[1],\n `two distinct unknown values must warn exactly once per call (one message listing both), warned ${n3}`,\n );\n for (const h of [0.1, 0.25, 1]) {\n const t = await verb(o.makeTarget(), xs, ys, vals, { height: h });\n for (const d of a.drawnMarks(t))\n if (Math.abs(d.height - h) / h > 0.01) fail(r[2], `asked height ${h}, drew ${d.height}`);\n }\n for (const bad of [0, 1.5, 40]) {\n const why = await notInputError(() => verb(o.makeTarget(), xs, ys, vals, { height: bad }));\n if (why) fail(r[2], `height ${bad} must throw InputError when the verb is called, ${why}`);\n }\n };\n const [ia = \"\", ib = \"\"] = resolveSync([ka, kb], league);\n await checkVerb(\n (t, x, y, v, opts) => a.addLogos(t, x, y, v, { league, ...opts }),\n [ka, kb],\n [ia, ib],\n [logoUrlSync(ka, league), logoUrlSync(kb, league)],\n [\"rule 1 (resolution)\", \"rule 2 (unknown team: warn and skip)\", \"rule 4 (height semantics)\"],\n );\n // rule 3: columnar parity (real Float64Array positions draw what plain arrays draw)\n const tA = await a.addLogos(o.makeTarget(), xs, ys, [ka, kb], { league });\n const tB = await a.addLogos(o.makeTarget(), new Float64Array(xs), new Float64Array(ys), [ka, kb], {\n league,\n });\n if (!same(xyz(a.drawnMarks(tA)), xyz(a.drawnMarks(tB))))\n fail(\"rule 3 (columnar parity)\", \"typed-array positions drew different marks\");\n // rule 5: wordmarks\n const [wia = \"\", wib = \"\"] = resolveSync([wa, wb], league);\n const wm = (t: string): string | undefined => logoUrlSync(t, league, { markType: \"wordmark\" });\n await checkVerb(\n (t, x, y, v, opts) => a.addWordmarks(t, x, y, v, { league, ...opts }),\n [wa, wb],\n [wia, wib],\n [wm(wa), wm(wb)],\n [\"rule 5 (wordmarks: resolution)\", \"rule 5 (wordmarks: warn and skip)\", \"rule 5 (wordmarks: height)\"],\n );\n // rule 6: headshots (ESPN leagues only; the narrowing keeps adapters cast-free)\n if (!Object.hasOwn(ESPN_HEADSHOT_LEAGUES, league))\n fail(\n \"rule 6 (headshots)\",\n `league ${JSON.stringify(league)} has no ESPN headshots; pass an ESPN headshot league`,\n );\n const hl = league as EspnHeadshotLeague;\n await checkVerb(\n (t, x, y, v, opts) => a.addHeadshots(t, x, y, v, { league: hl, ...opts }),\n [p, q],\n [p, q],\n [headshotUrl(p, hl), headshotUrl(q, hl)],\n [\"rule 6 (headshots)\", \"rule 6 (headshots: unknown id)\", \"rule 6 (headshots: height)\"],\n );\n // rule 7: axis logos\n const r7 = \"rule 7 (axis logos)\";\n if (a.supportsAxisLogos) {\n const mk = o.makeAxisTarget;\n if (!mk) return fail(r7, \"makeAxisTarget is required for an adapter that supports axis logos\");\n // an adapter may resolve at construction or only when the target renders: count across both\n const [[marks, shown], n] = await counted(async () => {\n const t = await a.axisLogos(mk([ka, \"XXX\", kb]), \"x\", { league });\n return [a.drawnAxisMarks(t, \"x\"), a.visibleAxisLabels(t, \"x\")] as const;\n });\n if (\n !same(\n marks.map((m) => m.id),\n [ia, ib],\n )\n )\n fail(r7, `expected axis images for [${ia}, ${ib}] in tick order, drew ${JSON.stringify(marks)}`);\n if (!((marks[0]?.tick ?? 0) < (marks[1]?.tick ?? 0)))\n fail(r7, \"axis images must sit at increasing tick positions\");\n if (n !== 1) fail(r7, `an unknown axis category must warn exactly once, warned ${n}`);\n if (!shown.includes(\"XXX\") || shown.includes(ka) || shown.includes(kb))\n fail(r7, `only the unknown category may stay as text, visible labels are ${JSON.stringify(shown)}`);\n for (const h of [0.1, 0.25])\n for (const m of a.drawnAxisMarks(await a.axisLogos(mk([ka, kb]), \"x\", { league, height: h }), \"x\"))\n if (Math.abs(m.height - h) / h > 0.01) fail(`${r7} (height)`, `asked ${h}, drew ${m.height}`);\n } else {\n let threw = false;\n try {\n await a.axisLogos(o.makeTarget(), \"x\", { league });\n } catch (e) {\n threw = e instanceof UnsupportedTargetError;\n }\n if (!threw)\n fail(r7, \"an adapter without axis-logo support must throw UnsupportedTargetError from axisLogos\");\n }\n // rule 8: alpha outside [0, 1] throws on every verb\n const alphaVerbs: [string, (alpha: number) => unknown][] = [\n [\"addLogos\", (alpha) => a.addLogos(o.makeTarget(), [10], [-3], [ka], { league, alpha })],\n [\"addWordmarks\", (alpha) => a.addWordmarks(o.makeTarget(), [10], [-3], [wa], { league, alpha })],\n [\"addHeadshots\", (alpha) => a.addHeadshots(o.makeTarget(), [10], [-3], [p], { league: hl, alpha })],\n ];\n for (const [name, call] of alphaVerbs)\n for (const bad of [-0.1, 1.1]) {\n const why = await notInputError(() => call(bad));\n if (why) fail(\"rule 8 (alpha)\", `${name} with alpha ${bad} must throw InputError, ${why}`);\n }\n // rule 9: no input mutation. A write into a frozen input throws (strict mode); the snapshot catches the rest.\n const frozen = <V>(v: V): V => deepFreeze(structuredClone(v));\n const inputVerbs: [string, () => T, (t: T) => unknown][] = [\n [\"addLogos\", o.makeTarget, (t) => a.addLogos(t, frozen(xs), frozen(ys), frozen([ka, kb]), { league })],\n [\n \"addWordmarks\",\n o.makeTarget,\n (t) => a.addWordmarks(t, frozen(xs), frozen(ys), frozen([wa, wb]), { league }),\n ],\n [\n \"addHeadshots\",\n o.makeTarget,\n (t) => a.addHeadshots(t, frozen(xs), frozen(ys), frozen([p, q]), { league: hl }),\n ],\n ];\n const mk = o.makeAxisTarget;\n if (a.supportsAxisLogos && mk)\n inputVerbs.push([\"axisLogos\", () => mk([ka, kb]), (t) => a.axisLogos(t, \"x\", { league })]);\n for (const [name, make, call] of inputVerbs) {\n const t = deepFreeze(make());\n const before = snapshot(t);\n try {\n await call(t);\n } catch (e) {\n fail(\n \"rule 9 (no input mutation)\",\n `${name} threw on a frozen input: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n if (snapshot(t) !== before) fail(\"rule 9 (no input mutation)\", `${name} changed its input target`);\n }\n setWarningHandler(null);\n}\n\n/** Axis images (`<image data-sdv-axis>`), in tick-position order; `height` is a fraction of the frame. */\nexport function drawnAxisMarks(node: ParentNode, axis: Axis): { id: string; tick: number; height: number }[] {\n return Array.from(node.querySelectorAll(`image[data-sdv-axis=\"${axis}\"]`))\n .map((img) => ({\n id: img.getAttribute(\"data-sdv-id\") ?? \"\",\n tick: Number(img.getAttribute(\"data-sdv-tick\")),\n height: Number(img.getAttribute(\"height\")) / Number(img.getAttribute(\"data-sdv-frame\")),\n }))\n .sort((a, b) => a.tick - b.tick);\n}\n/** Non-empty tick `<text>` still on the axis (categories that did not resolve to an image). */\nexport function visibleAxisLabels(node: ParentNode, axis: Axis): string[] {\n return Array.from(node.querySelectorAll(`[aria-label=\"${axis}-axis tick label\"] text`))\n .map((t) => t.textContent ?? \"\")\n .filter((s) => s !== \"\");\n}\n"]}
|
package/dist/three.d.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { Units, Sport, LeagueOf, OptionsOf } from '@sportsdataverse/sporty';
|
|
2
|
+
import { W as Which, S as SeasonInput, I as IdSystem, L as League } from './types-C7n6gJF3.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Baseball Savant (Statcast) pitch fields `statcastPitchPath` reads, by their Savant CSV column names: feet, feet
|
|
6
|
+
* per second and feet per second squared, in Statcast's frame (catcher's view: x to the catcher's right, the
|
|
7
|
+
* first-base side; y from the point of home plate towards the mound; z up).
|
|
8
|
+
*/
|
|
9
|
+
interface StatcastPitch {
|
|
10
|
+
/** Velocity at y = 50 ft, ft/s. */
|
|
11
|
+
vx0: number;
|
|
12
|
+
vy0: number;
|
|
13
|
+
vz0: number;
|
|
14
|
+
/** Acceleration (constant over the flight), ft/s². */
|
|
15
|
+
ax: number;
|
|
16
|
+
ay: number;
|
|
17
|
+
az: number;
|
|
18
|
+
/**
|
|
19
|
+
* Where the pitch crossed home plate, ft: at its front edge (y = 17/12 ft) through 2025, and at its middle
|
|
20
|
+
* (y = 8.5/12 ft, "to align with the ABS system") from 2026, per Savant's CSV documentation.
|
|
21
|
+
*/
|
|
22
|
+
plate_x: number;
|
|
23
|
+
plate_z: number;
|
|
24
|
+
/** The release point's distance from home plate, ft (60.5 less the pitcher's extension). */
|
|
25
|
+
release_pos_y: number;
|
|
26
|
+
/** The season: it says which plate plane plate_x and plate_z are at (see `StatcastPathOptions.plate`). */
|
|
27
|
+
game_year?: number;
|
|
28
|
+
}
|
|
29
|
+
/** `statcastPitchPath` options. */
|
|
30
|
+
interface StatcastPathOptions {
|
|
31
|
+
/** Steps from release to the plate, equal in time: the path has `steps + 1` points. Default 40. */
|
|
32
|
+
steps?: number;
|
|
33
|
+
/**
|
|
34
|
+
* The plane plate_x and plate_z were measured at: "front" (the plate's front edge, Savant through 2025) or "middle"
|
|
35
|
+
* (halfway along it, from 2026). Default: from the pitch's `game_year`; with neither it is an InputError, as a wrong
|
|
36
|
+
* plane moves the whole path 8.5 inches along its flight.
|
|
37
|
+
*/
|
|
38
|
+
plate?: "front" | "middle";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A pitch's flight from release to the front of home plate, as `[x, y, z]` points in feet in Statcast's frame, which
|
|
42
|
+
* is sporty's baseball frame too (home plate's point at the origin, y towards the mound), so the path draws over
|
|
43
|
+
* `surfaceMesh("baseball", "mlb")` unconverted. Statcast describes each pitch with the PITCHf/x nine-parameter fit:
|
|
44
|
+
* constant acceleration (ax, ay, az) and the velocity (vx0, vy0, vz0) at y = 50 ft, so
|
|
45
|
+
* `p(t) = p50 + v0 t + a t² / 2` (Baseball Savant CSV documentation, https://baseballsavant.mlb.com/csv-docs: "vx0,
|
|
46
|
+
* vy0, vz0: the velocity of the pitch, in feet per second, in x, y, and z dimensions, determined at y=50 feet"; "ax,
|
|
47
|
+
* ay, az: the acceleration of the pitch, in feet per second per second ... determined at y=50 feet"; A. M. Nathan,
|
|
48
|
+
* "Determining Pitch Movement from PITCHf/x Data", 2012, https://baseball.physics.illinois.edu/Movement.pdf). Savant's
|
|
49
|
+
* CSV leaves out x and z at y = 50, so they are solved from where the fit crosses the plate (plate_x, plate_z: at its
|
|
50
|
+
* front edge, y = 17/12 ft, through 2025; at its middle, y = 8.5/12 ft, from 2026, per the same documentation); the
|
|
51
|
+
* path then runs, equal steps in time, from y = release_pos_y to that plane. The 2026 middle-of-plate rule is from
|
|
52
|
+
* Savant's documentation of its MLB search: for its World Baseball Classic or minor-league searches, pass `plate`
|
|
53
|
+
* explicitly rather than rely on `game_year`. Each field must be a number: filter out rows with an empty CSV cell
|
|
54
|
+
* first, since `Number("")` is 0 and would pass as a real value. Traced back to the release, it lands on
|
|
55
|
+
* Savant's own release_pos_x / release_pos_z within their 0.01 ft rounding (checked on 2024 and 2026 pitches).
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* import { statcastPitchPath } from "@sportsdataverse/sdvplot/three";
|
|
60
|
+
*
|
|
61
|
+
* // a 96.3 mph four-seamer, Yankees at Red Sox, 2024-06-15 (Baseball Savant: game 746946, at-bat 48, pitch 4)
|
|
62
|
+
* const pitch = {
|
|
63
|
+
* vx0: -6.245145230392043, vy0: -140.0548111003631, vz0: -4.647911601289857,
|
|
64
|
+
* ax: 12.681580763686934, ay: 32.15363682007945, az: -12.428443024407864,
|
|
65
|
+
* plate_x: 0.42927397039380755, plate_z: 3.546869816011372, release_pos_y: 54.12, game_year: 2024,
|
|
66
|
+
* };
|
|
67
|
+
* const path = statcastPitchPath(pitch); // 41 points, release to plate
|
|
68
|
+
* path[0]; // about [2.05, 54.12, 6.17]: Savant's release_pos_x, release_pos_y, release_pos_z
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare function statcastPitchPath(pitch: StatcastPitch, o?: StatcastPathOptions): [x: number, y: number, z: number][];
|
|
72
|
+
|
|
73
|
+
/** One closed loop of `surfaceMesh`: where its vertices and triangles sit in the mesh buffers. */
|
|
74
|
+
interface SurfaceLoop {
|
|
75
|
+
/** The sporty feature it comes from (e.g. "infield_dirt"), or "background" for the surface's own background. */
|
|
76
|
+
name: string;
|
|
77
|
+
/** The feature's sporty fill, `#rrggbb` or `#rrggbbaa`, as sporty gives it. */
|
|
78
|
+
color: string;
|
|
79
|
+
/** Its first vertex and number of vertices. */
|
|
80
|
+
vertexStart: number;
|
|
81
|
+
vertexCount: number;
|
|
82
|
+
/** Its first index and number of indices (three per triangle). */
|
|
83
|
+
indexStart: number;
|
|
84
|
+
indexCount: number;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A sporty playing surface as one triangle mesh in the z = 0 plane, in the surface's own units and frame (feet for a
|
|
88
|
+
* baseball field or a rink, metres for a pitch; origin where sporty puts it), ready for a three.js `BufferGeometry`.
|
|
89
|
+
*/
|
|
90
|
+
interface SurfaceMesh {
|
|
91
|
+
/** x, y, z per vertex; z is 0. */
|
|
92
|
+
positions: Float32Array;
|
|
93
|
+
/** r, g, b, a per vertex, 0 to 1, in linear sRGB (three.js's working colour space): a vertex-colour attribute. */
|
|
94
|
+
colors: Float32Array;
|
|
95
|
+
/** Three vertex indices per triangle, counter-clockwise seen from +z, back to front. */
|
|
96
|
+
indices: Uint32Array;
|
|
97
|
+
/** Every loop, in drawing order (later ones lie over earlier ones in the same plane). */
|
|
98
|
+
loops: SurfaceLoop[];
|
|
99
|
+
units: Units;
|
|
100
|
+
/** The surface's extent, `[x0, y0, x1, y1]`. */
|
|
101
|
+
bbox: readonly [x0: number, y0: number, x1: number, y1: number];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* sporty's playing surface for `sport` and `league` (sporty's own league keys, e.g. `surfaceMesh("baseball", "mlb")`,
|
|
105
|
+
* `surfaceMesh("hockey", "nhl")`) as one triangle mesh in the z = 0 plane: plain buffers (positions, linear-sRGB
|
|
106
|
+
* vertex colours, indices), with no three.js import, ready for `new BufferGeometry()` in three.js or the `<Surface3D>`
|
|
107
|
+
* component in `sdvplot/react`. World units: the surface's own (feet for a baseball field or a rink, metres for a
|
|
108
|
+
* pitch), in sporty's frame: a baseball field has home plate's point at the origin and +y towards centre field,
|
|
109
|
+
* Statcast's frame, so `statcastPitchPath` points draw over it unconverted; the other surfaces have the origin at
|
|
110
|
+
* their centre. The surface's background colour (a baseball field's outfield grass) is the first loop, over the
|
|
111
|
+
* extent; then each closed loop of each polygon feature, in sporty's drawing order (a faceoff circle is two loops,
|
|
112
|
+
* its half rings), filled with its colour, alpha included. Hidden features (a `#rrggbb00` fill) and text (a football
|
|
113
|
+
* field's numbers) are left out, and sporty's heights (goal frames, backboards) are flattened: z is 0 throughout.
|
|
114
|
+
* Every loop lies in one plane, so draw the mesh in order without depth writes (`depthWrite: false`), first
|
|
115
|
+
* (`renderOrder` -1), with `vertexColors`, `transparent` and `toneMapped: false`, as `<Surface3D>` does: later loops
|
|
116
|
+
* then paint over earlier ones with no z-fighting. `o` is passed to sporty's `surface()` (`colorUpdates`, `units`,
|
|
117
|
+
* `rotation`, ...).
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* import { surfaceMesh } from "@sportsdataverse/sdvplot/three";
|
|
122
|
+
*
|
|
123
|
+
* const field = surfaceMesh("baseball", "mlb");
|
|
124
|
+
* // in three.js: geometry.setAttribute("position", new BufferAttribute(field.positions, 3)), and so on
|
|
125
|
+
* field.loops.map((l) => l.name);
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
declare function surfaceMesh<S extends Sport>(sport: S, league: LeagueOf<S>, o?: OptionsOf<S>): SurfaceMesh;
|
|
129
|
+
|
|
130
|
+
/** `teamColorPalette` options. */
|
|
131
|
+
interface ThreeColorOptions {
|
|
132
|
+
/** "primary" (default) or "secondary": the team colour each team gets. */
|
|
133
|
+
which?: Which;
|
|
134
|
+
season?: SeasonInput;
|
|
135
|
+
idSystem?: IdSystem;
|
|
136
|
+
/** The colour of a team that does not resolve. Default "#808080". */
|
|
137
|
+
fallback?: string;
|
|
138
|
+
/**
|
|
139
|
+
* The scene's background (what the colours are seen against, such as the surface under them). Given, a colour under
|
|
140
|
+
* 3:1 contrast against it is swapped for the team's other colour, else moved towards white (on a dark background)
|
|
141
|
+
* or black (a light one) until it reads; a colour that would then sit too close to an earlier team's (CIE76 ΔE under
|
|
142
|
+
* 20) takes the team's other colour, or the nearest colour to its own that is distinct and still reads; failing that
|
|
143
|
+
* it is kept, with one warning. A team that does not resolve keeps `fallback` and stays out of that search.
|
|
144
|
+
*/
|
|
145
|
+
background?: string;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* One colour per team, in order, for each team's mesh material (a `Trajectory`'s `color`, a `MeshBasicMaterial`'s
|
|
149
|
+
* `color`). With `background`, every colour keeps 3:1 contrast against it (dark mode) and the teams aim to stay
|
|
150
|
+
* distinct (when they cannot, it keeps the readable colour and warns once). A team listed twice gets one colour.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```ts
|
|
154
|
+
* import { loadLeague } from "@sportsdataverse/sdvplot";
|
|
155
|
+
* import { teamColorPalette } from "@sportsdataverse/sdvplot/three";
|
|
156
|
+
*
|
|
157
|
+
* await loadLeague("mlb");
|
|
158
|
+
* teamColorPalette("mlb", ["NYY", "CWS"], { background: "#395d33" }); // readable on sporty's baseball grass
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
declare function teamColorPalette(league: League, teams: readonly unknown[], o?: ThreeColorOptions): string[];
|
|
162
|
+
/** A point in world units: `[x, y, z]`, z up. */
|
|
163
|
+
type Point3 = readonly [x: number, y: number, z: number];
|
|
164
|
+
/** `trajectoryTube` options: the radius in world units, and either a colour or a team. */
|
|
165
|
+
interface TrajectoryTubeOptions extends Omit<ThreeColorOptions, "fallback"> {
|
|
166
|
+
/**
|
|
167
|
+
* The tube's radius in world units: the points' own units (feet for a pitch over a baseball field), not px. Three.js
|
|
168
|
+
* draws in the scene's units, so a tube is as thick as this whatever the camera; a baseball's radius is 0.121 ft.
|
|
169
|
+
*/
|
|
170
|
+
radius: number;
|
|
171
|
+
/** Vertices round each ring, 3 or more. Default 12. */
|
|
172
|
+
radialSegments?: number;
|
|
173
|
+
/** The tube's colour, `#rrggbb`. Give this or `team` and `league`. */
|
|
174
|
+
color?: string;
|
|
175
|
+
/** The team whose colour the tube takes (abbreviation, name or id), with `league`. */
|
|
176
|
+
team?: unknown;
|
|
177
|
+
league?: League;
|
|
178
|
+
/** The colour of a team that does not resolve. Default "#808080". */
|
|
179
|
+
fallback?: string;
|
|
180
|
+
}
|
|
181
|
+
/** A tube mesh, ready for a three.js `BufferGeometry` (world units). */
|
|
182
|
+
interface TubeMesh {
|
|
183
|
+
/** x, y, z per vertex: `radialSegments` vertices round each point of the path, ring after ring. */
|
|
184
|
+
positions: Float32Array;
|
|
185
|
+
/** The outward unit normal of each vertex. */
|
|
186
|
+
normals: Float32Array;
|
|
187
|
+
/** Three vertex indices per triangle, counter-clockwise seen from outside the tube. */
|
|
188
|
+
indices: Uint32Array;
|
|
189
|
+
/** The tube's colour, `#rrggbb` (sRGB, as a three.js `Color` takes it). */
|
|
190
|
+
color: string;
|
|
191
|
+
radius: number;
|
|
192
|
+
/** The path's length, world units. */
|
|
193
|
+
length: number;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* A tube along a 3D polyline (a pitch, a ball flight, a skater's path) as plain mesh buffers, in the points' own world
|
|
197
|
+
* units: ready for `new BufferGeometry()` in three.js, or the `<Trajectory>` component in `sdvplot/react` for
|
|
198
|
+
* react-three-fiber. The tube is `radius` round (world units, not px), one ring of `radialSegments` vertices at each
|
|
199
|
+
* point, the rings turned along the path without twisting (each ring's frame is the last one's, carried along: a
|
|
200
|
+
* parallel-transport frame), with open ends. Its colour is `color`, or the team's (`team` and `league`; `background`
|
|
201
|
+
* makes it read against the scene, as `teamColorPalette` does); draw it with that colour unlit and not tone-mapped
|
|
202
|
+
* (`MeshBasicMaterial`, `toneMapped: false`) to show the team's colour as it is. A repeated point is skipped.
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```ts
|
|
206
|
+
* import { loadLeague } from "@sportsdataverse/sdvplot";
|
|
207
|
+
* import { statcastPitchPath, trajectoryTube } from "@sportsdataverse/sdvplot/three";
|
|
208
|
+
*
|
|
209
|
+
* await loadLeague("mlb");
|
|
210
|
+
* // a 96.3 mph four-seamer, Yankees at Red Sox, 2024-06-15 (Baseball Savant: game 746946, at-bat 48, pitch 4)
|
|
211
|
+
* const pitch = {
|
|
212
|
+
* vx0: -6.245145230392043, vy0: -140.0548111003631, vz0: -4.647911601289857,
|
|
213
|
+
* ax: 12.681580763686934, ay: 32.15363682007945, az: -12.428443024407864,
|
|
214
|
+
* plate_x: 0.42927397039380755, plate_z: 3.546869816011372, release_pos_y: 54.12, game_year: 2024,
|
|
215
|
+
* };
|
|
216
|
+
* const tube = trajectoryTube(statcastPitchPath(pitch), { radius: 0.121, team: "NYY", league: "mlb" });
|
|
217
|
+
* // in three.js: geometry.setAttribute("position", new BufferAttribute(tube.positions, 3)), and so on
|
|
218
|
+
* tube.color;
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
declare function trajectoryTube(points: readonly Point3[], o: TrajectoryTubeOptions): TubeMesh;
|
|
222
|
+
|
|
223
|
+
export { type Point3, type StatcastPathOptions, type StatcastPitch, type SurfaceLoop, type SurfaceMesh, type ThreeColorOptions, type TrajectoryTubeOptions, type TubeMesh, statcastPitchPath, surfaceMesh, teamColorPalette, trajectoryTube };
|
package/dist/three.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export { surfaceMesh } from './chunk-B2MP37SR.js';
|
|
2
|
+
export { teamColorPalette, trajectoryTube } from './chunk-AMXASQHT.js';
|
|
3
|
+
import './chunk-UL5M6KDE.js';
|
|
4
|
+
import './chunk-7ULOCCFL.js';
|
|
5
|
+
import './chunk-ZAEQXFJM.js';
|
|
6
|
+
import './chunk-J6R2LEJ7.js';
|
|
7
|
+
import { InputError } from './chunk-E7G7VSLV.js';
|
|
8
|
+
|
|
9
|
+
// src/three/statcast.ts
|
|
10
|
+
var FIELDS = ["vx0", "vy0", "vz0", "ax", "ay", "az", "plate_x", "plate_z", "release_pos_y"];
|
|
11
|
+
var PLATE_Y = { front: 17 / 12, middle: 8.5 / 12 };
|
|
12
|
+
var Y0 = 50;
|
|
13
|
+
function statcastPitchPath(pitch, o = {}) {
|
|
14
|
+
for (const k of FIELDS)
|
|
15
|
+
if (!Number.isFinite(pitch[k]))
|
|
16
|
+
throw new InputError(`statcastPitchPath needs a number in the pitch's ${k} (a Baseball Savant column)`);
|
|
17
|
+
const steps = o.steps ?? 40;
|
|
18
|
+
if (!(Number.isInteger(steps) && steps >= 1))
|
|
19
|
+
throw new InputError(`steps is a whole number, 1 or more, got ${String(steps)}`);
|
|
20
|
+
const year = pitch.game_year;
|
|
21
|
+
const plate = o.plate ?? (Number.isFinite(year) ? year >= 2026 ? "middle" : "front" : void 0);
|
|
22
|
+
if (plate !== "front" && plate !== "middle")
|
|
23
|
+
throw new InputError(
|
|
24
|
+
`statcastPitchPath needs the pitch's game_year, or plate: "front" (through 2025) or "middle" (from 2026), to know where plate_x and plate_z were measured`
|
|
25
|
+
);
|
|
26
|
+
const { vx0, vy0, vz0, ax, ay, az, plate_x, plate_z, release_pos_y } = pitch;
|
|
27
|
+
const when = (y) => {
|
|
28
|
+
const c = Y0 - y;
|
|
29
|
+
const d = vy0 * vy0 - 2 * ay * c;
|
|
30
|
+
if (!(vy0 < 0 && d >= 0))
|
|
31
|
+
throw new InputError(`the pitch never reaches y = ${y} ft (vy0 ${vy0}, ay ${ay})`);
|
|
32
|
+
return 2 * c / (-vy0 + Math.sqrt(d));
|
|
33
|
+
};
|
|
34
|
+
const tp = when(PLATE_Y[plate]);
|
|
35
|
+
const tr = when(release_pos_y);
|
|
36
|
+
const x50 = plate_x - vx0 * tp - ax * tp * tp / 2;
|
|
37
|
+
const z50 = plate_z - vz0 * tp - az * tp * tp / 2;
|
|
38
|
+
const at = (t) => [
|
|
39
|
+
x50 + vx0 * t + ax * t * t / 2,
|
|
40
|
+
Y0 + vy0 * t + ay * t * t / 2,
|
|
41
|
+
z50 + vz0 * t + az * t * t / 2
|
|
42
|
+
];
|
|
43
|
+
const path = Array.from({ length: steps + 1 }, (_, i) => at(tr + (tp - tr) * i / steps));
|
|
44
|
+
path[steps] = [plate_x, PLATE_Y[plate], plate_z];
|
|
45
|
+
return path;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { statcastPitchPath };
|
|
49
|
+
//# sourceMappingURL=three.js.map
|
|
50
|
+
//# sourceMappingURL=three.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/three/statcast.ts"],"names":[],"mappings":"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","file":"three.js","sourcesContent":["import { InputError } from \"../errors.js\";\n\n/**\n * The Baseball Savant (Statcast) pitch fields `statcastPitchPath` reads, by their Savant CSV column names: feet, feet\n * per second and feet per second squared, in Statcast's frame (catcher's view: x to the catcher's right, the\n * first-base side; y from the point of home plate towards the mound; z up).\n */\nexport interface StatcastPitch {\n /** Velocity at y = 50 ft, ft/s. */\n vx0: number;\n vy0: number;\n vz0: number;\n /** Acceleration (constant over the flight), ft/s². */\n ax: number;\n ay: number;\n az: number;\n /**\n * Where the pitch crossed home plate, ft: at its front edge (y = 17/12 ft) through 2025, and at its middle\n * (y = 8.5/12 ft, \"to align with the ABS system\") from 2026, per Savant's CSV documentation.\n */\n plate_x: number;\n plate_z: number;\n /** The release point's distance from home plate, ft (60.5 less the pitcher's extension). */\n release_pos_y: number;\n /** The season: it says which plate plane plate_x and plate_z are at (see `StatcastPathOptions.plate`). */\n game_year?: number;\n}\n/** `statcastPitchPath` options. */\nexport interface StatcastPathOptions {\n /** Steps from release to the plate, equal in time: the path has `steps + 1` points. Default 40. */\n steps?: number;\n /**\n * The plane plate_x and plate_z were measured at: \"front\" (the plate's front edge, Savant through 2025) or \"middle\"\n * (halfway along it, from 2026). Default: from the pitch's `game_year`; with neither it is an InputError, as a wrong\n * plane moves the whole path 8.5 inches along its flight.\n */\n plate?: \"front\" | \"middle\";\n}\n\nconst FIELDS = [\"vx0\", \"vy0\", \"vz0\", \"ax\", \"ay\", \"az\", \"plate_x\", \"plate_z\", \"release_pos_y\"] as const;\n/** Where Statcast reports plate_x and plate_z, ft from home plate's point: its front edge, or halfway to it. */\nconst PLATE_Y = { front: 17 / 12, middle: 8.5 / 12 } as const;\n/** The plane Statcast's velocities (vx0, vy0, vz0) are measured at, ft from home plate. */\nconst Y0 = 50;\n\n/**\n * A pitch's flight from release to the front of home plate, as `[x, y, z]` points in feet in Statcast's frame, which\n * is sporty's baseball frame too (home plate's point at the origin, y towards the mound), so the path draws over\n * `surfaceMesh(\"baseball\", \"mlb\")` unconverted. Statcast describes each pitch with the PITCHf/x nine-parameter fit:\n * constant acceleration (ax, ay, az) and the velocity (vx0, vy0, vz0) at y = 50 ft, so\n * `p(t) = p50 + v0 t + a t² / 2` (Baseball Savant CSV documentation, https://baseballsavant.mlb.com/csv-docs: \"vx0,\n * vy0, vz0: the velocity of the pitch, in feet per second, in x, y, and z dimensions, determined at y=50 feet\"; \"ax,\n * ay, az: the acceleration of the pitch, in feet per second per second ... determined at y=50 feet\"; A. M. Nathan,\n * \"Determining Pitch Movement from PITCHf/x Data\", 2012, https://baseball.physics.illinois.edu/Movement.pdf). Savant's\n * CSV leaves out x and z at y = 50, so they are solved from where the fit crosses the plate (plate_x, plate_z: at its\n * front edge, y = 17/12 ft, through 2025; at its middle, y = 8.5/12 ft, from 2026, per the same documentation); the\n * path then runs, equal steps in time, from y = release_pos_y to that plane. The 2026 middle-of-plate rule is from\n * Savant's documentation of its MLB search: for its World Baseball Classic or minor-league searches, pass `plate`\n * explicitly rather than rely on `game_year`. Each field must be a number: filter out rows with an empty CSV cell\n * first, since `Number(\"\")` is 0 and would pass as a real value. Traced back to the release, it lands on\n * Savant's own release_pos_x / release_pos_z within their 0.01 ft rounding (checked on 2024 and 2026 pitches).\n *\n * @example\n * ```ts\n * import { statcastPitchPath } from \"@sportsdataverse/sdvplot/three\";\n *\n * // a 96.3 mph four-seamer, Yankees at Red Sox, 2024-06-15 (Baseball Savant: game 746946, at-bat 48, pitch 4)\n * const pitch = {\n * vx0: -6.245145230392043, vy0: -140.0548111003631, vz0: -4.647911601289857,\n * ax: 12.681580763686934, ay: 32.15363682007945, az: -12.428443024407864,\n * plate_x: 0.42927397039380755, plate_z: 3.546869816011372, release_pos_y: 54.12, game_year: 2024,\n * };\n * const path = statcastPitchPath(pitch); // 41 points, release to plate\n * path[0]; // about [2.05, 54.12, 6.17]: Savant's release_pos_x, release_pos_y, release_pos_z\n * ```\n */\nexport function statcastPitchPath(\n pitch: StatcastPitch,\n o: StatcastPathOptions = {},\n): [x: number, y: number, z: number][] {\n for (const k of FIELDS)\n if (!Number.isFinite(pitch[k]))\n throw new InputError(`statcastPitchPath needs a number in the pitch's ${k} (a Baseball Savant column)`);\n const steps = o.steps ?? 40;\n if (!(Number.isInteger(steps) && steps >= 1))\n throw new InputError(`steps is a whole number, 1 or more, got ${String(steps)}`);\n const year = pitch.game_year;\n const plate =\n o.plate ?? (Number.isFinite(year) ? ((year as number) >= 2026 ? \"middle\" : \"front\") : undefined);\n if (plate !== \"front\" && plate !== \"middle\")\n throw new InputError(\n 'statcastPitchPath needs the pitch\\'s game_year, or plate: \"front\" (through 2025) or \"middle\" (from 2026), ' +\n \"to know where plate_x and plate_z were measured\",\n );\n const { vx0, vy0, vz0, ax, ay, az, plate_x, plate_z, release_pos_y } = pitch;\n // the time (from y = 50 ft) at which y(t) = 50 + vy0 t + ay t^2 / 2 reaches y: the root nearer 0, written so it stays\n // accurate however small ay is\n const when = (y: number): number => {\n const c = Y0 - y;\n const d = vy0 * vy0 - 2 * ay * c;\n if (!(vy0 < 0 && d >= 0))\n throw new InputError(`the pitch never reaches y = ${y} ft (vy0 ${vy0}, ay ${ay})`);\n return (2 * c) / (-vy0 + Math.sqrt(d));\n };\n const tp = when(PLATE_Y[plate]);\n const tr = when(release_pos_y);\n const x50 = plate_x - vx0 * tp - (ax * tp * tp) / 2;\n const z50 = plate_z - vz0 * tp - (az * tp * tp) / 2;\n const at = (t: number): [number, number, number] => [\n x50 + vx0 * t + (ax * t * t) / 2,\n Y0 + vy0 * t + (ay * t * t) / 2,\n z50 + vz0 * t + (az * t * t) / 2,\n ];\n const path = Array.from({ length: steps + 1 }, (_, i) => at(tr + ((tp - tr) * i) / steps));\n path[steps] = [plate_x, PLATE_Y[plate], plate_z]; // the measured crossing itself, free of rounding\n return path;\n}\n"]}
|