@sportsdataverse/sdvplot 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/NOTICE.md +15 -0
  2. package/README.md +84 -8
  3. package/dist/chartjs-surface.d.ts +2 -2
  4. package/dist/chartjs-surface.js +2 -2
  5. package/dist/chartjs-surface.js.map +1 -1
  6. package/dist/chartjs.d.ts +32 -2
  7. package/dist/chartjs.js +7 -4
  8. package/dist/chartjs.js.map +1 -1
  9. package/dist/chunk-57WFSI4Q.js +128 -0
  10. package/dist/chunk-57WFSI4Q.js.map +1 -0
  11. package/dist/{chunk-KKH6ABMA.js → chunk-7ULOCCFL.js} +94 -4
  12. package/dist/chunk-7ULOCCFL.js.map +1 -0
  13. package/dist/chunk-AMXASQHT.js +120 -0
  14. package/dist/chunk-AMXASQHT.js.map +1 -0
  15. package/dist/chunk-B2MP37SR.js +175 -0
  16. package/dist/chunk-B2MP37SR.js.map +1 -0
  17. package/dist/{chunk-NLPH2VZT.js → chunk-DYMUVE6K.js} +4 -4
  18. package/dist/{chunk-NLPH2VZT.js.map → chunk-DYMUVE6K.js.map} +1 -1
  19. package/dist/{chunk-QY65DUEF.js → chunk-H2C3FMMA.js} +4 -10
  20. package/dist/chunk-H2C3FMMA.js.map +1 -0
  21. package/dist/{chunk-JJFTD4VB.js → chunk-J4P4HDBF.js} +4 -4
  22. package/dist/{chunk-JJFTD4VB.js.map → chunk-J4P4HDBF.js.map} +1 -1
  23. package/dist/chunk-M3XD2OSN.js +132 -0
  24. package/dist/chunk-M3XD2OSN.js.map +1 -0
  25. package/dist/{chunk-OKEE4AH6.js → chunk-OIILARXI.js} +3 -3
  26. package/dist/{chunk-OKEE4AH6.js.map → chunk-OIILARXI.js.map} +1 -1
  27. package/dist/chunk-RPVVJY74.js +170 -0
  28. package/dist/chunk-RPVVJY74.js.map +1 -0
  29. package/dist/chunk-UL5M6KDE.js +205 -0
  30. package/dist/chunk-UL5M6KDE.js.map +1 -0
  31. package/dist/chunk-WQT4IVLX.js +10 -0
  32. package/dist/chunk-WQT4IVLX.js.map +1 -0
  33. package/dist/{chunk-TAXO3VXH.js → chunk-ZAEQXFJM.js} +3 -3
  34. package/dist/{chunk-TAXO3VXH.js.map → chunk-ZAEQXFJM.js.map} +1 -1
  35. package/dist/cytoscape.d.ts +188 -0
  36. package/dist/cytoscape.js +219 -0
  37. package/dist/cytoscape.js.map +1 -0
  38. package/dist/d3.d.ts +3 -3
  39. package/dist/d3.js +6 -6
  40. package/dist/d3.js.map +1 -1
  41. package/dist/deckgl.d.ts +232 -0
  42. package/dist/deckgl.js +197 -0
  43. package/dist/deckgl.js.map +1 -0
  44. package/dist/echarts.js +5 -5
  45. package/dist/g2.d.ts +251 -0
  46. package/dist/g2.js +102 -0
  47. package/dist/g2.js.map +1 -0
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +4 -5
  50. package/dist/lightweight-charts.d.ts +217 -0
  51. package/dist/lightweight-charts.js +155 -0
  52. package/dist/lightweight-charts.js.map +1 -0
  53. package/dist/maplibre.d.ts +271 -0
  54. package/dist/maplibre.js +175 -0
  55. package/dist/maplibre.js.map +1 -0
  56. package/dist/mui.d.ts +338 -0
  57. package/dist/mui.js +184 -0
  58. package/dist/mui.js.map +1 -0
  59. package/dist/nivo.d.ts +193 -0
  60. package/dist/nivo.js +8 -0
  61. package/dist/nivo.js.map +1 -0
  62. package/dist/perspective.d.ts +198 -0
  63. package/dist/perspective.js +171 -0
  64. package/dist/perspective.js.map +1 -0
  65. package/dist/placement-UWkqUzpa.d.ts +32 -0
  66. package/dist/plot.d.ts +7 -5
  67. package/dist/plot.js +9 -8
  68. package/dist/plot.js.map +1 -1
  69. package/dist/plotly.js +5 -5
  70. package/dist/react.d.ts +279 -3
  71. package/dist/react.js +364 -6
  72. package/dist/react.js.map +1 -1
  73. package/dist/recharts.d.ts +162 -0
  74. package/dist/recharts.js +8 -0
  75. package/dist/recharts.js.map +1 -0
  76. package/dist/shadcn.d.ts +83 -0
  77. package/dist/shadcn.js +88 -0
  78. package/dist/shadcn.js.map +1 -0
  79. package/dist/shots.js +3 -2
  80. package/dist/shots.js.map +1 -1
  81. package/dist/surface-TR7YFJW3.js +4 -0
  82. package/dist/surface-TR7YFJW3.js.map +1 -0
  83. package/dist/surfaces-MIG7HOFU.js +6 -0
  84. package/dist/surfaces-MIG7HOFU.js.map +1 -0
  85. package/dist/surfaces-W-odWLpT.d.ts +47 -0
  86. package/dist/svelte/HeadshotMark.svelte +25 -0
  87. package/dist/svelte/HeadshotMark.svelte.d.ts +10 -0
  88. package/dist/svelte/HeadshotMark.svelte.d.ts.map +1 -0
  89. package/dist/svelte/LogoMark.svelte +25 -0
  90. package/dist/svelte/LogoMark.svelte.d.ts +10 -0
  91. package/dist/svelte/LogoMark.svelte.d.ts.map +1 -0
  92. package/dist/svelte/Marks.svelte +25 -0
  93. package/dist/svelte/Marks.svelte.d.ts +10 -0
  94. package/dist/svelte/Marks.svelte.d.ts.map +1 -0
  95. package/dist/svelte/Surface.svelte +29 -0
  96. package/dist/svelte/Surface.svelte.d.ts +9 -0
  97. package/dist/svelte/Surface.svelte.d.ts.map +1 -0
  98. package/dist/svelte/SurfaceBackdrop.svelte +26 -0
  99. package/dist/svelte/SurfaceBackdrop.svelte.d.ts +11 -0
  100. package/dist/svelte/SurfaceBackdrop.svelte.d.ts.map +1 -0
  101. package/dist/svelte/index.d.ts +13 -0
  102. package/dist/svelte/index.d.ts.map +1 -0
  103. package/dist/svelte/index.js +11 -0
  104. package/dist/svelte-core.d.ts +139 -0
  105. package/dist/svelte-core.js +123 -0
  106. package/dist/svelte-core.js.map +1 -0
  107. package/dist/tanstack.d.ts +240 -0
  108. package/dist/tanstack.js +110 -0
  109. package/dist/tanstack.js.map +1 -0
  110. package/dist/testing.js +4 -5
  111. package/dist/testing.js.map +1 -1
  112. package/dist/three.d.ts +223 -0
  113. package/dist/three.js +50 -0
  114. package/dist/three.js.map +1 -0
  115. package/dist/{tiers-B6pUBPcE.d.ts → tiers-BN4md-GB.d.ts} +2 -30
  116. package/dist/unovis.d.ts +127 -0
  117. package/dist/unovis.js +108 -0
  118. package/dist/unovis.js.map +1 -0
  119. package/dist/uplot.d.ts +120 -0
  120. package/dist/uplot.js +110 -0
  121. package/dist/uplot.js.map +1 -0
  122. package/dist/vega.js +5 -5
  123. package/dist/victory.d.ts +107 -0
  124. package/dist/victory.js +38 -0
  125. package/dist/victory.js.map +1 -0
  126. package/package.json +218 -6
  127. package/dist/chunk-KKH6ABMA.js.map +0 -1
  128. package/dist/chunk-QY65DUEF.js.map +0 -1
  129. package/dist/chunk-RMT2GMFH.js +0 -97
  130. package/dist/chunk-RMT2GMFH.js.map +0 -1
  131. package/dist/chunk-SEBO35DV.js +0 -76
  132. package/dist/chunk-SEBO35DV.js.map +0 -1
  133. package/dist/chunk-TDNBAEQB.js +0 -105
  134. package/dist/chunk-TDNBAEQB.js.map +0 -1
  135. package/dist/surfaces-ChaD9nei.d.ts +0 -38
@@ -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"]}
@@ -1,33 +1,5 @@
1
- import { L as League, S as SeasonInput, V as Variant, I as IdSystem, H as HeadshotIdSystem, a as MarkRow, T as TeamId } from './types-C7n6gJF3.js';
2
1
  import { V as Value } from './resolve-CQW-jzvr.js';
3
-
4
- type Kind = "logo" | "wordmark" | "headshot";
5
- /** One point to draw: the caller's own x/y, the image URL and its width / height ratio (null when unknown). `index` is the row's position in the input arrays (skipped rows leave gaps). */
6
- interface Placement {
7
- index: number;
8
- id: string;
9
- x: Value;
10
- y: Value;
11
- url: string;
12
- aspect: number | null;
13
- mark: MarkRow | null;
14
- }
15
- interface PlaceOptions {
16
- league: League;
17
- season?: SeasonInput | readonly SeasonInput[];
18
- kind?: Kind;
19
- variant?: Variant;
20
- /** Team id system for logos/wordmarks (default "auto"); "espn" or "gsis" for headshots (default "espn"). */
21
- idSystem?: IdSystem | HeadshotIdSystem;
22
- strict?: boolean;
23
- warn?: boolean;
24
- }
25
-
26
- declare function checkHeight(height: unknown): number;
27
- declare function checkAlpha(alpha: unknown): number;
28
- /** Port of `_placement.place` (post-preload): pairs each (x, y) with its team's mark or player's headshot, dropping and warning (once per reason) on rows that cannot be drawn. Needs `loadLeague(league)` (or `preloadAll()`) first; gsis headshots also need `loadGsis()`. */
29
- declare function placeSync(xs: readonly Value[], ys: readonly Value[], teams: readonly Value[], o: PlaceOptions): Placement[];
30
- declare function place(xs: readonly Value[], ys: readonly Value[], teams: readonly Value[], o: PlaceOptions): Promise<Placement[]>;
2
+ import { T as TeamId, V as Variant, L as League } from './types-C7n6gJF3.js';
31
3
 
32
4
  declare const TIER_DESC: Readonly<Record<number, string>>;
33
5
  declare const TIERS_SUBTITLE = "created with the #sdvplot Tiermaker";
@@ -90,4 +62,4 @@ declare function wrapLabel(text: string): string;
90
62
  /** Port of `sdvplot._tiers.prepare`: validate the rows and compute everything a tier plot draws. */
91
63
  declare function prepareTiers(rows: readonly TierRow[], league: League, o?: TiersOptions): Tiers;
92
64
 
93
- export { type Kind as K, type Placement as P, type TiersOptions as T, type TierRow as a, type PlaceOptions as b, TIERS_SUBTITLE as c, TIER_DESC as d, TIER_THEMES as e, type Tiers as f, checkAlpha as g, checkHeight as h, placeSync as i, prepareTiers as j, place as p, wrapLabel as w };
65
+ export { type TiersOptions as T, type TierRow as a, TIERS_SUBTITLE as b, TIER_DESC as c, TIER_THEMES as d, type Tiers as e, prepareTiers as p, wrapLabel as w };
@@ -0,0 +1,127 @@
1
+ import { E as EspnHeadshotLeague } from './headshots-B2J_LOO8.js';
2
+ import { W as Which, S as SeasonInput, I as IdSystem, H as HeadshotIdSystem, V as Variant, L as League } from './types-C7n6gJF3.js';
3
+ import './resolve-CQW-jzvr.js';
4
+
5
+ /** `teamColorPalette` / `teamColorScale` options. */
6
+ interface UnovisColorOptions {
7
+ /** "primary" (default) or "secondary" colour. */
8
+ which?: Which;
9
+ /** The season the teams are resolved for (a number or string). */
10
+ season?: SeasonInput;
11
+ /** How the team values are read. Default "auto". */
12
+ idSystem?: IdSystem;
13
+ /** The colour of an unresolved team. Default "#808080". */
14
+ fallback?: string;
15
+ /**
16
+ * The chart's background. Given, a team colour under 3:1 contrast against it is swapped for the team's other
17
+ * colour, else moved towards white (on a dark background) or black (a light one) until it reads: dark mode. A
18
+ * colour that would then sit too close to an earlier team's (CIE76 ΔE under 20) takes the team's other colour, or
19
+ * the nearest colour to its own that is distinct and still reads; failing that it is kept, with one warning.
20
+ */
21
+ background?: string;
22
+ }
23
+ /**
24
+ * One colour per team, in order: an XY component's `color` (one per `y` accessor), or anything else that takes
25
+ * colours by position. With `background`, every colour keeps 3:1 contrast against it (dark mode) and aims to stay
26
+ * distinct from the others. `teamColorScale` gives the same colours keyed by team, for accessors.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * import { loadLeague } from "@sportsdataverse/sdvplot";
31
+ * import { teamColorPalette } from "@sportsdataverse/sdvplot/unovis";
32
+ *
33
+ * await loadLeague("nfl");
34
+ * // new Line({ x, y: [kc, phi], color: <this> })
35
+ * teamColorPalette("nfl", ["KC", "PHI"], { background: "#111827" });
36
+ * ```
37
+ */
38
+ declare function teamColorPalette(league: League, teams: readonly unknown[], o?: UnovisColorOptions): string[];
39
+ /** A team's colour: call it from a Unovis colour accessor with the team value the datum holds. */
40
+ interface UnovisTeamColorScale<T = unknown> {
41
+ /** The colour of `team`: its own for a team of the domain (by any value naming it), else `fallback`. */
42
+ (team: unknown): string;
43
+ /** The team values exactly as given (numbers stay numbers), each once and in order. */
44
+ readonly domain: readonly T[];
45
+ /** Their colours, in the same order. */
46
+ readonly range: readonly string[];
47
+ }
48
+ /**
49
+ * Team colours for Unovis's colour accessors: `color(d.team)` from a Graph's `nodeFill`, `nodeStroke` or
50
+ * `linkStroke`, a Sankey's `nodeColor` or `linkColor`, a Chord's `nodeColor`. Give every team the chart draws, so
51
+ * the colours are worked out together: with `background` they read on it (dark mode) and stay distinct. The domain
52
+ * keeps the values as the data holds them (12 and "12" are both kept); the colours are the resolved team's, so 12,
53
+ * "12" and "KC" draw in one colour. A value outside the domain gets `fallback`, with one warning.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * import { loadLeague } from "@sportsdataverse/sdvplot";
58
+ * import { teamColorScale } from "@sportsdataverse/sdvplot/unovis";
59
+ *
60
+ * await loadLeague("nfl");
61
+ * const color = teamColorScale("nfl", ["KC", "PHI"]);
62
+ * // new Sankey({ linkColor: (l) => color(l.team), ... })
63
+ * [color("KC"), color(21), color.range];
64
+ * ```
65
+ */
66
+ declare function teamColorScale<T>(league: League, teams: readonly T[], o?: UnovisColorOptions): UnovisTeamColorScale<T>;
67
+ /** `logoIcons` options. */
68
+ interface UnovisLogoOptions {
69
+ /** The season the teams are resolved for (a number or string). */
70
+ season?: SeasonInput;
71
+ /** Logo variant, e.g. "dark". Default "default". */
72
+ variant?: Variant;
73
+ /** How the team values are read. Default "auto". */
74
+ idSystem?: IdSystem;
75
+ }
76
+ /** `headshotIcons` options. */
77
+ interface UnovisHeadshotOptions {
78
+ /** ESPN athlete ids, or nflverse gsis ids (needs `loadGsis()` first). Default "espn". */
79
+ idSystem?: HeadshotIdSystem;
80
+ }
81
+ /** Images for a Graph's node icons: call it from `nodeIcon` with the team (or player) the node holds. */
82
+ interface UnovisIcons<T = unknown> {
83
+ /** `#<symbol id>` of the image of `value` (by any value naming the same team or player), else undefined. */
84
+ (value: unknown): string | undefined;
85
+ /** The values that have an image, as given (numbers stay numbers), each once and in order. */
86
+ readonly domain: readonly T[];
87
+ /** Their `#<symbol id>`s, in the same order. */
88
+ readonly range: readonly string[];
89
+ /** One `<symbol>` per image: the chart container's `svgDefs` (join two sets' with `+`). */
90
+ readonly svgDefs: string;
91
+ }
92
+ /**
93
+ * Team logos for a Unovis Graph's nodes: `nodeIcon: (n) => logos(n.team)`, with
94
+ * `svgDefs: logos.svgDefs` on the container. Unovis draws each icon as a `<use>` of the logo's `<symbol>`, `nodeIconSize` px across, the logo
95
+ * fitted inside, uncut; a node's fill shows round it. A team with no logo has no icon, with one warning. Every value
96
+ * naming a team finds its logo (12, "12" and "KC").
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * import { loadLeague } from "@sportsdataverse/sdvplot";
101
+ * import { logoIcons } from "@sportsdataverse/sdvplot/unovis";
102
+ *
103
+ * await loadLeague("nfl");
104
+ * const logos = logoIcons("nfl", ["KC", "LV"], { variant: "dark" });
105
+ * // new SingleContainer(el, { component: new Graph({ nodeIcon: (n) => logos(n.team), nodeIconSize: 28 }),
106
+ * // svgDefs: logos.svgDefs }, data)
107
+ * [logos("KC"), logos.range];
108
+ * ```
109
+ */
110
+ declare function logoIcons<T>(league: League, teams: readonly T[], o?: UnovisLogoOptions): UnovisIcons<T>;
111
+ /**
112
+ * ESPN player headshots for a Unovis Graph's nodes, as `logoIcons`: `nodeIcon: (n) => heads(n.player)`, with the
113
+ * container's `svgDefs: heads.svgDefs`. A gsis id with no headshot has no icon, with one warning; an ESPN id's URL is
114
+ * built without checking the image exists, so an id ESPN has no photo for keeps an empty icon, with no warning.
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * import { headshotIcons } from "@sportsdataverse/sdvplot/unovis";
119
+ *
120
+ * // Patrick Mahomes and Travis Kelce, by ESPN athlete id
121
+ * const heads = headshotIcons("nfl", [3139477, 15847]);
122
+ * [heads(3139477), heads.svgDefs.length > 0];
123
+ * ```
124
+ */
125
+ declare function headshotIcons<T>(league: EspnHeadshotLeague, players: readonly T[], o?: UnovisHeadshotOptions): UnovisIcons<T>;
126
+
127
+ export { type UnovisColorOptions, type UnovisHeadshotOptions, type UnovisIcons, type UnovisLogoOptions, type UnovisTeamColorScale, headshotIcons, logoIcons, teamColorPalette, teamColorScale };
package/dist/unovis.js ADDED
@@ -0,0 +1,108 @@
1
+ import { contentId } from './chunk-WQT4IVLX.js';
2
+ import { colorPalette, imageSource, aspect } from './chunk-UL5M6KDE.js';
3
+ import { placeSync } from './chunk-7ULOCCFL.js';
4
+ import { resolveSync, normValue } from './chunk-ZAEQXFJM.js';
5
+ import './chunk-J6R2LEJ7.js';
6
+ import { warn } from './chunk-E7G7VSLV.js';
7
+
8
+ // src/unovis.ts
9
+ function teamColorPalette(league, teams, o = {}) {
10
+ return colorPalette(league, teams, o);
11
+ }
12
+ var keyOf = (v) => `${typeof v}:${String(v)}`;
13
+ function distinct(values) {
14
+ const seen = /* @__PURE__ */ new Map();
15
+ for (const v of values) if (!seen.has(keyOf(v))) seen.set(keyOf(v), v);
16
+ return [...seen.values()];
17
+ }
18
+ var shown = (v) => typeof v === "string" ? JSON.stringify(v) : String(v);
19
+ function teamColorScale(league, teams, o = {}) {
20
+ const domain = distinct(teams);
21
+ const ro = {
22
+ ...o.season === void 0 ? {} : { season: o.season },
23
+ ...o.idSystem === void 0 ? {} : { idSystem: o.idSystem }
24
+ };
25
+ const fallback = o.fallback ?? "#808080";
26
+ const ids = resolveSync(domain, league, ro);
27
+ const known = [...new Set(ids.filter((id) => id !== void 0))];
28
+ const palette = colorPalette(league, known, {
29
+ ...ro,
30
+ idSystem: "team_id",
31
+ ...o.which === void 0 ? {} : { which: o.which },
32
+ ...o.background === void 0 ? {} : { background: o.background },
33
+ fallback
34
+ });
35
+ const byTeam = new Map(known.map((id, j) => [id, palette[j]]));
36
+ const memo = new Map(domain.map((t, i) => [keyOf(t), byTeam.get(ids[i]) ?? fallback]));
37
+ const scale = (team) => {
38
+ const k = keyOf(team);
39
+ let c = memo.get(k);
40
+ if (c === void 0) {
41
+ const id = resolveSync(team, league, ro);
42
+ c = id === void 0 ? void 0 : byTeam.get(id);
43
+ if (c === void 0)
44
+ warn(
45
+ `unovis:${league}:color:${k}`,
46
+ `teamColorScale: ${shown(team)} is not one of its teams; drawn in ${fallback}`
47
+ );
48
+ c ??= fallback;
49
+ memo.set(k, c);
50
+ }
51
+ return c;
52
+ };
53
+ return Object.assign(scale, { domain, range: domain.map((t) => memo.get(keyOf(t))) });
54
+ }
55
+ var escapeAttr = (s) => s.replace(/[&<>"]/g, (c) => `&#${c.charCodeAt(0)};`);
56
+ var n = (v) => String(Math.round(v * 1e4) / 1e4);
57
+ function icons(kind, league, values, o) {
58
+ const given = distinct(values);
59
+ const at = given.map(() => 0);
60
+ const placed = placeSync(at, at, given, {
61
+ league,
62
+ kind,
63
+ ...o.season === void 0 ? {} : { season: o.season },
64
+ ...o.variant === void 0 ? {} : { variant: o.variant },
65
+ ...o.idSystem === void 0 ? {} : { idSystem: o.idSystem }
66
+ });
67
+ const idOf = (v) => kind === "headshot" ? o.idSystem === "gsis" ? String(v).trim() : normValue(v) ?? void 0 : resolveSync(v, league, {
68
+ ...o.season === void 0 ? {} : { season: o.season },
69
+ ...o.idSystem === void 0 ? {} : { idSystem: o.idSystem }
70
+ });
71
+ const byId = /* @__PURE__ */ new Map();
72
+ let svgDefs = "";
73
+ for (const p of placed) {
74
+ if (byId.has(p.id)) continue;
75
+ const href = imageSource(p);
76
+ const id = contentId(`sdv-${kind}`, href);
77
+ const w = 100 * aspect(p);
78
+ if (!svgDefs.includes(`id="${id}"`))
79
+ svgDefs += `<symbol id="${id}" viewBox="0 0 ${n(w)} 100" preserveAspectRatio="xMidYMid meet"><image href="${escapeAttr(href)}" width="${n(w)}" height="100" preserveAspectRatio="xMidYMid meet"/></symbol>`;
80
+ byId.set(p.id, `#${id}`);
81
+ }
82
+ const memo = new Map(
83
+ placed.map((p) => [keyOf(given[p.index]), byId.get(p.id)])
84
+ );
85
+ const icon = (value) => {
86
+ const k = keyOf(value);
87
+ if (!memo.has(k)) {
88
+ const id = value === null || value === void 0 ? void 0 : idOf(value);
89
+ memo.set(k, id === void 0 ? void 0 : byId.get(id));
90
+ }
91
+ return memo.get(k);
92
+ };
93
+ return Object.assign(icon, {
94
+ domain: placed.map((p) => given[p.index]),
95
+ range: placed.map((p) => byId.get(p.id)),
96
+ svgDefs
97
+ });
98
+ }
99
+ function logoIcons(league, teams, o = {}) {
100
+ return icons("logo", league, teams, o);
101
+ }
102
+ function headshotIcons(league, players, o = {}) {
103
+ return icons("headshot", league, players, o);
104
+ }
105
+
106
+ export { headshotIcons, logoIcons, teamColorPalette, teamColorScale };
107
+ //# sourceMappingURL=unovis.js.map
108
+ //# sourceMappingURL=unovis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/unovis.ts"],"names":[],"mappings":"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","file":"unovis.js","sourcesContent":["/**\n * The Unovis 1.7 adapter: accessor helpers in Unovis's own idiom, for `@unovis/ts` (plain JavaScript, Svelte, any\n * framework) and `@unovis/react`. Team colours as a function a colour accessor calls (`teamColorScale`: a Graph's\n * `nodeFill` / `nodeStroke` / `linkStroke`, a Sankey's `nodeColor` / `linkColor`, a Chord's `nodeColor`), or as a list\n * (`teamColorPalette`, for an XY component's `color`). Logos and headshots for a Graph's nodes (`logoIcons`,\n * `headshotIcons`): Unovis draws a node icon that starts with `#` as an SVG `<use>` of that id, and the icons' `svgDefs`\n * (for the container's `svgDefs`) hold one `<symbol>` per image. A Sankey's node icon is text only, so a Sankey takes\n * team colours, not logos. Pure: `@unovis/*` is never imported. Call after `loadLeague(league)` / `preloadAll()`.\n */\nimport { aspect, colorPalette, imageSource } from \"./_web.js\";\nimport { contentId } from \"./content-id.js\";\nimport { warn } from \"./errors.js\";\nimport type { EspnHeadshotLeague } from \"./headshots.js\";\nimport { normValue } from \"./normalize.js\";\nimport { type Kind, placeSync } from \"./placement.js\";\nimport { type Value, resolveSync } from \"./resolve.js\";\nimport type { HeadshotIdSystem, IdSystem, League, SeasonInput, Variant, Which } from \"./types.js\";\n\n/** `teamColorPalette` / `teamColorScale` options. */\nexport interface UnovisColorOptions {\n /** \"primary\" (default) or \"secondary\" colour. */\n which?: Which;\n /** The season the teams are resolved for (a number or string). */\n season?: SeasonInput;\n /** How the team values are read. Default \"auto\". */\n idSystem?: IdSystem;\n /** The colour of an unresolved team. Default \"#808080\". */\n fallback?: string;\n /**\n * The chart's background. Given, a team colour under 3:1 contrast against it is swapped for the team's other\n * colour, else moved towards white (on a dark background) or black (a light one) until it reads: dark mode. A\n * colour that would then sit too close to an earlier team's (CIE76 ΔE under 20) takes the team's other colour, or\n * the nearest colour to its own that is distinct and still reads; failing that it is kept, with one warning.\n */\n background?: string;\n}\n\n/**\n * One colour per team, in order: an XY component's `color` (one per `y` accessor), or anything else that takes\n * colours by position. With `background`, every colour keeps 3:1 contrast against it (dark mode) and aims to stay\n * distinct from the others. `teamColorScale` gives the same colours keyed by team, for accessors.\n *\n * @example\n * ```ts\n * import { loadLeague } from \"@sportsdataverse/sdvplot\";\n * import { teamColorPalette } from \"@sportsdataverse/sdvplot/unovis\";\n *\n * await loadLeague(\"nfl\");\n * // new Line({ x, y: [kc, phi], color: <this> })\n * teamColorPalette(\"nfl\", [\"KC\", \"PHI\"], { background: \"#111827\" });\n * ```\n */\nexport function teamColorPalette(\n league: League,\n teams: readonly unknown[],\n o: UnovisColorOptions = {},\n): string[] {\n return colorPalette(league, teams, o);\n}\n\n/** A value's key as the data holds it: 12 and \"12\" are two keys, each kept. */\nconst keyOf = (v: unknown): string => `${typeof v}:${String(v)}`;\nfunction distinct<T>(values: readonly T[]): T[] {\n const seen = new Map<string, T>();\n for (const v of values) if (!seen.has(keyOf(v))) seen.set(keyOf(v), v);\n return [...seen.values()];\n}\nconst shown = (v: unknown): string => (typeof v === \"string\" ? JSON.stringify(v) : String(v));\n\n/** A team's colour: call it from a Unovis colour accessor with the team value the datum holds. */\nexport interface UnovisTeamColorScale<T = unknown> {\n /** The colour of `team`: its own for a team of the domain (by any value naming it), else `fallback`. */\n (team: unknown): string;\n /** The team values exactly as given (numbers stay numbers), each once and in order. */\n readonly domain: readonly T[];\n /** Their colours, in the same order. */\n readonly range: readonly string[];\n}\n/**\n * Team colours for Unovis's colour accessors: `color(d.team)` from a Graph's `nodeFill`, `nodeStroke` or\n * `linkStroke`, a Sankey's `nodeColor` or `linkColor`, a Chord's `nodeColor`. Give every team the chart draws, so\n * the colours are worked out together: with `background` they read on it (dark mode) and stay distinct. The domain\n * keeps the values as the data holds them (12 and \"12\" are both kept); the colours are the resolved team's, so 12,\n * \"12\" and \"KC\" draw in one colour. A value outside the domain gets `fallback`, with one warning.\n *\n * @example\n * ```ts\n * import { loadLeague } from \"@sportsdataverse/sdvplot\";\n * import { teamColorScale } from \"@sportsdataverse/sdvplot/unovis\";\n *\n * await loadLeague(\"nfl\");\n * const color = teamColorScale(\"nfl\", [\"KC\", \"PHI\"]);\n * // new Sankey({ linkColor: (l) => color(l.team), ... })\n * [color(\"KC\"), color(21), color.range];\n * ```\n */\nexport function teamColorScale<T>(\n league: League,\n teams: readonly T[],\n o: UnovisColorOptions = {},\n): UnovisTeamColorScale<T> {\n const domain = distinct(teams);\n const ro = {\n ...(o.season === undefined ? {} : { season: o.season }),\n ...(o.idSystem === undefined ? {} : { idSystem: o.idSystem }),\n };\n const fallback = o.fallback ?? \"#808080\";\n const ids = resolveSync(domain as readonly Value[], league, ro);\n // keyed by the resolved team: a team named twice (12 and \"KC\") is one team, with one colour\n const known = [...new Set(ids.filter((id) => id !== undefined))];\n const palette = colorPalette(league, known, {\n ...ro,\n idSystem: \"team_id\",\n ...(o.which === undefined ? {} : { which: o.which }),\n ...(o.background === undefined ? {} : { background: o.background }),\n fallback,\n });\n const byTeam = new Map<string, string>(known.map((id, j) => [id, palette[j] as string]));\n const memo = new Map(domain.map((t, i) => [keyOf(t), byTeam.get(ids[i] as string) ?? fallback]));\n const scale = (team: unknown): string => {\n const k = keyOf(team);\n let c = memo.get(k);\n if (c === undefined) {\n const id = resolveSync(team as Value, league, ro);\n c = id === undefined ? undefined : byTeam.get(id);\n if (c === undefined)\n warn(\n `unovis:${league}:color:${k}`,\n `teamColorScale: ${shown(team)} is not one of its teams; drawn in ${fallback}`,\n );\n c ??= fallback;\n memo.set(k, c);\n }\n return c;\n };\n return Object.assign(scale, { domain, range: domain.map((t) => memo.get(keyOf(t)) as string) });\n}\n\n/** `logoIcons` options. */\nexport interface UnovisLogoOptions {\n /** The season the teams are resolved for (a number or string). */\n season?: SeasonInput;\n /** Logo variant, e.g. \"dark\". Default \"default\". */\n variant?: Variant;\n /** How the team values are read. Default \"auto\". */\n idSystem?: IdSystem;\n}\n/** `headshotIcons` options. */\nexport interface UnovisHeadshotOptions {\n /** ESPN athlete ids, or nflverse gsis ids (needs `loadGsis()` first). Default \"espn\". */\n idSystem?: HeadshotIdSystem;\n}\n/** Images for a Graph's node icons: call it from `nodeIcon` with the team (or player) the node holds. */\nexport interface UnovisIcons<T = unknown> {\n /** `#<symbol id>` of the image of `value` (by any value naming the same team or player), else undefined. */\n (value: unknown): string | undefined;\n /** The values that have an image, as given (numbers stay numbers), each once and in order. */\n readonly domain: readonly T[];\n /** Their `#<symbol id>`s, in the same order. */\n readonly range: readonly string[];\n /** One `<symbol>` per image: the chart container's `svgDefs` (join two sets' with `+`). */\n readonly svgDefs: string;\n}\n\nconst escapeAttr = (s: string): string => s.replace(/[&<>\"]/g, (c) => `&#${c.charCodeAt(0)};`);\nconst n = (v: number): string => String(Math.round(v * 1e4) / 1e4);\n\nfunction icons<T>(\n kind: Kind,\n league: League,\n values: readonly T[],\n o: { season?: SeasonInput; variant?: Variant; idSystem?: IdSystem | HeadshotIdSystem },\n): UnovisIcons<T> {\n const given = distinct(values);\n const at = given.map(() => 0); // a node's place is Unovis's layout: placeSync only pairs each value with its image\n const placed = placeSync(at, at, given as readonly Value[], {\n league,\n kind,\n ...(o.season === undefined ? {} : { season: o.season }),\n ...(o.variant === undefined ? {} : { variant: o.variant }),\n ...(o.idSystem === undefined ? {} : { idSystem: o.idSystem }),\n });\n // a node's value -> the id its image is keyed by: the resolved team, or the player id as placeSync normalises it\n const idOf = (v: unknown): string | undefined =>\n kind === \"headshot\"\n ? o.idSystem === \"gsis\"\n ? String(v).trim()\n : (normValue(v) ?? undefined)\n : resolveSync(v as Value, league, {\n ...(o.season === undefined ? {} : { season: o.season }),\n ...(o.idSystem === undefined ? {} : { idSystem: o.idSystem as IdSystem }),\n });\n const byId = new Map<string, string>();\n let svgDefs = \"\";\n for (const p of placed) {\n if (byId.has(p.id)) continue;\n const href = imageSource(p);\n const id = contentId(`sdv-${kind}`, href);\n // the symbol's box is the image's own aspect: a `<use>` of it fits the image, uncut, in nodeIconSize\n const w = 100 * aspect(p);\n if (!svgDefs.includes(`id=\"${id}\"`))\n svgDefs += `<symbol id=\"${id}\" viewBox=\"0 0 ${n(w)} 100\" preserveAspectRatio=\"xMidYMid meet\"><image href=\"${escapeAttr(href)}\" width=\"${n(w)}\" height=\"100\" preserveAspectRatio=\"xMidYMid meet\"/></symbol>`;\n byId.set(p.id, `#${id}`);\n }\n const memo = new Map<string, string | undefined>(\n placed.map((p) => [keyOf(given[p.index]), byId.get(p.id)]),\n );\n const icon = (value: unknown): string | undefined => {\n const k = keyOf(value);\n if (!memo.has(k)) {\n const id = value === null || value === undefined ? undefined : idOf(value);\n memo.set(k, id === undefined ? undefined : byId.get(id));\n }\n return memo.get(k);\n };\n return Object.assign(icon, {\n domain: placed.map((p) => given[p.index] as T),\n range: placed.map((p) => byId.get(p.id) as string),\n svgDefs,\n });\n}\n\n/**\n * Team logos for a Unovis Graph's nodes: `nodeIcon: (n) => logos(n.team)`, with\n * `svgDefs: logos.svgDefs` on the container. Unovis draws each icon as a `<use>` of the logo's `<symbol>`, `nodeIconSize` px across, the logo\n * fitted inside, uncut; a node's fill shows round it. A team with no logo has no icon, with one warning. Every value\n * naming a team finds its logo (12, \"12\" and \"KC\").\n *\n * @example\n * ```ts\n * import { loadLeague } from \"@sportsdataverse/sdvplot\";\n * import { logoIcons } from \"@sportsdataverse/sdvplot/unovis\";\n *\n * await loadLeague(\"nfl\");\n * const logos = logoIcons(\"nfl\", [\"KC\", \"LV\"], { variant: \"dark\" });\n * // new SingleContainer(el, { component: new Graph({ nodeIcon: (n) => logos(n.team), nodeIconSize: 28 }),\n * // svgDefs: logos.svgDefs }, data)\n * [logos(\"KC\"), logos.range];\n * ```\n */\nexport function logoIcons<T>(league: League, teams: readonly T[], o: UnovisLogoOptions = {}): UnovisIcons<T> {\n return icons(\"logo\", league, teams, o);\n}\n\n/**\n * ESPN player headshots for a Unovis Graph's nodes, as `logoIcons`: `nodeIcon: (n) => heads(n.player)`, with the\n * container's `svgDefs: heads.svgDefs`. A gsis id with no headshot has no icon, with one warning; an ESPN id's URL is\n * built without checking the image exists, so an id ESPN has no photo for keeps an empty icon, with no warning.\n *\n * @example\n * ```ts\n * import { headshotIcons } from \"@sportsdataverse/sdvplot/unovis\";\n *\n * // Patrick Mahomes and Travis Kelce, by ESPN athlete id\n * const heads = headshotIcons(\"nfl\", [3139477, 15847]);\n * [heads(3139477), heads.svgDefs.length > 0];\n * ```\n */\nexport function headshotIcons<T>(\n league: EspnHeadshotLeague,\n players: readonly T[],\n o: UnovisHeadshotOptions = {},\n): UnovisIcons<T> {\n return icons(\"headshot\", league, players, o);\n}\n"]}