@sportsdataverse/sdvplot 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +94 -0
- package/README.md +908 -2
- package/dist/_web-D7Aa5Rd8.d.ts +69 -0
- package/dist/aaf-GJ3SH34T.js +13 -0
- package/dist/aaf-HCYN5T6Y.js +37 -0
- package/dist/aaf-ZAE5KS2Y.js +13 -0
- package/dist/ahl-E66MDVCU.js +66 -0
- package/dist/ahl-NPQQWZEV.js +249 -0
- package/dist/ahl-QRDU73XX.js +309 -0
- package/dist/axis-CraWQRUJ.d.ts +49 -0
- package/dist/bins.d.ts +174 -0
- package/dist/bins.js +4 -0
- package/dist/bins.js.map +1 -0
- package/dist/cfb-F42SVS4X.js +5000 -0
- package/dist/cfb-P3SHEYRZ.js +692 -0
- package/dist/cfb-WXRZANLP.js +10226 -0
- package/dist/chartjs-surface.d.ts +42 -0
- package/dist/chartjs-surface.js +65 -0
- package/dist/chartjs-surface.js.map +1 -0
- package/dist/chartjs.d.ts +132 -0
- package/dist/chartjs.js +339 -0
- package/dist/chartjs.js.map +1 -0
- package/dist/chunk-4C3GX6QO.js +92 -0
- package/dist/chunk-4C3GX6QO.js.map +1 -0
- package/dist/chunk-C2CDY5OG.js +137 -0
- package/dist/chunk-C2CDY5OG.js.map +1 -0
- package/dist/chunk-E7G7VSLV.js +43 -0
- package/dist/chunk-E7G7VSLV.js.map +1 -0
- package/dist/chunk-J6R2LEJ7.js +58 -0
- package/dist/chunk-J6R2LEJ7.js.map +1 -0
- package/dist/chunk-JJFTD4VB.js +175 -0
- package/dist/chunk-JJFTD4VB.js.map +1 -0
- package/dist/chunk-KKH6ABMA.js +223 -0
- package/dist/chunk-KKH6ABMA.js.map +1 -0
- package/dist/chunk-NLPH2VZT.js +127 -0
- package/dist/chunk-NLPH2VZT.js.map +1 -0
- package/dist/chunk-OKEE4AH6.js +30 -0
- package/dist/chunk-OKEE4AH6.js.map +1 -0
- package/dist/chunk-QY65DUEF.js +306 -0
- package/dist/chunk-QY65DUEF.js.map +1 -0
- package/dist/chunk-RMT2GMFH.js +97 -0
- package/dist/chunk-RMT2GMFH.js.map +1 -0
- package/dist/chunk-SEBO35DV.js +76 -0
- package/dist/chunk-SEBO35DV.js.map +1 -0
- package/dist/chunk-TAXO3VXH.js +556 -0
- package/dist/chunk-TAXO3VXH.js.map +1 -0
- package/dist/chunk-TDNBAEQB.js +105 -0
- package/dist/chunk-TDNBAEQB.js.map +1 -0
- package/dist/cricket-KPCYG2HL.js +493 -0
- package/dist/cricket-NDXLE25B.js +116 -0
- package/dist/cricket-YUUF7P6M.js +160 -0
- package/dist/d3.d.ts +234 -0
- package/dist/d3.js +198 -0
- package/dist/d3.js.map +1 -0
- package/dist/echarts.d.ts +121 -0
- package/dist/echarts.js +228 -0
- package/dist/echarts.js.map +1 -0
- package/dist/echl-3CR4RGFC.js +241 -0
- package/dist/echl-NPC32C23.js +107 -0
- package/dist/echl-PI33DGJC.js +64 -0
- package/dist/export.d.ts +130 -0
- package/dist/export.js +258 -0
- package/dist/export.js.map +1 -0
- package/dist/headshots-B2J_LOO8.d.ts +40 -0
- package/dist/index.d.ts +182 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/interact.d.ts +567 -0
- package/dist/interact.js +809 -0
- package/dist/interact.js.map +1 -0
- package/dist/marks-DH-tQIP-.d.ts +75 -0
- package/dist/mbb-3TRLVW3H.js +3619 -0
- package/dist/mbb-DQUHCHZP.js +5111 -0
- package/dist/mbb-PRAIO2P5.js +371 -0
- package/dist/milb-6IQ6NCEW.js +291 -0
- package/dist/milb-EYY5MD4T.js +1752 -0
- package/dist/milb-EYZ4SZLI.js +797 -0
- package/dist/mlb-4PBUJSVQ.js +35 -0
- package/dist/mlb-FHPY3GQJ.js +701 -0
- package/dist/mlb-REVGL3N2.js +665 -0
- package/dist/nba-ATT775GS.js +485 -0
- package/dist/nba-LKM4UHVU.js +482 -0
- package/dist/nba-YX7PXM3R.js +35 -0
- package/dist/nbagl-3WGZCZ4S.js +37 -0
- package/dist/nbagl-LP4XJRAL.js +229 -0
- package/dist/nbagl-VTTOU7LN.js +499 -0
- package/dist/ncaa_baseball-UV75LNVR.js +2670 -0
- package/dist/ncaa_baseball-VD5J5C72.js +419 -0
- package/dist/ncaa_baseball-YRBS32D6.js +4592 -0
- package/dist/ncaa_mhockey-IMYHS7QS.js +1033 -0
- package/dist/ncaa_mhockey-TXTKWHE3.js +115 -0
- package/dist/ncaa_mhockey-U3MFW5VT.js +663 -0
- package/dist/ncaa_softball-6WR6KS64.js +420 -0
- package/dist/ncaa_softball-RZCTIE56.js +2654 -0
- package/dist/ncaa_softball-VOD4PVGY.js +4599 -0
- package/dist/ncaa_whockey-6XXGUTBK.js +607 -0
- package/dist/ncaa_whockey-AKKYKY67.js +53 -0
- package/dist/ncaa_whockey-KBEKXFBB.js +293 -0
- package/dist/nfl-BDB2HYAR.js +625 -0
- package/dist/nfl-EHYHDLJD.js +37 -0
- package/dist/nfl-VF4NJCHY.js +514 -0
- package/dist/nfl_gsis-UY64JO37.js +4 -0
- package/dist/nhl-6WPHME7H.js +511 -0
- package/dist/nhl-EMIJFZHY.js +884 -0
- package/dist/nhl-ISFUKRFU.js +37 -0
- package/dist/ohl-2FEM554G.js +113 -0
- package/dist/ohl-C5Q4W6JB.js +32 -0
- package/dist/ohl-TQ4J46VC.js +66 -0
- package/dist/phf-H7POU2KW.js +32 -0
- package/dist/phf-K5IFMZUM.js +13 -0
- package/dist/phf-TWJI3BOW.js +20 -0
- package/dist/plot.d.ts +449 -0
- package/dist/plot.js +711 -0
- package/dist/plot.js.map +1 -0
- package/dist/plotly.d.ts +129 -0
- package/dist/plotly.js +360 -0
- package/dist/plotly.js.map +1 -0
- package/dist/pwhl-OX3BO4JI.js +53 -0
- package/dist/pwhl-W44OC3C6.js +17 -0
- package/dist/pwhl-ZJGG3H7W.js +23 -0
- package/dist/qmjhl-5HISEHLL.js +35 -0
- package/dist/qmjhl-DA3UHKE5.js +125 -0
- package/dist/qmjhl-YG4BOQPF.js +614 -0
- package/dist/react.d.ts +101 -0
- package/dist/react.js +164 -0
- package/dist/react.js.map +1 -0
- package/dist/resolve-CQW-jzvr.d.ts +28 -0
- package/dist/selection-D4T3PkRt.d.ts +140 -0
- package/dist/shots.d.ts +364 -0
- package/dist/shots.js +85 -0
- package/dist/shots.js.map +1 -0
- package/dist/soccer-7BH3ZLVL.js +7898 -0
- package/dist/soccer-MRU25OGE.js +2636 -0
- package/dist/soccer-PF4IPPVA.js +12342 -0
- package/dist/surfaces-ChaD9nei.d.ts +38 -0
- package/dist/testing.d.ts +75 -0
- package/dist/testing.js +240 -0
- package/dist/testing.js.map +1 -0
- package/dist/tiers-B6pUBPcE.d.ts +93 -0
- package/dist/types-C7n6gJF3.d.ts +62 -0
- package/dist/ufl-7VLZTLTT.js +143 -0
- package/dist/ufl-CCMBLPSW.js +100 -0
- package/dist/ufl-F2BJRXME.js +16 -0
- package/dist/usfl-4CR5RMZJ.js +32 -0
- package/dist/usfl-LD7HP2YK.js +14 -0
- package/dist/usfl-VU4YSJQD.js +19 -0
- package/dist/ushl-CPG2LB4L.js +73 -0
- package/dist/ushl-PQL6HJAD.js +88 -0
- package/dist/ushl-ZT2ATPHK.js +22 -0
- package/dist/vega.d.ts +85 -0
- package/dist/vega.js +362 -0
- package/dist/vega.js.map +1 -0
- package/dist/wbb-3KHCDYTK.js +5087 -0
- package/dist/wbb-BMX53Q3Q.js +369 -0
- package/dist/wbb-QNRN2CJJ.js +3620 -0
- package/dist/whl-4ATYZWUB.js +97 -0
- package/dist/whl-RVFPL6MH.js +79 -0
- package/dist/whl-Y7ASKYMM.js +28 -0
- package/dist/wnba-FWX5SFVI.js +232 -0
- package/dist/wnba-GX6HZR4B.js +220 -0
- package/dist/wnba-NTMWIPP6.js +20 -0
- package/dist/xfl-43R2JMJK.js +30 -0
- package/dist/xfl-H3BXQPZZ.js +16 -0
- package/dist/xfl-XON7A7FI.js +69 -0
- package/package.json +154 -3
|
@@ -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 };
|