@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.
Files changed (166) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +94 -0
  3. package/README.md +908 -2
  4. package/dist/_web-D7Aa5Rd8.d.ts +69 -0
  5. package/dist/aaf-GJ3SH34T.js +13 -0
  6. package/dist/aaf-HCYN5T6Y.js +37 -0
  7. package/dist/aaf-ZAE5KS2Y.js +13 -0
  8. package/dist/ahl-E66MDVCU.js +66 -0
  9. package/dist/ahl-NPQQWZEV.js +249 -0
  10. package/dist/ahl-QRDU73XX.js +309 -0
  11. package/dist/axis-CraWQRUJ.d.ts +49 -0
  12. package/dist/bins.d.ts +174 -0
  13. package/dist/bins.js +4 -0
  14. package/dist/bins.js.map +1 -0
  15. package/dist/cfb-F42SVS4X.js +5000 -0
  16. package/dist/cfb-P3SHEYRZ.js +692 -0
  17. package/dist/cfb-WXRZANLP.js +10226 -0
  18. package/dist/chartjs-surface.d.ts +42 -0
  19. package/dist/chartjs-surface.js +65 -0
  20. package/dist/chartjs-surface.js.map +1 -0
  21. package/dist/chartjs.d.ts +132 -0
  22. package/dist/chartjs.js +339 -0
  23. package/dist/chartjs.js.map +1 -0
  24. package/dist/chunk-4C3GX6QO.js +92 -0
  25. package/dist/chunk-4C3GX6QO.js.map +1 -0
  26. package/dist/chunk-C2CDY5OG.js +137 -0
  27. package/dist/chunk-C2CDY5OG.js.map +1 -0
  28. package/dist/chunk-E7G7VSLV.js +43 -0
  29. package/dist/chunk-E7G7VSLV.js.map +1 -0
  30. package/dist/chunk-J6R2LEJ7.js +58 -0
  31. package/dist/chunk-J6R2LEJ7.js.map +1 -0
  32. package/dist/chunk-JJFTD4VB.js +175 -0
  33. package/dist/chunk-JJFTD4VB.js.map +1 -0
  34. package/dist/chunk-KKH6ABMA.js +223 -0
  35. package/dist/chunk-KKH6ABMA.js.map +1 -0
  36. package/dist/chunk-NLPH2VZT.js +127 -0
  37. package/dist/chunk-NLPH2VZT.js.map +1 -0
  38. package/dist/chunk-OKEE4AH6.js +30 -0
  39. package/dist/chunk-OKEE4AH6.js.map +1 -0
  40. package/dist/chunk-QY65DUEF.js +306 -0
  41. package/dist/chunk-QY65DUEF.js.map +1 -0
  42. package/dist/chunk-RMT2GMFH.js +97 -0
  43. package/dist/chunk-RMT2GMFH.js.map +1 -0
  44. package/dist/chunk-SEBO35DV.js +76 -0
  45. package/dist/chunk-SEBO35DV.js.map +1 -0
  46. package/dist/chunk-TAXO3VXH.js +556 -0
  47. package/dist/chunk-TAXO3VXH.js.map +1 -0
  48. package/dist/chunk-TDNBAEQB.js +105 -0
  49. package/dist/chunk-TDNBAEQB.js.map +1 -0
  50. package/dist/cricket-KPCYG2HL.js +493 -0
  51. package/dist/cricket-NDXLE25B.js +116 -0
  52. package/dist/cricket-YUUF7P6M.js +160 -0
  53. package/dist/d3.d.ts +234 -0
  54. package/dist/d3.js +198 -0
  55. package/dist/d3.js.map +1 -0
  56. package/dist/echarts.d.ts +121 -0
  57. package/dist/echarts.js +228 -0
  58. package/dist/echarts.js.map +1 -0
  59. package/dist/echl-3CR4RGFC.js +241 -0
  60. package/dist/echl-NPC32C23.js +107 -0
  61. package/dist/echl-PI33DGJC.js +64 -0
  62. package/dist/export.d.ts +130 -0
  63. package/dist/export.js +258 -0
  64. package/dist/export.js.map +1 -0
  65. package/dist/headshots-B2J_LOO8.d.ts +40 -0
  66. package/dist/index.d.ts +182 -0
  67. package/dist/index.js +10 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/interact.d.ts +567 -0
  70. package/dist/interact.js +809 -0
  71. package/dist/interact.js.map +1 -0
  72. package/dist/marks-DH-tQIP-.d.ts +75 -0
  73. package/dist/mbb-3TRLVW3H.js +3619 -0
  74. package/dist/mbb-DQUHCHZP.js +5111 -0
  75. package/dist/mbb-PRAIO2P5.js +371 -0
  76. package/dist/milb-6IQ6NCEW.js +291 -0
  77. package/dist/milb-EYY5MD4T.js +1752 -0
  78. package/dist/milb-EYZ4SZLI.js +797 -0
  79. package/dist/mlb-4PBUJSVQ.js +35 -0
  80. package/dist/mlb-FHPY3GQJ.js +701 -0
  81. package/dist/mlb-REVGL3N2.js +665 -0
  82. package/dist/nba-ATT775GS.js +485 -0
  83. package/dist/nba-LKM4UHVU.js +482 -0
  84. package/dist/nba-YX7PXM3R.js +35 -0
  85. package/dist/nbagl-3WGZCZ4S.js +37 -0
  86. package/dist/nbagl-LP4XJRAL.js +229 -0
  87. package/dist/nbagl-VTTOU7LN.js +499 -0
  88. package/dist/ncaa_baseball-UV75LNVR.js +2670 -0
  89. package/dist/ncaa_baseball-VD5J5C72.js +419 -0
  90. package/dist/ncaa_baseball-YRBS32D6.js +4592 -0
  91. package/dist/ncaa_mhockey-IMYHS7QS.js +1033 -0
  92. package/dist/ncaa_mhockey-TXTKWHE3.js +115 -0
  93. package/dist/ncaa_mhockey-U3MFW5VT.js +663 -0
  94. package/dist/ncaa_softball-6WR6KS64.js +420 -0
  95. package/dist/ncaa_softball-RZCTIE56.js +2654 -0
  96. package/dist/ncaa_softball-VOD4PVGY.js +4599 -0
  97. package/dist/ncaa_whockey-6XXGUTBK.js +607 -0
  98. package/dist/ncaa_whockey-AKKYKY67.js +53 -0
  99. package/dist/ncaa_whockey-KBEKXFBB.js +293 -0
  100. package/dist/nfl-BDB2HYAR.js +625 -0
  101. package/dist/nfl-EHYHDLJD.js +37 -0
  102. package/dist/nfl-VF4NJCHY.js +514 -0
  103. package/dist/nfl_gsis-UY64JO37.js +4 -0
  104. package/dist/nhl-6WPHME7H.js +511 -0
  105. package/dist/nhl-EMIJFZHY.js +884 -0
  106. package/dist/nhl-ISFUKRFU.js +37 -0
  107. package/dist/ohl-2FEM554G.js +113 -0
  108. package/dist/ohl-C5Q4W6JB.js +32 -0
  109. package/dist/ohl-TQ4J46VC.js +66 -0
  110. package/dist/phf-H7POU2KW.js +32 -0
  111. package/dist/phf-K5IFMZUM.js +13 -0
  112. package/dist/phf-TWJI3BOW.js +20 -0
  113. package/dist/plot.d.ts +449 -0
  114. package/dist/plot.js +711 -0
  115. package/dist/plot.js.map +1 -0
  116. package/dist/plotly.d.ts +129 -0
  117. package/dist/plotly.js +360 -0
  118. package/dist/plotly.js.map +1 -0
  119. package/dist/pwhl-OX3BO4JI.js +53 -0
  120. package/dist/pwhl-W44OC3C6.js +17 -0
  121. package/dist/pwhl-ZJGG3H7W.js +23 -0
  122. package/dist/qmjhl-5HISEHLL.js +35 -0
  123. package/dist/qmjhl-DA3UHKE5.js +125 -0
  124. package/dist/qmjhl-YG4BOQPF.js +614 -0
  125. package/dist/react.d.ts +101 -0
  126. package/dist/react.js +164 -0
  127. package/dist/react.js.map +1 -0
  128. package/dist/resolve-CQW-jzvr.d.ts +28 -0
  129. package/dist/selection-D4T3PkRt.d.ts +140 -0
  130. package/dist/shots.d.ts +364 -0
  131. package/dist/shots.js +85 -0
  132. package/dist/shots.js.map +1 -0
  133. package/dist/soccer-7BH3ZLVL.js +7898 -0
  134. package/dist/soccer-MRU25OGE.js +2636 -0
  135. package/dist/soccer-PF4IPPVA.js +12342 -0
  136. package/dist/surfaces-ChaD9nei.d.ts +38 -0
  137. package/dist/testing.d.ts +75 -0
  138. package/dist/testing.js +240 -0
  139. package/dist/testing.js.map +1 -0
  140. package/dist/tiers-B6pUBPcE.d.ts +93 -0
  141. package/dist/types-C7n6gJF3.d.ts +62 -0
  142. package/dist/ufl-7VLZTLTT.js +143 -0
  143. package/dist/ufl-CCMBLPSW.js +100 -0
  144. package/dist/ufl-F2BJRXME.js +16 -0
  145. package/dist/usfl-4CR5RMZJ.js +32 -0
  146. package/dist/usfl-LD7HP2YK.js +14 -0
  147. package/dist/usfl-VU4YSJQD.js +19 -0
  148. package/dist/ushl-CPG2LB4L.js +73 -0
  149. package/dist/ushl-PQL6HJAD.js +88 -0
  150. package/dist/ushl-ZT2ATPHK.js +22 -0
  151. package/dist/vega.d.ts +85 -0
  152. package/dist/vega.js +362 -0
  153. package/dist/vega.js.map +1 -0
  154. package/dist/wbb-3KHCDYTK.js +5087 -0
  155. package/dist/wbb-BMX53Q3Q.js +369 -0
  156. package/dist/wbb-QNRN2CJJ.js +3620 -0
  157. package/dist/whl-4ATYZWUB.js +97 -0
  158. package/dist/whl-RVFPL6MH.js +79 -0
  159. package/dist/whl-Y7ASKYMM.js +28 -0
  160. package/dist/wnba-FWX5SFVI.js +232 -0
  161. package/dist/wnba-GX6HZR4B.js +220 -0
  162. package/dist/wnba-NTMWIPP6.js +20 -0
  163. package/dist/xfl-43R2JMJK.js +30 -0
  164. package/dist/xfl-H3BXQPZZ.js +16 -0
  165. package/dist/xfl-XON7A7FI.js +69 -0
  166. package/package.json +154 -3
@@ -0,0 +1,567 @@
1
+ import { S as SelectionStore, R as RowFilter } from './selection-D4T3PkRt.js';
2
+
3
+ /** A row field name, or an accessor. Index-based accessors cannot apply: linked tables re-test their own rows. */
4
+ type Field<R> = (keyof R & string) | ((row: R) => unknown);
5
+ /**
6
+ * What `brushFilter` brushes: the figure's rows, the fields its brushed axes encode (x and y, or one alone), each row's
7
+ * link id, what a brush holding no row means, and the scales of a chart that has no `figure.scale`.
8
+ */
9
+ interface BrushFilterOptions<R> {
10
+ /** The rows the figure was drawn from. */
11
+ data: readonly R[];
12
+ /** The field the figure's x scale encodes. Without `y`, a 1-D brush along x over the svg's full height. */
13
+ x?: Field<R>;
14
+ /** The field the figure's y scale encodes. Without `x`, a 1-D brush along y over the svg's full width. */
15
+ y?: Field<R>;
16
+ /** The link id per row: the same values as the marks' `id` / `linkIds` channel. Default: the row index. */
17
+ id?: Field<R>;
18
+ /**
19
+ * A brush that holds no row. `"dim"` (the default): its region stays in the store, so every mark dims and a linked
20
+ * table shows no row. `"clear"`: it is no filter at all; the store goes idle and, when the gesture ends, the drawn
21
+ * rectangle is removed (a date window over the All-Star break selects no game, and left drawn would look applied).
22
+ */
23
+ empty?: "dim" | "clear";
24
+ /**
25
+ * The brushed axes' scales, overriding `figure.scale`: how a d3-drawn chart, which has none, is brushed. A d3 scale
26
+ * goes in as it is (`d3.scaleUtc()`, whose `range()` is a method); a {@link ScaleLike} works too.
27
+ */
28
+ scales?: {
29
+ x?: ScaleLike | D3ScaleLike;
30
+ y?: ScaleLike | D3ScaleLike;
31
+ };
32
+ }
33
+ /** A live brush: drive it programmatically, or remove it. */
34
+ interface BrushFilterHandle {
35
+ /**
36
+ * Brush a region in DATA coordinates, one range per brushed axis (`null` clears), as if the user had dragged it. Rows
37
+ * are tested against these exact bounds, never their pixel round trip: a row on a bound is inside.
38
+ */
39
+ move(region: {
40
+ x?: readonly [unknown, unknown];
41
+ y?: readonly [unknown, unknown];
42
+ } | null): void;
43
+ /** Remove the overlay and stop following the store; clears its selection and predicate if this brush set them. */
44
+ destroy(): void;
45
+ }
46
+ /** The slice of a materialised Plot scale the brush reads (`plot.scale("x")`); structural, so no Plot import. */
47
+ interface ScaleLike {
48
+ /** Data value to pixel. */
49
+ apply(value: unknown): unknown;
50
+ /** Pixel to data value; absent on band and point scales, which a brush cannot invert. */
51
+ invert?(pixel: unknown): unknown;
52
+ /** The pixel range; the brush's extent spans it. Absent (or with no finite value), the svg's box on that axis. */
53
+ range?: Iterable<unknown>;
54
+ }
55
+ /**
56
+ * A d3-scale scale as `d3.scaleLinear()`, `d3.scaleUtc()` or `d3.scaleBand()` makes it: callable, with `range()` a
57
+ * METHOD (a {@link ScaleLike}'s `range` is the pixels themselves), and on a band scale `bandwidth()` and `domain()`
58
+ * methods too. `brushFilter` and `linkCursor` adapt it, so a d3-drawn chart passes its own scales as they are.
59
+ */
60
+ interface D3ScaleLike {
61
+ /** Data value to pixel. */
62
+ (value: never): unknown;
63
+ /** Pixel to data value; absent on band and point scales, which a brush cannot invert. */
64
+ invert?(pixel: number): unknown;
65
+ /** The pixel range, read once when the brush or cursor is made. */
66
+ range(): Iterable<unknown>;
67
+ /** A band scale's band width in pixels (a `linkCursor` band). */
68
+ bandwidth?(): number;
69
+ /** The domain; a band scale's values are each band's first value (a `linkCursor` band). */
70
+ domain?(): Iterable<unknown>;
71
+ }
72
+ /**
73
+ * What `Plot.plot` returns: the `<svg>`, or a `<figure>` wrapping it, exposing its scales. Any `<svg>` fits: a d3-drawn
74
+ * chart has no `scale` and passes `scales` instead.
75
+ */
76
+ type PlotFigure = Element & {
77
+ scale?(name: "x" | "y"): ScaleLike | undefined;
78
+ };
79
+ /**
80
+ * Overlay a d3-brush on a rendered Plot figure, or on a d3-drawn `<svg>` given its own `scales`. Each brush move sets
81
+ * the store's `predicate` (the region as a row test, for linked tables) and `selected` (the ids of `data` inside it,
82
+ * for highlighting) in ONE notification. Pass `x` and `y` for a rectangle, or one of them for a 1-D brush across the
83
+ * whole svg (a date window on a timeline). Clearing the brush, or a click on the empty chart, clears both, but only
84
+ * while the store still holds this brush's region. A selection made elsewhere since (a table row, a toggled mark)
85
+ * survives, and so does one made before that holds exactly the brushed ids: the store kept that set, so the brush
86
+ * wrote none of its own. The drawn brush follows the store: when its region is cleared or replaced elsewhere, the
87
+ * rectangle is removed with no second write. `empty` says what a brush holding no row means. The overlay is inserted
88
+ * BEHIND the marks, so hovering a mark still reaches it. On a figure with a Plot `tip`, the press that starts a brush
89
+ * also pins the tip showing at that moment (Plot's pointer toggles a sticky tip on `pointerdown`); the brush still
90
+ * works. Throws `InputError` without `x` or `y`, or unless each brushed axis has a continuous (invertible) scale, in
91
+ * Node too. Returns a HANDLE, not a teardown function, because a brush has more to do than tear down: `move` drives it
92
+ * and `destroy` removes it (`linkSelection` and `linkCursor`, whose teardown is all they have, return a function). A
93
+ * no-op handle without a DOM.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * import * as Plot from "@observablehq/plot";
98
+ * import { createSelection } from "@sportsdataverse/sdvplot";
99
+ * import { brushFilter } from "@sportsdataverse/sdvplot/interact";
100
+ * import { linkIds } from "@sportsdataverse/sdvplot/plot";
101
+ *
102
+ * const rows = [
103
+ * { team: "KC", wins: 15, net_epa: 0.063 },
104
+ * { team: "LAC", wins: 11, net_epa: 0.101 },
105
+ * { team: "MIA", wins: 8, net_epa: -0.019 },
106
+ * ];
107
+ * const svg = Plot.plot({ marks: [Plot.dot(rows, { x: "wins", y: "net_epa", render: linkIds(rows, "team") })] });
108
+ * const store = createSelection<(typeof rows)[number]>();
109
+ * const brush = brushFilter(svg, store, { data: rows, x: "wins", y: "net_epa", id: "team" });
110
+ * brush.move({ x: [10, 16], y: [0, 0.15] }); // drag over the 10+ win teams with a positive net EPA
111
+ * [...store.getState().selected]; // ["KC", "LAC"]
112
+ * ```
113
+ *
114
+ * @example A 1-D date window that clears when it holds no game
115
+ * ```ts
116
+ * import * as Plot from "@observablehq/plot";
117
+ * import { createSelection } from "@sportsdataverse/sdvplot";
118
+ * import { brushFilter } from "@sportsdataverse/sdvplot/interact";
119
+ * import { linkIds } from "@sportsdataverse/sdvplot/plot";
120
+ *
121
+ * // five of Brooklyn's 2025-26 games
122
+ * const games = [
123
+ * { game_id: "0022500130", game_date: "2025-10-29", matchup: "BKN vs. ATL" },
124
+ * { game_id: "0022500149", game_date: "2025-11-02", matchup: "BKN vs. PHI" },
125
+ * { game_id: "0022500156", game_date: "2025-11-03", matchup: "BKN vs. MIN" },
126
+ * { game_id: "0022500173", game_date: "2025-11-05", matchup: "BKN @ IND" },
127
+ * { game_id: "0022500031", game_date: "2025-11-07", matchup: "BKN vs. DET" },
128
+ * ];
129
+ * const day = (g: (typeof games)[number]): Date => new Date(`${g.game_date}T00:00:00Z`);
130
+ * const svg = Plot.plot({
131
+ * height: 60,
132
+ * x: { type: "utc" },
133
+ * marks: [Plot.tickX(games, { x: day, render: linkIds(games, "game_id") })],
134
+ * });
135
+ * const store = createSelection<(typeof games)[number]>();
136
+ * // x alone: a brush along the dates; "clear": a window holding no game is no filter and is not left drawn
137
+ * const brush = brushFilter(svg, store, { data: games, x: day, id: "game_id", empty: "clear" });
138
+ * brush.move({ x: [new Date("2025-11-01"), new Date("2025-11-06")] });
139
+ * [...store.getState().selected]; // ["0022500149", "0022500156", "0022500173"]
140
+ * ```
141
+ *
142
+ * @example A d3-drawn chart brushed through its own d3 scale
143
+ * ```ts
144
+ * import { scaleUtc, select } from "d3";
145
+ * import { createSelection } from "@sportsdataverse/sdvplot";
146
+ * import { brushFilter } from "@sportsdataverse/sdvplot/interact";
147
+ *
148
+ * // five of Brooklyn's 2025-26 games
149
+ * const games = [
150
+ * { game_id: "0022500130", game_date: "2025-10-29", matchup: "BKN vs. ATL" },
151
+ * { game_id: "0022500149", game_date: "2025-11-02", matchup: "BKN vs. PHI" },
152
+ * { game_id: "0022500156", game_date: "2025-11-03", matchup: "BKN vs. MIN" },
153
+ * { game_id: "0022500173", game_date: "2025-11-05", matchup: "BKN @ IND" },
154
+ * { game_id: "0022500031", game_date: "2025-11-07", matchup: "BKN vs. DET" },
155
+ * ];
156
+ * const day = (g: (typeof games)[number]): Date => new Date(`${g.game_date}T00:00:00Z`);
157
+ * const x = scaleUtc([new Date("2025-10-28"), new Date("2025-11-08")], [10, 630]);
158
+ * const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
159
+ * svg.setAttribute("viewBox", "0 0 640 60");
160
+ * select(svg)
161
+ * .selectAll("rect")
162
+ * .data(games)
163
+ * .join("rect")
164
+ * .attr("x", (g) => x(day(g)) - 1.5)
165
+ * .attr("y", 6)
166
+ * .attr("width", 3)
167
+ * .attr("height", 40)
168
+ * .attr("data-sdv-id", (g) => g.game_id); // the stamp linkIds would write
169
+ * const store = createSelection<(typeof games)[number]>();
170
+ * // no figure.scale on a d3 chart: pass the d3 scale itself (its range() is a method; brushFilter reads it)
171
+ * brushFilter(svg, store, { data: games, x: day, id: "game_id", scales: { x } }).move({
172
+ * x: [new Date("2025-11-04"), new Date("2025-11-08")],
173
+ * });
174
+ * [...store.getState().selected]; // ["0022500173", "0022500031"]
175
+ * ```
176
+ */
177
+ declare function brushFilter<R>(figure: PlotFigure, store: SelectionStore<R>, o: BrushFilterOptions<R>): BrushFilterHandle;
178
+
179
+ /** A scale a cursor maps through: a {@link ScaleLike}, plus a band scale's width and domain. */
180
+ interface BandScaleLike extends ScaleLike {
181
+ /** The band width in pixels. A number marks a band scale, whose domain values are the bands' first values. */
182
+ readonly bandwidth?: number;
183
+ /** The domain; a band scale's must be all numbers. */
184
+ readonly domain?: Iterable<unknown>;
185
+ }
186
+ /**
187
+ * What a figure draws for the cursor. `axis: "x" | "y"`: a rule across the plot at the value, or with `width` a band
188
+ * `width` data units wide centred on it, or on a band scale (`bandwidth`) the band holding it. `cross` is the other
189
+ * axis' scale: the rule spans its range (default: the figure's own other scale on a Plot figure, else the svg's
190
+ * viewBox). `axis: "ring"`: an ellipse around `center` (data coordinates) whose radii are the value mapped through the
191
+ * x and y scales, such as a shot distance around the hoop. Every scale is a Plot figure's (`svg.scale("x")`) or a d3
192
+ * scale as it is ({@link D3ScaleLike}: `d3.scaleLinear()`, `d3.scaleBand()`), as `brushFilter`'s `scales` are.
193
+ */
194
+ type CursorShape = {
195
+ readonly axis: "x" | "y";
196
+ readonly scale: BandScaleLike | D3ScaleLike;
197
+ readonly width?: number;
198
+ readonly cross?: ScaleLike | D3ScaleLike;
199
+ } | {
200
+ readonly axis: "ring";
201
+ readonly x: ScaleLike | D3ScaleLike;
202
+ readonly y: ScaleLike | D3ScaleLike;
203
+ readonly center: readonly [number, number];
204
+ };
205
+ /** How {@link linkCursor} follows and emits the store's cursor. */
206
+ interface LinkCursorOptions {
207
+ /** The cursor field this figure draws and emits, such as `"shot_distance"`; a cursor naming another field hides it. */
208
+ readonly field: string;
209
+ /** What to draw, through which scale. */
210
+ readonly shape: CursorShape;
211
+ /** Write the value under the pointer to the store. Default `true` for `"x"` / `"y"`; a ring never emits. */
212
+ readonly emit?: boolean;
213
+ /** Snap an emitted value on a linear scale, such as `(v) => Math.floor(v) + 0.5` for 1 ft bins. Default: none. */
214
+ readonly snap?: (value: number) => number;
215
+ /** A readout beside an `"x"` cursor, one line per string. */
216
+ readonly label?: (value: number) => readonly string[];
217
+ /** Past this fraction of the axis' pixel range (0, 1] the readout flips to the cursor's left. Default 30 / 35. */
218
+ readonly flipAt?: number;
219
+ /** A dot on the cursor at this OTHER-axis data value (a curve's value there); `null` hides it. Needs `cross`. */
220
+ readonly dot?: (value: number) => number | null;
221
+ }
222
+ /**
223
+ * Draw the store's cursor (J34: one shared hover VALUE, such as a shot distance) through THIS figure's own scale, and
224
+ * write the value under the pointer back. Every linked chart follows the same number its own way: a band on share bars
225
+ * (linear x), the band holding it on FG% bars (a band scale), a band on a side chart (y), a rule on a curve, a ring
226
+ * around the hoop. Moving inside one snapped bin writes nothing; crossing a bin edge is one update; leaving the figure
227
+ * (`pointerleave`, `pointercancel`) or the axis' range clears the cursor this figure wrote, while the store still holds
228
+ * it: never one an app `store.set` or another figure wrote since, nor an equal one set before (the store kept that one,
229
+ * so this figure wrote none). A cursor never dims marks or filters a table (it is not an id). A store change moves
230
+ * attributes only: no element is added or removed, so a cursor costs O(1) per figure. The move and press listeners
231
+ * capture, so a Plot `tip` that stops a press from reaching other listeners does not stop this one; the leave listeners
232
+ * do not, so a mark's own `pointerleave` (the pointer crossing a bar's edge inside one bin) writes nothing. Styled by
233
+ * `--sdv-cursor-color` and `--sdv-cursor-width`. Returns a teardown FUNCTION, not a handle, because teardown is all it
234
+ * has (as `linkSelection`'s; `brushFilter`, `nearestHover` and `tooltip` return a handle with `destroy()`). The scales'
235
+ * pixel ranges are read once, so after a resize, relink. The teardown removes the cursor and its listeners, and clears
236
+ * the store's cursor when it still holds the one this figure last wrote (as `linkSelection`'s teardown does with its
237
+ * hover): a chart redrawn under the pointer leaves no cursor that no pointer drives. Throws `InputError` on a bad
238
+ * option, in Node too; a no-op without a DOM.
239
+ *
240
+ * @example
241
+ * ```ts
242
+ * import * as Plot from "@observablehq/plot";
243
+ * import { createSelection } from "@sportsdataverse/sdvplot";
244
+ * import { linkCursor } from "@sportsdataverse/sdvplot/interact";
245
+ * import { fgPctByDistance } from "@sportsdataverse/sdvplot/shots";
246
+ *
247
+ * // Brooklyn shots from the sportsdataverse-data nba_stats_shots release, 2025-26
248
+ * const shots = [
249
+ * { x_legacy: -1, y_legacy: 7, shot_distance: 1, shot_value: 2, shot_result: "Made" },
250
+ * { x_legacy: 0, y_legacy: 0, shot_distance: 0, shot_value: 2, shot_result: "Missed" },
251
+ * { x_legacy: -44, y_legacy: 252, shot_distance: 26, shot_value: 3, shot_result: "Made" },
252
+ * ];
253
+ * const bins = fgPctByDistance(shots, 1, 30);
254
+ * const svg = Plot.plot({
255
+ * x: { label: "Shot distance (ft)" },
256
+ * marks: [Plot.rectY(bins, { x1: "distance", x2: (b) => b.distance + 1, y: "attempts" })],
257
+ * });
258
+ * const store = createSelection();
259
+ * linkCursor(svg, store, {
260
+ * field: "shot_distance",
261
+ * shape: { axis: "x", scale: svg.scale("x")!, width: 1 },
262
+ * snap: (feet) => Math.floor(feet) + 0.5, // one value per 1 ft bin
263
+ * label: (feet) => [`@ ${Math.floor(feet)} ft`],
264
+ * });
265
+ * store.set({ cursor: { field: "shot_distance", value: 26.5 } }); // what a pointer 26.3 ft out writes
266
+ * svg; // the 26-27 ft band shaded, with its readout
267
+ * ```
268
+ */
269
+ declare function linkCursor<R>(root: Element, store: SelectionStore<R>, o: LinkCursorOptions): () => void;
270
+
271
+ /**
272
+ * Interaction needs a live DOM. Without one (Node / SSR) every `sdvplot/interact` function is a no-op, so a
273
+ * server-rendered figure is byte-identical with or without a link.
274
+ *
275
+ * @example
276
+ * ```ts
277
+ * import { hasDom } from "@sportsdataverse/sdvplot/interact";
278
+ *
279
+ * // it depends on where this runs: a browser (or the jsdom that prerendered this page) has a DOM, Node has none
280
+ * `this ran ${hasDom() ? "with a DOM: interact functions work" : "without a DOM: interact functions are no-ops"}`;
281
+ * ```
282
+ */
283
+ declare function hasDom(): boolean;
284
+ /**
285
+ * Emphasise the marks under `root` stamped (`data-sdv-id`) with one of `ids` and dim the rest; `null` clears and an
286
+ * empty set dims everything. A class toggle, never a redraw: each call touches only the marks whose state changed,
287
+ * plus the root's class, and one CSS rule (`--sdv-dim-opacity`, default 0.2) does the dimming. Marks Plot draws later
288
+ * (a `Plot.pointer` layer) are indexed as they appear; axis logos (`data-sdv-axis`) are decorations and never dim.
289
+ * Returns the ids that match no mark here (an id absent from this figure is not an error). No-op without a DOM.
290
+ *
291
+ * @example
292
+ * ```ts
293
+ * import * as Plot from "@observablehq/plot";
294
+ * import { highlight } from "@sportsdataverse/sdvplot/interact";
295
+ * import { linkIds } from "@sportsdataverse/sdvplot/plot";
296
+ *
297
+ * const rows = [
298
+ * { team: "KC", wins: 15, pf: 385 },
299
+ * { team: "BUF", wins: 13, pf: 525 },
300
+ * { team: "MIA", wins: 8, pf: 345 },
301
+ * ];
302
+ * const svg = Plot.plot({ marks: [Plot.dot(rows, { x: "wins", y: "pf", r: 6, render: linkIds(rows, "team") })] });
303
+ * const missing = highlight(svg, new Set(["KC", "SEA"])); // ["SEA"]: no SEA dot here; BUF and MIA dim
304
+ * svg;
305
+ * ```
306
+ */
307
+ declare function highlight(root: Element, ids: ReadonlySet<string> | null): string[];
308
+
309
+ /** What an sdvtables engine emits on `subscribe` (structurally its `TableEvent`). */
310
+ type LinkEvent = {
311
+ readonly type: "change";
312
+ } | {
313
+ readonly type: "hover";
314
+ readonly id: string | null;
315
+ } | {
316
+ readonly type: "select";
317
+ readonly ids: ReadonlySet<string>;
318
+ };
319
+ /** The slice of an sdvtables `Table` that linking needs. Structural, so sdvplot never depends on sdvtables. */
320
+ interface LinkableTable<Row> {
321
+ /** Filter the table's rows by a row test (the store's brush region); `null` clears it. */
322
+ setExternalFilter(filter: RowFilter<Row> | null): void;
323
+ /** Select rows by id; the table renders them selected. */
324
+ setSelection(ids: ReadonlySet<string>): void;
325
+ /** Light one row by id (the store's first hover id); `null` clears it. A table holds one hover id. */
326
+ setHover(id: string | null): void;
327
+ /** Row hover and click events; returns the unsubscribe function. */
328
+ subscribe(fn: (event: LinkEvent) => void): () => void;
329
+ }
330
+ /** What {@link linkSelection} wires to a store: one figure and/or one table. */
331
+ interface LinkSelectionOptions<Row, Datum = unknown> {
332
+ /**
333
+ * The figure whose marks carry `data-sdv-id`: what `Plot.plot` returns (stamped by `linkIds`), or a d3-drawn `<svg>`
334
+ * with its own stamps.
335
+ */
336
+ figure?: Element;
337
+ /**
338
+ * How the figure writes `hover`. `true` (the default): the stamped mark under the pointer (`mouseover`). `false`: it
339
+ * writes nothing, for a figure another writer hovers. `{ id }`: the datum Plot's own `tip`/`pointer` picks, the
340
+ * nearest within its `maxRadius`, published as the figure's `value` with an `input` event; `id` maps it to a link id
341
+ * (`null` clears). Pass `figure` as `Plot.plot` returned it: with a caption or legend that is a `<figure>`, and only the
342
+ * figure hears `input`. Anything else throws `InputError`, in Node too.
343
+ */
344
+ hover?: boolean | {
345
+ readonly id: (datum: Datum) => unknown;
346
+ };
347
+ /** A `createTable` engine: `useTable().table`, or the one passed to `hydrate`. */
348
+ table?: LinkableTable<Row>;
349
+ /**
350
+ * `"toggle"`: each stamped mark of `figure` becomes a checkbox (`role`, `tabindex="0"`, `aria-checked` kept in step
351
+ * with the store), and a click, Enter or Space toggles its id in `selected`. Each checkbox keeps the mark's own
352
+ * accessible name: name the marks through Plot's `ariaLabel` channel (`ariaLabel: "matchup"`, an `aria-label`) or its
353
+ * `title` channel (a `<title>`); a mark with neither is named by its link id until teardown. Throws `InputError` when
354
+ * a mark sits in an `<a href>`: a checkbox inside a link is nested interactive content with two tab stops; and, with
355
+ * a DOM, when the figure already has a live toggle link, whose click would then toggle twice (tear it down first).
356
+ * On a Plot figure with a `tip`, a click also pins Plot's tip; give the tip `pointerEvents: "none"` so the pinned tip
357
+ * never takes a click meant for a mark under it.
358
+ */
359
+ select?: "toggle";
360
+ }
361
+ /**
362
+ * Wire a figure and/or a table to a selection store (J31). Figure: pointer hover → `hover` (see
363
+ * {@link LinkSelectionOptions.hover}); every store change → `highlight(figure, focusIds(state))`. Table: row hover /
364
+ * click → `hover` / `selected`; every store change → `setSelection(selected)`, `setExternalFilter(predicate)` and
365
+ * `setHover` with the first hover id. Loop-free: the store and the engine drop no-op updates, and the events a table
366
+ * emits while the store is being applied to it are not written back (so a two-id hover is never narrowed to the one id
367
+ * a table holds). One figure or one table per call; link several by calling again with the same store. The store
368
+ * comes first because the call takes a figure, a table or both. Returns a teardown FUNCTION, not a handle: teardown is
369
+ * all a link has, so `useEffect(() => linkSelection(store, o), deps)` is one line (`brushFilter`, `nearestHover` and
370
+ * `tooltip`, which have more to do, return a handle with `destroy()`). The teardown clears `hover` when the store still
371
+ * holds the hover this link wrote (as `nearestHover`'s `destroy` does; an equal hover set first elsewhere is not one it
372
+ * wrote): a figure redrawn under the pointer leaves no stale hover dimming the others. It then restores the view it
373
+ * changed, so a figure or table unlinked and kept on the page shows no store state: the figure un-dims (once the last
374
+ * link on it goes), its toggled marks get their own attributes back, and the table drops the brush filter and the hover
375
+ * it showed. It never clears store state another writer owns: a brush's region and a selection stay in the store for
376
+ * the views still linked. To replace a figure, tear its link down before linking the new one, which otherwise reads the
377
+ * old figure's hover. Inert without a DOM, so server-rendered markup never changes. With `select: "toggle"`, the
378
+ * figure's marks are keyboard-reachable checkboxes over `selected` ({@link LinkSelectionOptions.select}).
379
+ *
380
+ * @example
381
+ * ```ts
382
+ * import * as Plot from "@observablehq/plot";
383
+ * import { createSelection } from "@sportsdataverse/sdvplot";
384
+ * import { linkSelection } from "@sportsdataverse/sdvplot/interact";
385
+ * import { linkIds } from "@sportsdataverse/sdvplot/plot";
386
+ * import { createTable, defineTable } from "@sportsdataverse/sdvtables";
387
+ *
388
+ * const rows = [
389
+ * { team: "KC", wins: 15, net_epa: 0.063 },
390
+ * { team: "BUF", wins: 13, net_epa: 0.19 },
391
+ * { team: "MIA", wins: 8, net_epa: -0.019 },
392
+ * ];
393
+ * type Row = (typeof rows)[number];
394
+ * const svg = Plot.plot({ marks: [Plot.dot(rows, { x: "wins", y: "net_epa", r: 6, render: linkIds(rows, "team") })] });
395
+ * const spec = defineTable<Row>().columns((c) => [c.text("team"), c.int("wins")]).rowKey("team").build();
396
+ * const table = createTable(spec, rows); // the engine a hydrated table or <SdvTable table/> renders
397
+ * const store = createSelection<Row>();
398
+ * linkSelection(store, { figure: svg });
399
+ * linkSelection(store, { table });
400
+ * table.setSelection(new Set(["BUF"])); // what a click on Buffalo's row calls: the store and the figure follow
401
+ * svg; // Buffalo's dot lit, the rest dimmed
402
+ * ```
403
+ *
404
+ * @example Games as checkboxes: click a cell, or Tab to it and press Enter or Space
405
+ * ```ts
406
+ * import * as Plot from "@observablehq/plot";
407
+ * import { createSelection } from "@sportsdataverse/sdvplot";
408
+ * import { linkSelection } from "@sportsdataverse/sdvplot/interact";
409
+ * import { linkIds } from "@sportsdataverse/sdvplot/plot";
410
+ *
411
+ * // five of Brooklyn's 2025-26 games
412
+ * const games = [
413
+ * { game_id: "0022500130", game_date: "2025-10-29", matchup: "BKN vs. ATL", wl: "L" },
414
+ * { game_id: "0022500149", game_date: "2025-11-02", matchup: "BKN vs. PHI", wl: "L" },
415
+ * { game_id: "0022500156", game_date: "2025-11-03", matchup: "BKN vs. MIN", wl: "L" },
416
+ * { game_id: "0022500173", game_date: "2025-11-05", matchup: "BKN @ IND", wl: "W" },
417
+ * { game_id: "0022500031", game_date: "2025-11-07", matchup: "BKN vs. DET", wl: "L" },
418
+ * ];
419
+ * const svg = Plot.plot({
420
+ * height: 60,
421
+ * x: { type: "band" },
422
+ * marks: [
423
+ * Plot.cell(games, { x: "game_date", fill: "wl", ariaLabel: "matchup", render: linkIds(games, "game_id") }),
424
+ * ],
425
+ * });
426
+ * const store = createSelection<(typeof games)[number]>();
427
+ * linkSelection(store, { figure: svg, select: "toggle" }); // each cell: role="checkbox", tabindex="0", aria-checked
428
+ * svg.querySelector('[data-sdv-id="0022500173"]')?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
429
+ * svg; // the win at Indiana checked and lit, the rest dimmed: click a game, or Tab to it and press Enter or Space
430
+ * ```
431
+ */
432
+ declare function linkSelection<Row, Datum = unknown>(store: SelectionStore<Row>, o: LinkSelectionOptions<Row, Datum>): () => void;
433
+
434
+ /** A hover target: a mark's centre in PIXELS, in the svg's user space (where its scales put it), and its link id. */
435
+ interface HoverPoint {
436
+ /** Horizontal position, svg user units. */
437
+ readonly x: number;
438
+ /** Vertical position, svg user units. */
439
+ readonly y: number;
440
+ /** The link id the mark is stamped with (`data-sdv-id`). */
441
+ readonly id: string;
442
+ }
443
+ /** Options for {@link nearestHover}. */
444
+ interface NearestHoverOptions {
445
+ /** Every hoverable mark's centre. Points with a non-finite coordinate are skipped. */
446
+ readonly points: readonly HoverPoint[];
447
+ /** `"xy"` (the default): nearest in the plane. `"x"` / `"y"`: nearest along that axis alone, ignoring the other. */
448
+ readonly dimension?: "x" | "y" | "xy";
449
+ /** Farthest hover, in px: Euclidean for `"xy"`, along the axis for `"x"` / `"y"`. Default: unlimited. */
450
+ readonly radius?: number;
451
+ /** A margin inside the svg's bounds, in px, where the pointer hovers nothing. Default 0. */
452
+ readonly padding?: number;
453
+ /** The tooltip for a hovered id, shown at its point; `null` shows none. Default: no tooltip. */
454
+ readonly label?: (id: string) => {
455
+ readonly lines: readonly string[];
456
+ readonly swatch?: string;
457
+ } | null;
458
+ }
459
+ /** A live {@link nearestHover}. */
460
+ interface NearestHoverHandle {
461
+ /** Re-target after a zoom or a resize: the marks' new centres. */
462
+ update(points: readonly HoverPoint[]): void;
463
+ /** Remove the listeners and the tooltip; clears `hover` when it still holds what this handle wrote. */
464
+ destroy(): void;
465
+ }
466
+ /**
467
+ * Hover the NEAREST mark of a d3-drawn (or any non-Plot) figure, within a radius, rather than the element under the
468
+ * pointer: sparse or small marks stay easy to hit. `"xy"` finds it with a Delaunay triangulation (`Delaunay.find`,
469
+ * as blazing-the-nets' hex chart does within 18 px); `"x"` / `"y"` bisect the points sorted along that axis (a 1-D
470
+ * Voronoi, as its distance bars do). The pointer writes `hover: [id]`, once per distinct mark; beyond `radius`, inside
471
+ * the `padding` margin, on `pointerleave` / `pointercancel` or with no points, it writes `hover: []`. `label` shows a
472
+ * {@link tooltip} at the mark, not at the pointer. Link the same figure with `hover: false` in `linkSelection` so
473
+ * this is its only hover writer while `highlight` still follows the store. The pointer listeners capture,
474
+ * so a handler registered earlier on the svg that stops the event (Plot's `tip` on `pointerdown`) cannot hide it.
475
+ * A Plot figure hovers through its own `tip` instead (`linkSelection`'s `hover: { id }`). Throws `InputError` on a
476
+ * negative `radius` or `padding` or an unknown `dimension`, in Node too, and when `root` is not an `<svg>`. Returns a
477
+ * HANDLE, not a teardown function, because it has more to do than tear down: `update` re-targets it and `destroy`
478
+ * removes it (`linkSelection` and `linkCursor`, whose teardown is all they have, return a function). A no-op handle
479
+ * without a DOM.
480
+ *
481
+ * @example
482
+ * ```ts
483
+ * import { createSelection } from "@sportsdataverse/sdvplot";
484
+ * import { linkSelection, nearestHover } from "@sportsdataverse/sdvplot/interact";
485
+ * import * as d3 from "d3";
486
+ *
487
+ * const rows = [
488
+ * { team: "KC", wins: 15, net_epa: 0.063 },
489
+ * { team: "BUF", wins: 13, net_epa: 0.19 },
490
+ * { team: "MIA", wins: 8, net_epa: -0.019 },
491
+ * { team: "LV", wins: 4, net_epa: -0.146 },
492
+ * ];
493
+ * const x = d3.scaleLinear([0, 17], [30, 410]);
494
+ * const y = d3.scaleLinear([-0.2, 0.2], [190, 10]);
495
+ * const svg = d3.create("svg").attr("viewBox", "0 0 440 200").attr("width", 440);
496
+ * svg
497
+ * .selectAll("circle")
498
+ * .data(rows)
499
+ * .join("circle")
500
+ * .attr("data-sdv-id", (d) => d.team) // the d3 way to stamp link ids
501
+ * .attr("cx", (d) => x(d.wins))
502
+ * .attr("cy", (d) => y(d.net_epa))
503
+ * .attr("r", 5)
504
+ * .attr("fill", "currentColor");
505
+ * const node = svg.node() as SVGSVGElement;
506
+ * const store = createSelection();
507
+ * linkSelection(store, { figure: node, hover: false }); // highlight follows the store; nearestHover writes hover
508
+ * nearestHover(node, store, {
509
+ * points: rows.map((d) => ({ x: x(d.wins), y: y(d.net_epa), id: d.team })),
510
+ * radius: 18,
511
+ * label: (team) => ({ lines: [team, `net EPA/play ${rows.find((d) => d.team === team)?.net_epa}`] }),
512
+ * });
513
+ * node; // hover near a dot: within 18 px it lights and its tooltip shows
514
+ * ```
515
+ */
516
+ declare function nearestHover<R>(root: Element, store: SelectionStore<R>, o: NearestHoverOptions): NearestHoverHandle;
517
+
518
+ /** Options for {@link tooltip}. */
519
+ interface TooltipOptions {
520
+ /** The width the box stays inside, in the svg's user units. Default: the svg's viewBox width. */
521
+ readonly width?: number;
522
+ /** The height the box stays inside. Default: the svg's viewBox height. */
523
+ readonly height?: number;
524
+ /** Text size in px; the box is sized from it. Default 11. */
525
+ readonly fontSize?: number;
526
+ }
527
+ /** A live tooltip that {@link tooltip} returns. */
528
+ interface TooltipHandle {
529
+ /**
530
+ * Show `lines` (the first bold) beside the point `x`, `y` in the svg's user space, with an optional colour swatch
531
+ * at the right. Placed 12 px right of the point and above it; flipped to its left when it would pass the right
532
+ * edge, and kept inside the top and bottom.
533
+ */
534
+ show(x: number, y: number, lines: readonly string[], swatch?: string): void;
535
+ /** Hide the box; `show` brings it back. */
536
+ hide(): void;
537
+ /** Remove the box. */
538
+ destroy(): void;
539
+ }
540
+ /**
541
+ * An in-SVG tooltip for d3-drawn and other non-Plot figures (a Plot figure uses its own `tip` mark): a rounded box of
542
+ * text lines, the first bold, with an optional 12 px colour swatch, kept inside the svg. It is drawn hidden, with
543
+ * `pointer-events="none"`, and re-appended last on each `show` so it stays above the marks. Its size is ESTIMATED from
544
+ * the text (`0.6 * fontSize` per character, `1.35 * fontSize` per line), never measured, so it renders the same under
545
+ * jsdom and in every browser. Colours: `--sdv-tip-bg` (default `rgba(34,34,34,.85)`) and `--sdv-tip-fg` (default
546
+ * `#ddd`). Throws `InputError` on a non-positive `fontSize` or a negative size, in Node too, and when `root` is not an
547
+ * `<svg>`. Returns a HANDLE (`show`, `hide`, `destroy`), not a teardown function, because it has more to do than tear
548
+ * down. A no-op handle without a DOM: a server render never holds a tooltip.
549
+ *
550
+ * @example
551
+ * ```ts
552
+ * import { loadLeague, teamColorsSync } from "@sportsdataverse/sdvplot";
553
+ * import { tooltip } from "@sportsdataverse/sdvplot/interact";
554
+ * import * as d3 from "d3";
555
+ *
556
+ * await loadLeague("nfl");
557
+ * const red = teamColorsSync("nfl", "KC");
558
+ * const svg = d3.create("svg").attr("viewBox", "0 0 320 120").attr("width", 320);
559
+ * svg.append("circle").attr("cx", 280).attr("cy", 80).attr("r", 6).attr("fill", red ?? "currentColor");
560
+ * // Kansas City's 2024 line, as a chart shows it on hover: no room right of the dot, so the box flips to its left
561
+ * tooltip(svg.node() as SVGSVGElement).show(280, 80, ["KC 15-2", "Net EPA/play 0.063", "QB Patrick Mahomes"], red);
562
+ * svg.node();
563
+ * ```
564
+ */
565
+ declare function tooltip(root: Element, o?: TooltipOptions): TooltipHandle;
566
+
567
+ export { type BandScaleLike, type BrushFilterHandle, type BrushFilterOptions, type CursorShape, type D3ScaleLike, type Field, type HoverPoint, type LinkCursorOptions, type LinkEvent, type LinkSelectionOptions, type LinkableTable, type NearestHoverHandle, type NearestHoverOptions, type PlotFigure, type ScaleLike, type TooltipHandle, type TooltipOptions, brushFilter, hasDom, highlight, linkCursor, linkSelection, nearestHover, tooltip };