@sportsdataverse/sdvplot 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +94 -0
- package/README.md +908 -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 +132 -0
- package/dist/chartjs.js +339 -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-C2CDY5OG.js +137 -0
- package/dist/chunk-C2CDY5OG.js.map +1 -0
- package/dist/chunk-E7G7VSLV.js +43 -0
- package/dist/chunk-E7G7VSLV.js.map +1 -0
- package/dist/chunk-J6R2LEJ7.js +58 -0
- package/dist/chunk-J6R2LEJ7.js.map +1 -0
- package/dist/chunk-JJFTD4VB.js +175 -0
- package/dist/chunk-JJFTD4VB.js.map +1 -0
- package/dist/chunk-KKH6ABMA.js +223 -0
- package/dist/chunk-KKH6ABMA.js.map +1 -0
- package/dist/chunk-NLPH2VZT.js +127 -0
- package/dist/chunk-NLPH2VZT.js.map +1 -0
- package/dist/chunk-OKEE4AH6.js +30 -0
- package/dist/chunk-OKEE4AH6.js.map +1 -0
- package/dist/chunk-QY65DUEF.js +306 -0
- package/dist/chunk-QY65DUEF.js.map +1 -0
- package/dist/chunk-RMT2GMFH.js +97 -0
- package/dist/chunk-RMT2GMFH.js.map +1 -0
- package/dist/chunk-SEBO35DV.js +76 -0
- package/dist/chunk-SEBO35DV.js.map +1 -0
- package/dist/chunk-TAXO3VXH.js +556 -0
- package/dist/chunk-TAXO3VXH.js.map +1 -0
- package/dist/chunk-TDNBAEQB.js +105 -0
- package/dist/chunk-TDNBAEQB.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/d3.d.ts +234 -0
- package/dist/d3.js +198 -0
- package/dist/d3.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/headshots-B2J_LOO8.d.ts +40 -0
- package/dist/index.d.ts +182 -0
- package/dist/index.js +10 -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/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/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/ohl-2FEM554G.js +113 -0
- package/dist/ohl-C5Q4W6JB.js +32 -0
- package/dist/ohl-TQ4J46VC.js +66 -0
- package/dist/phf-H7POU2KW.js +32 -0
- package/dist/phf-K5IFMZUM.js +13 -0
- package/dist/phf-TWJI3BOW.js +20 -0
- package/dist/plot.d.ts +449 -0
- package/dist/plot.js +711 -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 +101 -0
- package/dist/react.js +164 -0
- package/dist/react.js.map +1 -0
- package/dist/resolve-CQW-jzvr.d.ts +28 -0
- package/dist/selection-D4T3PkRt.d.ts +140 -0
- package/dist/shots.d.ts +364 -0
- package/dist/shots.js +85 -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/surfaces-ChaD9nei.d.ts +38 -0
- package/dist/testing.d.ts +75 -0
- package/dist/testing.js +240 -0
- package/dist/testing.js.map +1 -0
- package/dist/tiers-B6pUBPcE.d.ts +93 -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/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/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 +154 -3
package/dist/shots.d.ts
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { Lattice, BinnerOptions } from './bins.js';
|
|
2
|
+
export { BinOf, BinShape, Binner, binner, cellPath, cellPoints, hexagonPath, hexbin, squarePath, squarebin } from './bins.js';
|
|
3
|
+
import { BasketballZone } from '@sportsdataverse/sporty';
|
|
4
|
+
|
|
5
|
+
/** The diff colours: `"rdbu"` (blazing-the-nets `main`, the default) or `"master"` (its 2021 five stops). */
|
|
6
|
+
type DiffPalette = "rdbu" | "master";
|
|
7
|
+
/** Options for `diffScale`. */
|
|
8
|
+
interface DiffScaleOptions {
|
|
9
|
+
/**
|
|
10
|
+
* `"rdbu"` (default): RdBu reversed, above league red (blazing-the-nets `main`), clamped at `±domain`.
|
|
11
|
+
* `"master"`: the 2021 five-stop red-yellow-green, UNCLAMPED like master's d3 `scaleLinear`: a diff past the end
|
|
12
|
+
* stops (±0.99) extrapolates, each channel then clamped to 0-255. An infinite diff takes the end colour (d3 gives
|
|
13
|
+
* `rgb(0, 0, 0)` for `Infinity` and `rgb(0, 255, 0)` for `-Infinity`).
|
|
14
|
+
*/
|
|
15
|
+
palette?: DiffPalette;
|
|
16
|
+
/** `"dark"` swaps RdBu's near-white centre for `#303030` so an average mark recedes (`theme.ts:52-58`). Ignored by `"master"`. */
|
|
17
|
+
theme?: "light" | "dark";
|
|
18
|
+
/** Saturation in FG% fraction, finite and above 0 (else InputError); default 0.15 (`theme.ts:29`). `"master"` keeps its own stops. */
|
|
19
|
+
domain?: number;
|
|
20
|
+
/** Colour for `null` (no league figure); default `var(--sdv-muted, #525252)`. */
|
|
21
|
+
nullColor?: string;
|
|
22
|
+
}
|
|
23
|
+
/** A diff colour function plus the equivalent Plot colour-scale options (for fills and `Plot.legend`). */
|
|
24
|
+
interface DiffScale {
|
|
25
|
+
(diff: number | null): string;
|
|
26
|
+
/** Diffs to place gradient stops at: 5 across [-D, D] (`theme.ts:127-130`), or `master`'s own 5 stops. */
|
|
27
|
+
readonly stops: readonly number[];
|
|
28
|
+
readonly plot: {
|
|
29
|
+
readonly type: "diverging" | "linear";
|
|
30
|
+
readonly domain: readonly number[];
|
|
31
|
+
readonly range?: readonly string[];
|
|
32
|
+
readonly scheme?: "RdBu";
|
|
33
|
+
readonly reverse?: boolean;
|
|
34
|
+
readonly pivot?: number;
|
|
35
|
+
readonly interpolate?: "rgb";
|
|
36
|
+
readonly clamp: boolean;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The FG%-vs-league colour: `diffScale()(0.05)`. Light RdBu, dark RdBu, or `master`'s palette; `null` is
|
|
41
|
+
* `nullColor`. `.plot` is the same scale as Observable Plot `color` options, so `Plot.legend({ color: s.plot })`
|
|
42
|
+
* draws the matching key.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { diffScale } from "@sportsdataverse/sdvplot/shots";
|
|
47
|
+
*
|
|
48
|
+
* diffScale({ theme: "dark" })(0.05);
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
declare function diffScale(o?: DiffScaleOptions): DiffScale;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* One field-goal attempt, as the `nba_stats_shots` release ships it: `x_legacy`/`y_legacy` in tenths of a foot,
|
|
55
|
+
* hoop at the origin, y toward half court; `shot_distance` in ROUNDED feet. Map other schemas once, first.
|
|
56
|
+
*/
|
|
57
|
+
interface ShotRow {
|
|
58
|
+
readonly x_legacy: number;
|
|
59
|
+
readonly y_legacy: number;
|
|
60
|
+
readonly shot_distance: number;
|
|
61
|
+
/** 2 or 3; zones read it (the scorer's call, not the coordinates). */
|
|
62
|
+
readonly shot_value: number;
|
|
63
|
+
/** `"Made"` or `"Missed"`. */
|
|
64
|
+
readonly shot_result: string;
|
|
65
|
+
}
|
|
66
|
+
/** Attempts, makes and FG% (`makes / attempts`, null with no attempts): the counts every shot aggregate carries. */
|
|
67
|
+
interface Split {
|
|
68
|
+
readonly attempts: number;
|
|
69
|
+
readonly makes: number;
|
|
70
|
+
readonly fgPct: number | null;
|
|
71
|
+
}
|
|
72
|
+
/** One cell of `binShots`: its centre, counts, mean shot distance and majority zone. */
|
|
73
|
+
interface CellBin extends Split {
|
|
74
|
+
/** Cell centre, legacy tenths. */
|
|
75
|
+
readonly x: number;
|
|
76
|
+
readonly y: number;
|
|
77
|
+
readonly meanDistance: number;
|
|
78
|
+
/** The zone most of the cell's attempts fall in (first seen wins a tie). */
|
|
79
|
+
readonly zone: BasketballZone;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Bin shots in DATA space (legacy tenths), dropping off-court points (aggregate.ts:86-104) and points with a
|
|
83
|
+
* non-finite coordinate (`main` lets `y_legacy = Infinity` through). `cell` is a hexagon
|
|
84
|
+
* radius (10 = a 1 ft hex) or any `binner` lattice, e.g. `{ shape: "square", side: 15 }`. Bins come out in
|
|
85
|
+
* first-seen order (d3-hexbin's, for hexagons).
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* import { binShots } from "@sportsdataverse/sdvplot/shots";
|
|
90
|
+
*
|
|
91
|
+
* // Brooklyn shots from the sportsdataverse-data nba_stats_shots release, 2025-26
|
|
92
|
+
* const shots = [
|
|
93
|
+
* { x_legacy: -1, y_legacy: 7, shot_distance: 1, shot_value: 2, shot_result: "Made" },
|
|
94
|
+
* { x_legacy: 0, y_legacy: 0, shot_distance: 0, shot_value: 2, shot_result: "Missed" },
|
|
95
|
+
* { x_legacy: -44, y_legacy: 252, shot_distance: 26, shot_value: 3, shot_result: "Made" },
|
|
96
|
+
* ];
|
|
97
|
+
* binShots(shots, 15);
|
|
98
|
+
* binShots(shots, { shape: "square", side: 15 });
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
declare function binShots(shots: readonly ShotRow[], cell: number | BinnerOptions): CellBin[];
|
|
102
|
+
/**
|
|
103
|
+
* League attempts behind a cell's league rate before it falls back to the zone, and the colour prior: one number for
|
|
104
|
+
* both (aggregate.ts:106-110). It is blazing-the-nets' tunable, set in its commit 25c3ab9f ("shrunk colours"), not a
|
|
105
|
+
* derived or measured constant; `cellsVsLeague`'s `minLeague`, `shrunkDiff`'s `k` and `signaturePoints`' `prior`
|
|
106
|
+
* override it per call.
|
|
107
|
+
*/
|
|
108
|
+
declare const LEAGUE_PRIOR_ATTEMPTS = 25;
|
|
109
|
+
/**
|
|
110
|
+
* FG% minus the league rate `league`, shrunk toward it by a `k`-attempt prior: `(makes + k*L)/(attempts + k) - L`
|
|
111
|
+
* (aggregate.ts:112-119). `k = 0` is the raw difference (blazing-the-nets `master`). For colour, not labels.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* import { LEAGUE_PRIOR_ATTEMPTS, shrunkDiff } from "@sportsdataverse/sdvplot/shots";
|
|
116
|
+
*
|
|
117
|
+
* // Brooklyn 2025-26, the radius-15 hexagon centred (77.9, 45): 4 of 4, where the league made 270 of 636
|
|
118
|
+
* shrunkDiff(4, 4, 270 / 636, LEAGUE_PRIOR_ATTEMPTS); // +0.08, not the raw +0.58
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
declare function shrunkDiff(makes: number, attempts: number, league: number, k?: number): number;
|
|
122
|
+
/** One league cell in a `LeagueIndex`: centre (legacy tenths), attempts and FG%. */
|
|
123
|
+
interface LeagueCell {
|
|
124
|
+
readonly x: number;
|
|
125
|
+
readonly y: number;
|
|
126
|
+
readonly attempts: number;
|
|
127
|
+
readonly fgPct: number | null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* A season's league cells and zone rates on one lattice (`{ radius }` for hexagons, `{ shape: "square", side }`):
|
|
131
|
+
* plain JSON, computed once (aggregate.ts:125-146). blazing-the-nets names the cells `hexes`; here they are `cells`
|
|
132
|
+
* for either shape, so map that key when loading its JSON.
|
|
133
|
+
*/
|
|
134
|
+
type LeagueIndex = Lattice & {
|
|
135
|
+
readonly cells: readonly LeagueCell[];
|
|
136
|
+
readonly zones: Readonly<Record<BasketballZone, Split>>;
|
|
137
|
+
};
|
|
138
|
+
/** A player cell with the league FG% it is compared to (`cellsVsLeague`, `cellsVsDistance`); null with no league figure. */
|
|
139
|
+
interface CellVsLeague extends CellBin {
|
|
140
|
+
readonly leagueFgPct: number | null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The league index every player chart compares against (aggregate.ts:140-146), on a hexagon radius or any `binner`
|
|
144
|
+
* lattice; it stores the resolved lattice, so `equalArea` becomes the square's `side`.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* import { leagueIndex } from "@sportsdataverse/sdvplot/shots";
|
|
149
|
+
*
|
|
150
|
+
* // Brooklyn shots from the sportsdataverse-data nba_stats_shots release, 2025-26
|
|
151
|
+
* const shots = [
|
|
152
|
+
* { x_legacy: -1, y_legacy: 7, shot_distance: 1, shot_value: 2, shot_result: "Made" },
|
|
153
|
+
* { x_legacy: 0, y_legacy: 0, shot_distance: 0, shot_value: 2, shot_result: "Missed" },
|
|
154
|
+
* { x_legacy: -44, y_legacy: 252, shot_distance: 26, shot_value: 3, shot_result: "Made" },
|
|
155
|
+
* ];
|
|
156
|
+
* leagueIndex(shots, 15).cells;
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
declare function leagueIndex(league: readonly ShotRow[], cell: number | BinnerOptions): LeagueIndex;
|
|
160
|
+
/**
|
|
161
|
+
* Player cells with the league FG% of the SAME cell (the player is binned on the index's own lattice, so the
|
|
162
|
+
* centres are equal); a cell the league shot fewer than `minLeague` times from falls back to its zone's league rate
|
|
163
|
+
* (aggregate.ts:155-165; blazing-the-nets `main`).
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```ts
|
|
167
|
+
* import { cellsVsLeague, leagueIndex } from "@sportsdataverse/sdvplot/shots";
|
|
168
|
+
*
|
|
169
|
+
* // Brooklyn shots from the sportsdataverse-data nba_stats_shots release, 2025-26
|
|
170
|
+
* const made = { x_legacy: 0, y_legacy: 0, shot_distance: 0, shot_value: 2, shot_result: "Made" };
|
|
171
|
+
* const missed = { ...made, shot_result: "Missed" };
|
|
172
|
+
* cellsVsLeague([made], leagueIndex([made, missed], 15));
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
declare function cellsVsLeague(player: readonly ShotRow[], league: LeagueIndex, minLeague?: number): CellVsLeague[];
|
|
176
|
+
/** How a cell's attempts set its size: `main` (sqrt to the 95th percentile) or `master` (linear, capped). */
|
|
177
|
+
type SizeRule = "sqrt-p95" | "linear-cap";
|
|
178
|
+
/** `sizeCells`' result: each cell's size, the cap, the size of any count, and the counts for a size key. */
|
|
179
|
+
interface CellSizes {
|
|
180
|
+
/** Size per cell in legacy tenths: a hexagon's circumradius, or a square's side. 0 hides a cell. */
|
|
181
|
+
readonly r: readonly number[];
|
|
182
|
+
/** Attempts at which size saturates. */
|
|
183
|
+
readonly cap: number;
|
|
184
|
+
/** The size of any attempt count, for a size key. */
|
|
185
|
+
readonly size: (attempts: number) => number;
|
|
186
|
+
/** A size key: 1, half the cap, the cap (blazing-the-nets `main` `lib/charts/hexShotChart.ts:199`). */
|
|
187
|
+
readonly steps: readonly number[];
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Frequency size for each cell (the dual encoding's size half; colour is `diffScale`), on the lattice's full cell
|
|
191
|
+
* size `R` (a hexagon's radius, or a square's side; pass the index itself: `sizeCells(cells, index)`).
|
|
192
|
+
* `"sqrt-p95"` (default, `main` `hexShotChart.ts:65-81`): a sqrt scale of attempts to `R`, capped at the 95th
|
|
193
|
+
* percentile so one busy rim cell does not shrink the rest; area follows attempts for either shape.
|
|
194
|
+
* `"linear-cap"` (`master` `Hexagon/index.js:22-30`): attempts in tenths, capped at `R`, cells under 2 attempts
|
|
195
|
+
* hidden.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```ts
|
|
199
|
+
* import { binShots, sizeCells } from "@sportsdataverse/sdvplot/shots";
|
|
200
|
+
*
|
|
201
|
+
* // Brooklyn shots from the sportsdataverse-data nba_stats_shots release, 2025-26
|
|
202
|
+
* const shots = [
|
|
203
|
+
* { x_legacy: -1, y_legacy: 7, shot_distance: 1, shot_value: 2, shot_result: "Made" },
|
|
204
|
+
* { x_legacy: 0, y_legacy: 0, shot_distance: 0, shot_value: 2, shot_result: "Missed" },
|
|
205
|
+
* { x_legacy: -44, y_legacy: 252, shot_distance: 26, shot_value: 3, shot_result: "Made" },
|
|
206
|
+
* ];
|
|
207
|
+
* sizeCells(binShots(shots, 15), { radius: 15 }).r; // the 2-attempt rim cell is full size, the 1-attempt three smaller
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
declare function sizeCells(cells: readonly {
|
|
211
|
+
readonly attempts: number;
|
|
212
|
+
}[], o: BinnerOptions & {
|
|
213
|
+
readonly rule?: SizeRule;
|
|
214
|
+
}): CellSizes;
|
|
215
|
+
/**
|
|
216
|
+
* Attempts, makes and FG% per zone (aggregate.ts:269-277).
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```ts
|
|
220
|
+
* import { statsByZone } from "@sportsdataverse/sdvplot/shots";
|
|
221
|
+
*
|
|
222
|
+
* // a Brooklyn corner three, 2025-26 (sportsdataverse-data nba_stats_shots)
|
|
223
|
+
* statsByZone([{ x_legacy: 237, y_legacy: 34, shot_distance: 24, shot_value: 3, shot_result: "Made" }]);
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
declare function statsByZone(shots: readonly ShotRow[]): Record<BasketballZone, Split>;
|
|
227
|
+
|
|
228
|
+
/** One `fgPctByDistance` bin: the counts of the shots `distance` to `distance + binFt` ft out, and their share. */
|
|
229
|
+
interface DistanceBin extends Split {
|
|
230
|
+
/** First foot of the bin; `shot_distance` is rounded, so a 1-ft bin is centred on it. */
|
|
231
|
+
readonly distance: number;
|
|
232
|
+
/** Share of the attempts within `maxFt`. */
|
|
233
|
+
readonly share: number;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* FG% by `shot_distance` in `binFt` bins from 0 to `maxFt`; longer attempts are left out (aggregate.ts:177-192).
|
|
237
|
+
* The total within `maxFt` is the sum of `attempts`. `binFt` and `maxFt` must be finite numbers above 0 (else
|
|
238
|
+
* InputError).
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```ts
|
|
242
|
+
* import { fgPctByDistance } from "@sportsdataverse/sdvplot/shots";
|
|
243
|
+
*
|
|
244
|
+
* // Brooklyn shots from the sportsdataverse-data nba_stats_shots release, 2025-26
|
|
245
|
+
* const shots = [
|
|
246
|
+
* { x_legacy: -1, y_legacy: 7, shot_distance: 1, shot_value: 2, shot_result: "Made" },
|
|
247
|
+
* { x_legacy: 0, y_legacy: 0, shot_distance: 0, shot_value: 2, shot_result: "Missed" },
|
|
248
|
+
* { x_legacy: -44, y_legacy: 252, shot_distance: 26, shot_value: 3, shot_result: "Made" },
|
|
249
|
+
* ];
|
|
250
|
+
* fgPctByDistance(shots, 3)[0];
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
declare function fgPctByDistance(shots: readonly ShotRow[], binFt?: number, maxFt?: number): DistanceBin[];
|
|
254
|
+
/** A player distance bin with the league's FG% at the same distance and the difference (`vsLeague`). */
|
|
255
|
+
interface DistanceVsLeague extends DistanceBin {
|
|
256
|
+
readonly leagueFgPct: number | null;
|
|
257
|
+
/** Player minus league FG% (a fraction), null when either side has no attempts. */
|
|
258
|
+
readonly diff: number | null;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Pair player and league distance bins (same `binFt` and `maxFt`, else InputError) and take the difference
|
|
262
|
+
* (aggregate.ts:200-209).
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```ts
|
|
266
|
+
* import { fgPctByDistance, vsLeague } from "@sportsdataverse/sdvplot/shots";
|
|
267
|
+
*
|
|
268
|
+
* // Brooklyn shots from the sportsdataverse-data nba_stats_shots release, 2025-26
|
|
269
|
+
* const made = { x_legacy: 0, y_legacy: 0, shot_distance: 0, shot_value: 2, shot_result: "Made" };
|
|
270
|
+
* const missed = { ...made, shot_result: "Missed" };
|
|
271
|
+
* vsLeague(fgPctByDistance([made]), fgPctByDistance([made, missed]))[0];
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
declare function vsLeague(player: readonly DistanceBin[], league: readonly DistanceBin[]): DistanceVsLeague[];
|
|
275
|
+
/** One `statsBySide` bin: left, centre and right of the hoop for the shots in the bin that starts `distance` ft out. */
|
|
276
|
+
interface SideBin {
|
|
277
|
+
readonly distance: number;
|
|
278
|
+
readonly left: Split;
|
|
279
|
+
readonly centre: Split;
|
|
280
|
+
readonly right: Split;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Left / centre / right of the hoop by distance (aggregate.ts:218-240): left is `x < -centreHalfWidth`, right is
|
|
284
|
+
* `x > centreHalfWidth`. The default 0 puts `x == 0` in centre (`main`); `false` drops it, as `master`'s
|
|
285
|
+
* `binLeftRight` did (`src/utils/visuals/bin.ts:36-42`). `binFt` and `maxFt` as for `fgPctByDistance`; a
|
|
286
|
+
* `centreHalfWidth` that is negative or not finite throws InputError.
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* ```ts
|
|
290
|
+
* import { statsBySide } from "@sportsdataverse/sdvplot/shots";
|
|
291
|
+
*
|
|
292
|
+
* // Brooklyn shots from the sportsdataverse-data nba_stats_shots release, 2025-26
|
|
293
|
+
* const shots = [
|
|
294
|
+
* { x_legacy: -1, y_legacy: 7, shot_distance: 1, shot_value: 2, shot_result: "Made" },
|
|
295
|
+
* { x_legacy: 0, y_legacy: 0, shot_distance: 0, shot_value: 2, shot_result: "Missed" },
|
|
296
|
+
* { x_legacy: -44, y_legacy: 252, shot_distance: 26, shot_value: 3, shot_result: "Made" },
|
|
297
|
+
* ];
|
|
298
|
+
* statsBySide(shots, 3, 35, 7.5)[0];
|
|
299
|
+
* ```
|
|
300
|
+
*/
|
|
301
|
+
declare function statsBySide(shots: readonly ShotRow[], binFt?: number, maxFt?: number, centreHalfWidth?: number | false): SideBin[];
|
|
302
|
+
/**
|
|
303
|
+
* Player cells against the league FG% at the cell centre's distance, `floor(|centre| / 10)` ft, falling back one
|
|
304
|
+
* foot when that bin is empty (blazing-the-nets `master` `src/components/Hexagon/index.js:44-49`). `cell` is a
|
|
305
|
+
* hexagon radius (default 10, master's) or any `binner` lattice, e.g. `{ shape: "square", side: 10 }`. `byFoot` is
|
|
306
|
+
* read as feet, so it must be 1-ft bins from 0 (`fgPctByDistance(league)`), else InputError.
|
|
307
|
+
*
|
|
308
|
+
* The distance is `Math.hypot(x, y)`, a deliberate divergence from master's `Math.sqrt(x ** 2 + y ** 2)`
|
|
309
|
+
* (`src/lib/distance.js:3-4`). On a hexagon centre exactly a whole number of feet out, such as (15√3, 15) at 3 ft,
|
|
310
|
+
* master's sqrt returns 29.999…96 and reads the foot below; hypot reads the true foot. On the real BKN fixture at
|
|
311
|
+
* radius 10 that is 13 of 374 cells, among them the cells beside the rim (0.572 at 3 ft, where master reads 0.669
|
|
312
|
+
* at 2 ft). Every other cell reads master's foot.
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* ```ts
|
|
316
|
+
* import { cellsVsDistance, fgPctByDistance } from "@sportsdataverse/sdvplot/shots";
|
|
317
|
+
*
|
|
318
|
+
* // Brooklyn shots from the sportsdataverse-data nba_stats_shots release, 2025-26
|
|
319
|
+
* const made = { x_legacy: 0, y_legacy: 0, shot_distance: 0, shot_value: 2, shot_result: "Made" };
|
|
320
|
+
* const missed = { ...made, shot_result: "Missed" };
|
|
321
|
+
* cellsVsDistance([made], fgPctByDistance([made, missed]), { shape: "square", side: 10 });
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
declare function cellsVsDistance(player: readonly ShotRow[], byFoot: readonly DistanceBin[], cell?: number | BinnerOptions): CellVsLeague[];
|
|
325
|
+
|
|
326
|
+
/** One shooting-signature sample (`signaturePoints`): the input of the Plot `shootingSignature` and d3 `appendSignature`. */
|
|
327
|
+
interface SignaturePoint {
|
|
328
|
+
readonly distance: number;
|
|
329
|
+
/** Effective makes / attempts near this distance; null (a gap in the ribbon) under `minAttempts`. */
|
|
330
|
+
readonly fgPct: number | null;
|
|
331
|
+
readonly leagueFgPct: number | null;
|
|
332
|
+
readonly share: number;
|
|
333
|
+
/** The colour value: `shrunkDiff(makes, attempts, league, prior)`, null in a gap. */
|
|
334
|
+
readonly colourDiff: number | null;
|
|
335
|
+
}
|
|
336
|
+
/** Options for `signaturePoints`; `master` mode is `{ step: 1, smooth: false, minAttempts: 1, prior: 0 }`. */
|
|
337
|
+
interface SignatureOptions {
|
|
338
|
+
/** Feet between samples; default 0.25 (`:16`). `master` sampled each 1-ft bin. */
|
|
339
|
+
step?: number;
|
|
340
|
+
/** Kernel smoothing (sigma 0.9 ft for makes/attempts, 1 for share, 0.6 for league; `:17`, `:76-77`); default true. */
|
|
341
|
+
smooth?: boolean;
|
|
342
|
+
/** No ribbon past the last bin with this many attempts, nor where the effective attempts fall below it; default 5 (`:19-21`). */
|
|
343
|
+
minAttempts?: number;
|
|
344
|
+
/** Colour prior in attempts (`shrunkDiff` k); default 25. 0 = the raw difference (`master`). */
|
|
345
|
+
prior?: number;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* The shooting signature's samples: FG% from separately kernel-summed makes and attempts (a stretch with no
|
|
349
|
+
* shots is a gap, never 0%), the smoothed league FG% and shot share, and the shrunk colour value (`:69-100`).
|
|
350
|
+
* Input: 1-ft bins from `vsLeague(fgPctByDistance(player), fgPctByDistance(league))`.
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```ts
|
|
354
|
+
* import { fgPctByDistance, signaturePoints, vsLeague } from "@sportsdataverse/sdvplot/shots";
|
|
355
|
+
*
|
|
356
|
+
* // Brooklyn shots from the sportsdataverse-data nba_stats_shots release, 2025-26
|
|
357
|
+
* const made = { x_legacy: 0, y_legacy: 0, shot_distance: 0, shot_value: 2, shot_result: "Made" };
|
|
358
|
+
* const missed = { ...made, shot_result: "Missed" };
|
|
359
|
+
* signaturePoints(vsLeague(fgPctByDistance([made]), fgPctByDistance([made, missed])), { minAttempts: 1 })[0];
|
|
360
|
+
* ```
|
|
361
|
+
*/
|
|
362
|
+
declare function signaturePoints(bins: readonly DistanceVsLeague[], o?: SignatureOptions): SignaturePoint[];
|
|
363
|
+
|
|
364
|
+
export { BinnerOptions, type CellBin, type CellSizes, type CellVsLeague, type DiffPalette, type DiffScale, type DiffScaleOptions, type DistanceBin, type DistanceVsLeague, LEAGUE_PRIOR_ATTEMPTS, Lattice, type LeagueCell, type LeagueIndex, type ShotRow, type SideBin, type SignatureOptions, type SignaturePoint, type SizeRule, type Split, binShots, cellsVsDistance, cellsVsLeague, diffScale, fgPctByDistance, leagueIndex, shrunkDiff, signaturePoints, sizeCells, statsBySide, statsByZone, vsLeague };
|
package/dist/shots.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { made, split, binShots } from './chunk-QY65DUEF.js';
|
|
2
|
+
export { LEAGUE_PRIOR_ATTEMPTS, binShots, cellsVsLeague, diffScale, leagueIndex, shrunkDiff, signaturePoints, sizeCells, statsByZone } from './chunk-QY65DUEF.js';
|
|
3
|
+
export { binner, cellPath, cellPoints, hexagonPath, hexbin, squarePath, squarebin } from './chunk-C2CDY5OG.js';
|
|
4
|
+
import { InputError } from './chunk-E7G7VSLV.js';
|
|
5
|
+
|
|
6
|
+
// src/shots/distance.ts
|
|
7
|
+
function binCount(binFt, maxFt) {
|
|
8
|
+
for (const [name, v] of Object.entries({ binFt, maxFt }))
|
|
9
|
+
if (!(Number.isFinite(v) && v > 0))
|
|
10
|
+
throw new InputError(`${name} must be a finite number > 0, got ${String(v)}`);
|
|
11
|
+
return Math.floor(maxFt / binFt) + 1;
|
|
12
|
+
}
|
|
13
|
+
function fgPctByDistance(shots, binFt = 1, maxFt = 35) {
|
|
14
|
+
const bins = Array.from({ length: binCount(binFt, maxFt) }, () => ({ attempts: 0, makes: 0 }));
|
|
15
|
+
let total = 0;
|
|
16
|
+
for (const s of shots) {
|
|
17
|
+
if (s.shot_distance > maxFt) continue;
|
|
18
|
+
const b = bins[Math.floor(s.shot_distance / binFt)];
|
|
19
|
+
if (b === void 0) continue;
|
|
20
|
+
b.attempts += 1;
|
|
21
|
+
if (made(s)) b.makes += 1;
|
|
22
|
+
total += 1;
|
|
23
|
+
}
|
|
24
|
+
return bins.map((b, i) => ({
|
|
25
|
+
distance: i * binFt,
|
|
26
|
+
...split(b.attempts, b.makes),
|
|
27
|
+
share: total ? b.attempts / total : 0
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
function vsLeague(player, league) {
|
|
31
|
+
if (player.length !== league.length || player.some((b, i) => b.distance !== league[i]?.distance)) {
|
|
32
|
+
throw new InputError("vsLeague: player and league bins differ (use the same binFt and maxFt)");
|
|
33
|
+
}
|
|
34
|
+
return player.map((b, i) => {
|
|
35
|
+
const leagueFgPct = league[i]?.fgPct ?? null;
|
|
36
|
+
return {
|
|
37
|
+
...b,
|
|
38
|
+
leagueFgPct,
|
|
39
|
+
diff: b.fgPct !== null && leagueFgPct !== null ? b.fgPct - leagueFgPct : null
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function statsBySide(shots, binFt = 1, maxFt = 35, centreHalfWidth = 0) {
|
|
44
|
+
if (centreHalfWidth !== false && !(Number.isFinite(centreHalfWidth) && centreHalfWidth >= 0))
|
|
45
|
+
throw new InputError(
|
|
46
|
+
`statsBySide centreHalfWidth must be a finite number >= 0 or false, got ${String(centreHalfWidth)}`
|
|
47
|
+
);
|
|
48
|
+
const half = centreHalfWidth === false ? 0 : centreHalfWidth;
|
|
49
|
+
const acc = Array.from({ length: binCount(binFt, maxFt) }, () => ({
|
|
50
|
+
left: [0, 0],
|
|
51
|
+
centre: [0, 0],
|
|
52
|
+
right: [0, 0]
|
|
53
|
+
}));
|
|
54
|
+
for (const s of shots) {
|
|
55
|
+
if (s.shot_distance > maxFt) continue;
|
|
56
|
+
const bin = acc[Math.floor(s.shot_distance / binFt)];
|
|
57
|
+
if (bin === void 0) continue;
|
|
58
|
+
const x = s.x_legacy;
|
|
59
|
+
const side = x < -half ? bin.left : x > half ? bin.right : centreHalfWidth === false ? void 0 : bin.centre;
|
|
60
|
+
if (side === void 0) continue;
|
|
61
|
+
side[0] = (side[0] ?? 0) + 1;
|
|
62
|
+
if (made(s)) side[1] = (side[1] ?? 0) + 1;
|
|
63
|
+
}
|
|
64
|
+
return acc.map((b, i) => ({
|
|
65
|
+
distance: i * binFt,
|
|
66
|
+
left: split(b.left[0] ?? 0, b.left[1] ?? 0),
|
|
67
|
+
centre: split(b.centre[0] ?? 0, b.centre[1] ?? 0),
|
|
68
|
+
right: split(b.right[0] ?? 0, b.right[1] ?? 0)
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
function cellsVsDistance(player, byFoot, cell = 10) {
|
|
72
|
+
const bad = byFoot.findIndex((b, i) => b.distance !== i);
|
|
73
|
+
if (bad >= 0)
|
|
74
|
+
throw new InputError(
|
|
75
|
+
`cellsVsDistance needs 1-ft distance bins from 0 (fgPctByDistance(shots, 1)); bin ${bad} is at ${String(byFoot[bad]?.distance)} ft`
|
|
76
|
+
);
|
|
77
|
+
return binShots(player, cell).map((h) => {
|
|
78
|
+
const d = Math.floor(Math.hypot(h.x, h.y) / 10);
|
|
79
|
+
return { ...h, leagueFgPct: byFoot[d]?.fgPct ?? byFoot[d - 1]?.fgPct ?? null };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { cellsVsDistance, fgPctByDistance, statsBySide, vsLeague };
|
|
84
|
+
//# sourceMappingURL=shots.js.map
|
|
85
|
+
//# sourceMappingURL=shots.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shots/distance.ts"],"names":[],"mappings":"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","file":"shots.js","sourcesContent":["// Distance and side bins, ported from blazing-the-nets `main` lib/data/aggregate.ts (@31427b8), plus `master`'s\n// distance baseline for cells (src/components/Hexagon/index.js @35dfda6). Pure; the parity gate is fixtures/shots/oracle.json.\nimport type { BinnerOptions } from \"../bins/index.js\";\nimport { InputError } from \"../errors.js\";\nimport { type CellVsLeague, type ShotRow, type Split, binShots, made, split } from \"./aggregate.js\";\n\n/** One `fgPctByDistance` bin: the counts of the shots `distance` to `distance + binFt` ft out, and their share. */\nexport interface DistanceBin extends Split {\n /** First foot of the bin; `shot_distance` is rounded, so a 1-ft bin is centred on it. */\n readonly distance: number;\n /** Share of the attempts within `maxFt`. */\n readonly share: number;\n}\n\n/** The number of `binFt` bins from 0 to `maxFt`; both must be finite numbers above 0 (else InputError, naming it). */\nfunction binCount(binFt: number, maxFt: number): number {\n for (const [name, v] of Object.entries({ binFt, maxFt }))\n if (!(Number.isFinite(v) && v > 0))\n throw new InputError(`${name} must be a finite number > 0, got ${String(v)}`);\n return Math.floor(maxFt / binFt) + 1;\n}\n\n/**\n * FG% by `shot_distance` in `binFt` bins from 0 to `maxFt`; longer attempts are left out (aggregate.ts:177-192).\n * The total within `maxFt` is the sum of `attempts`. `binFt` and `maxFt` must be finite numbers above 0 (else\n * InputError).\n *\n * @example\n * ```ts\n * import { fgPctByDistance } from \"@sportsdataverse/sdvplot/shots\";\n *\n * // Brooklyn shots from the sportsdataverse-data nba_stats_shots release, 2025-26\n * const shots = [\n * { x_legacy: -1, y_legacy: 7, shot_distance: 1, shot_value: 2, shot_result: \"Made\" },\n * { x_legacy: 0, y_legacy: 0, shot_distance: 0, shot_value: 2, shot_result: \"Missed\" },\n * { x_legacy: -44, y_legacy: 252, shot_distance: 26, shot_value: 3, shot_result: \"Made\" },\n * ];\n * fgPctByDistance(shots, 3)[0];\n * ```\n */\nexport function fgPctByDistance(shots: readonly ShotRow[], binFt = 1, maxFt = 35): DistanceBin[] {\n const bins = Array.from({ length: binCount(binFt, maxFt) }, () => ({ attempts: 0, makes: 0 }));\n let total = 0;\n for (const s of shots) {\n if (s.shot_distance > maxFt) continue;\n const b = bins[Math.floor(s.shot_distance / binFt)];\n if (b === undefined) continue; // a negative distance\n b.attempts += 1;\n if (made(s)) b.makes += 1;\n total += 1;\n }\n return bins.map((b, i) => ({\n distance: i * binFt,\n ...split(b.attempts, b.makes),\n share: total ? b.attempts / total : 0,\n }));\n}\n\n/** A player distance bin with the league's FG% at the same distance and the difference (`vsLeague`). */\nexport interface DistanceVsLeague extends DistanceBin {\n readonly leagueFgPct: number | null;\n /** Player minus league FG% (a fraction), null when either side has no attempts. */\n readonly diff: number | null;\n}\n\n/**\n * Pair player and league distance bins (same `binFt` and `maxFt`, else InputError) and take the difference\n * (aggregate.ts:200-209).\n *\n * @example\n * ```ts\n * import { fgPctByDistance, vsLeague } from \"@sportsdataverse/sdvplot/shots\";\n *\n * // Brooklyn shots from the sportsdataverse-data nba_stats_shots release, 2025-26\n * const made = { x_legacy: 0, y_legacy: 0, shot_distance: 0, shot_value: 2, shot_result: \"Made\" };\n * const missed = { ...made, shot_result: \"Missed\" };\n * vsLeague(fgPctByDistance([made]), fgPctByDistance([made, missed]))[0];\n * ```\n */\nexport function vsLeague(player: readonly DistanceBin[], league: readonly DistanceBin[]): DistanceVsLeague[] {\n if (player.length !== league.length || player.some((b, i) => b.distance !== league[i]?.distance)) {\n throw new InputError(\"vsLeague: player and league bins differ (use the same binFt and maxFt)\");\n }\n return player.map((b, i) => {\n const leagueFgPct = league[i]?.fgPct ?? null;\n return {\n ...b,\n leagueFgPct,\n diff: b.fgPct !== null && leagueFgPct !== null ? b.fgPct - leagueFgPct : null,\n };\n });\n}\n\n/** One `statsBySide` bin: left, centre and right of the hoop for the shots in the bin that starts `distance` ft out. */\nexport interface SideBin {\n readonly distance: number;\n readonly left: Split;\n readonly centre: Split;\n readonly right: Split;\n}\n\n/**\n * Left / centre / right of the hoop by distance (aggregate.ts:218-240): left is `x < -centreHalfWidth`, right is\n * `x > centreHalfWidth`. The default 0 puts `x == 0` in centre (`main`); `false` drops it, as `master`'s\n * `binLeftRight` did (`src/utils/visuals/bin.ts:36-42`). `binFt` and `maxFt` as for `fgPctByDistance`; a\n * `centreHalfWidth` that is negative or not finite throws InputError.\n *\n * @example\n * ```ts\n * import { statsBySide } from \"@sportsdataverse/sdvplot/shots\";\n *\n * // Brooklyn shots from the sportsdataverse-data nba_stats_shots release, 2025-26\n * const shots = [\n * { x_legacy: -1, y_legacy: 7, shot_distance: 1, shot_value: 2, shot_result: \"Made\" },\n * { x_legacy: 0, y_legacy: 0, shot_distance: 0, shot_value: 2, shot_result: \"Missed\" },\n * { x_legacy: -44, y_legacy: 252, shot_distance: 26, shot_value: 3, shot_result: \"Made\" },\n * ];\n * statsBySide(shots, 3, 35, 7.5)[0];\n * ```\n */\nexport function statsBySide(\n shots: readonly ShotRow[],\n binFt = 1,\n maxFt = 35,\n centreHalfWidth: number | false = 0,\n): SideBin[] {\n if (centreHalfWidth !== false && !(Number.isFinite(centreHalfWidth) && centreHalfWidth >= 0))\n throw new InputError(\n `statsBySide centreHalfWidth must be a finite number >= 0 or false, got ${String(centreHalfWidth)}`,\n );\n const half = centreHalfWidth === false ? 0 : centreHalfWidth;\n const acc = Array.from({ length: binCount(binFt, maxFt) }, () => ({\n left: [0, 0],\n centre: [0, 0],\n right: [0, 0],\n }));\n for (const s of shots) {\n if (s.shot_distance > maxFt) continue;\n const bin = acc[Math.floor(s.shot_distance / binFt)];\n if (bin === undefined) continue;\n const x = s.x_legacy;\n const side =\n x < -half ? bin.left : x > half ? bin.right : centreHalfWidth === false ? undefined : bin.centre;\n if (side === undefined) continue;\n side[0] = (side[0] ?? 0) + 1;\n if (made(s)) side[1] = (side[1] ?? 0) + 1;\n }\n return acc.map((b, i) => ({\n distance: i * binFt,\n left: split(b.left[0] ?? 0, b.left[1] ?? 0),\n centre: split(b.centre[0] ?? 0, b.centre[1] ?? 0),\n right: split(b.right[0] ?? 0, b.right[1] ?? 0),\n }));\n}\n\n/**\n * Player cells against the league FG% at the cell centre's distance, `floor(|centre| / 10)` ft, falling back one\n * foot when that bin is empty (blazing-the-nets `master` `src/components/Hexagon/index.js:44-49`). `cell` is a\n * hexagon radius (default 10, master's) or any `binner` lattice, e.g. `{ shape: \"square\", side: 10 }`. `byFoot` is\n * read as feet, so it must be 1-ft bins from 0 (`fgPctByDistance(league)`), else InputError.\n *\n * The distance is `Math.hypot(x, y)`, a deliberate divergence from master's `Math.sqrt(x ** 2 + y ** 2)`\n * (`src/lib/distance.js:3-4`). On a hexagon centre exactly a whole number of feet out, such as (15√3, 15) at 3 ft,\n * master's sqrt returns 29.999…96 and reads the foot below; hypot reads the true foot. On the real BKN fixture at\n * radius 10 that is 13 of 374 cells, among them the cells beside the rim (0.572 at 3 ft, where master reads 0.669\n * at 2 ft). Every other cell reads master's foot.\n *\n * @example\n * ```ts\n * import { cellsVsDistance, fgPctByDistance } from \"@sportsdataverse/sdvplot/shots\";\n *\n * // Brooklyn shots from the sportsdataverse-data nba_stats_shots release, 2025-26\n * const made = { x_legacy: 0, y_legacy: 0, shot_distance: 0, shot_value: 2, shot_result: \"Made\" };\n * const missed = { ...made, shot_result: \"Missed\" };\n * cellsVsDistance([made], fgPctByDistance([made, missed]), { shape: \"square\", side: 10 });\n * ```\n */\nexport function cellsVsDistance(\n player: readonly ShotRow[],\n byFoot: readonly DistanceBin[],\n cell: number | BinnerOptions = 10,\n): CellVsLeague[] {\n const bad = byFoot.findIndex((b, i) => b.distance !== i);\n if (bad >= 0)\n throw new InputError(\n `cellsVsDistance needs 1-ft distance bins from 0 (fgPctByDistance(shots, 1)); bin ${bad} is at ${String(byFoot[bad]?.distance)} ft`,\n );\n return binShots(player, cell).map((h) => {\n // hypot, not master's sqrt(x ** 2 + y ** 2) (distance.js:3-4), which reads a whole-foot centre one foot short\n const d = Math.floor(Math.hypot(h.x, h.y) / 10);\n return { ...h, leagueFgPct: byFoot[d]?.fgPct ?? byFoot[d - 1]?.fgPct ?? null };\n });\n}\n"]}
|