@sportsdataverse/sdvtables 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +4 -0
- package/README.md +519 -2
- package/data/table_themes.json +12136 -0
- package/data/table_themes.json.sha256 +1 -0
- package/dist/aggrid.d.ts +104 -0
- package/dist/aggrid.js +230 -0
- package/dist/aggrid.js.map +1 -0
- package/dist/chunk-3TJR7TKS.js +223 -0
- package/dist/chunk-3TJR7TKS.js.map +1 -0
- package/dist/chunk-R375P3MN.js +2315 -0
- package/dist/chunk-R375P3MN.js.map +1 -0
- package/dist/chunk-UTOVFIFT.js +15176 -0
- package/dist/chunk-UTOVFIFT.js.map +1 -0
- package/dist/chunk-VLF2S6VW.js +181 -0
- package/dist/chunk-VLF2S6VW.js.map +1 -0
- package/dist/chunk-WBTFIGYK.js +189 -0
- package/dist/chunk-WBTFIGYK.js.map +1 -0
- package/dist/engine-BFIPJUlC.d.ts +190 -0
- package/dist/export.d.ts +88 -0
- package/dist/export.js +310 -0
- package/dist/export.js.map +1 -0
- package/dist/html.d.ts +333 -0
- package/dist/html.js +6 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +264 -0
- package/dist/index.js +141 -0
- package/dist/index.js.map +1 -0
- package/dist/mui.d.ts +134 -0
- package/dist/mui.js +193 -0
- package/dist/mui.js.map +1 -0
- package/dist/react.d.ts +72 -0
- package/dist/react.js +184 -0
- package/dist/react.js.map +1 -0
- package/dist/spec-A88vJX5A.d.ts +748 -0
- package/dist/tanstack-table.d.ts +113 -0
- package/dist/tanstack-table.js +98 -0
- package/dist/tanstack-table.js.map +1 -0
- package/dist/tokens-CHwBt9gs.d.ts +75 -0
- package/package.json +123 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SportsDataverse
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Notices
|
|
2
|
+
|
|
3
|
+
- `@sportsdataverse/sdvtables` ports the `great_tables` helpers of **sdvplot** (Python, MIT) and the `gt_*` table functions of **sdvplotR** (MIT). Team logos, headshots and colors come from `@sportsdataverse/sdvplot`.
|
|
4
|
+
- The social icons in `src/html/social-icons.ts` are Font Awesome Free 6 paths (https://fontawesome.com), licensed CC BY 4.0.
|
package/README.md
CHANGED
|
@@ -1,3 +1,520 @@
|
|
|
1
|
-
#
|
|
1
|
+
# **@sportsdataverse/sdvtables** <a href='https://plot.sportsdataverse.org/'><img src='https://raw.githubusercontent.com/sportsdataverse/sdvplot-js/main/docs/static/img/sdvplot-js-logo.png' align="right" width="25%" min-width="120px" alt="sdvplot-js hex logo" /></a>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<!-- badges: start -->
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@sportsdataverse/sdvtables)
|
|
6
|
+
[](https://www.npmjs.com/package/@sportsdataverse/sdvtables)
|
|
7
|
+
[](https://www.npmjs.com/package/@sportsdataverse/sdvtables)
|
|
8
|
+
[](https://nodejs.org/)
|
|
9
|
+
[](https://www.npmjs.com/package/@sportsdataverse/sdvtables?activeTab=code)
|
|
10
|
+
[](https://github.com/sportsdataverse/sdvplot-js/actions/workflows/ci.yml)
|
|
11
|
+
[](https://github.com/sportsdataverse/sdvplot-js/actions/workflows/release.yml)
|
|
12
|
+
[](https://plot.sportsdataverse.org)
|
|
13
|
+
[](https://lifecycle.r-lib.org/articles/stages.html#experimental)
|
|
14
|
+
[](https://github.com/sportsdataverse/sdvplot-js/blob/main/LICENSE)
|
|
15
|
+
[](https://github.com/sportsdataverse/sdvplot-js/graphs/contributors)
|
|
16
|
+
[](https://x.com/SportsDataverse)
|
|
17
|
+
|
|
18
|
+
<!-- badges: end -->
|
|
19
|
+
|
|
20
|
+
Serializable table specs rendered to HTML (static, or interactive with a headless engine) and to PNG, with
|
|
21
|
+
SportsDataverse team identity and themes. A `TableSpec` is plain
|
|
22
|
+
data (JSON-safe), so the same spec renders in Node, the browser and notebooks. Zero runtime dependencies beyond
|
|
23
|
+
[`@sportsdataverse/sdvplot`](https://plot.sportsdataverse.org) (team logos, headshots and colors).
|
|
24
|
+
|
|
25
|
+
Siblings: [sdvplot](https://sdvplot.sportsdataverse.org/) (Python, `great_tables` helpers) and
|
|
26
|
+
[sdvplotR](https://sdvplotR.sportsdataverse.org/) (R, `gt_*`). This package ports both; the name map is
|
|
27
|
+
[below](#gtutils--sdvplotr-names).
|
|
28
|
+
|
|
29
|
+
Part of [sdvplot-js](https://github.com/sportsdataverse/sdvplot-js#readme) · [Documentation](https://plot.sportsdataverse.org) · [API reference](https://plot.sportsdataverse.org/api/sdvtables/) ·
|
|
30
|
+
[Gallery](https://plot.sportsdataverse.org/gallery/sdvtables/) · [Notebooks](https://plot.sportsdataverse.org/notebooks/) · [Source](https://github.com/sportsdataverse/sdvplot-js/tree/main/packages/sdvtables)
|
|
31
|
+
|
|
32
|
+
## **Installation**
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npm install @sportsdataverse/sdvtables @sportsdataverse/sdvplot
|
|
36
|
+
# or
|
|
37
|
+
pnpm add @sportsdataverse/sdvtables @sportsdataverse/sdvplot
|
|
38
|
+
# or
|
|
39
|
+
yarn add @sportsdataverse/sdvtables @sportsdataverse/sdvplot
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
ESM only, Node >= 20.18.1. `@sportsdataverse/sdvplot` is a required peer (npm 7+ and pnpm install it for you).
|
|
43
|
+
|
|
44
|
+
## **Quick start**
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { defineTable } from "@sportsdataverse/sdvtables";
|
|
48
|
+
import { prepare, renderHTML, toElement } from "@sportsdataverse/sdvtables/html";
|
|
49
|
+
|
|
50
|
+
const spec = defineTable<{ team: string; wins: number }>()
|
|
51
|
+
.columns((c) => [c.logo("team", { league: "nfl", includeName: true }), c.int("wins")])
|
|
52
|
+
.theme("midnight")
|
|
53
|
+
.title("AFC West")
|
|
54
|
+
.build();
|
|
55
|
+
const rows = [
|
|
56
|
+
{ team: "KC", wins: 15 },
|
|
57
|
+
{ team: "LAC", wins: 11 },
|
|
58
|
+
{ team: "DEN", wins: 10 },
|
|
59
|
+
{ team: "LV", wins: 4 },
|
|
60
|
+
]; // the 2024 AFC West
|
|
61
|
+
|
|
62
|
+
await prepare(spec); // loads leagues, team names and gsis ids the spec needs
|
|
63
|
+
const html = renderHTML(spec, rows); // string (Node, SSR, notebooks)
|
|
64
|
+
document.body.append(toElement(spec, rows)); // browser; Observable: html`${renderHTML(spec, rows)}`
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`prepare(spec)` must be awaited before `renderHTML`. `renderHTMLAsync` does both.
|
|
68
|
+
|
|
69
|
+
## **Hosts and CSS**
|
|
70
|
+
|
|
71
|
+
Every themed value is a `--sdvt-*` custom property on the table wrapper, and each element carries at most one
|
|
72
|
+
`style` attribute. By default `renderHTML` inlines a scoped `<style>` block. A page that renders many tables should
|
|
73
|
+
emit the stylesheet once per theme and render the tables with `css: "none"`:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { renderHTML, styleSheet, themeKey } from "@sportsdataverse/sdvtables/html";
|
|
77
|
+
const seen = new Set<string>();
|
|
78
|
+
const parts = specs.map((spec) => {
|
|
79
|
+
const key = themeKey(spec.theme);
|
|
80
|
+
const sheet = seen.has(key) ? "" : `<style>${styleSheet(spec)}</style>`; // once per themeKey
|
|
81
|
+
seen.add(key);
|
|
82
|
+
return sheet + renderHTML(spec, rows, { css: "none" });
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`css: "none"` drops only the shared theme sheet. Each table still carries its own `#id`-scoped decoration `<style>`
|
|
87
|
+
and the Google Fonts `<link>` for its theme and `.font()` faces; pass `fonts: false` to drop the link and load the
|
|
88
|
+
fonts yourself (`fontsLink(fonts)` builds the same tag). `toElement` moves the link into `document.head`, once per URL.
|
|
89
|
+
|
|
90
|
+
Decoration CSS is scoped to the table `#id`. Two renders of the same spec on one page therefore need distinct ids:
|
|
91
|
+
call `.id("a")` / `.id("b")` on the builder.
|
|
92
|
+
|
|
93
|
+
## **Themes**
|
|
94
|
+
|
|
95
|
+
20 themes: the 18 gtUtils themes plus `sdv` (light/dark) and `sdvTeam`. Set one with
|
|
96
|
+
`.theme(name, { density, options })`.
|
|
97
|
+
|
|
98
|
+
| Theme | Options | Default density |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `sdv` | `style` (`light`/`dark`) | comfortable |
|
|
101
|
+
| `sdvTeam` | `league` (required), `team` | comfortable |
|
|
102
|
+
| `almanac` | `accent`, `stripe` (`"none"` for no banding) | compact |
|
|
103
|
+
| `broadsheet` | `accent`, `paper` | comfortable |
|
|
104
|
+
| `drench` | `color` | comfortable |
|
|
105
|
+
| `sofa`, `tier` | `style` | comfortable |
|
|
106
|
+
| `scoreboard`, `terminal` | `accent` | compact |
|
|
107
|
+
| `booktabs`, `brutalist`, `midnight`, `swiss`, `tufte` | `accent` | comfortable |
|
|
108
|
+
| `athletic`, `gtutils`, `kenpom`, `ncaa`, `pl`, `savant` | none | comfortable |
|
|
109
|
+
|
|
110
|
+
| Density | body | padding | title | subtitle | label | group | source |
|
|
111
|
+
|---|---|---|---|---|---|---|---|
|
|
112
|
+
| `comfortable` | 14 | 6 | 26 | 15 | 10 | 11 | 11 |
|
|
113
|
+
| `compact` | 12 | 3 | 22 | 13 | 9 | 10 | 10 |
|
|
114
|
+
| `social` | 17 | 9 | 34 | 19 | 12 | 13 | 13 |
|
|
115
|
+
|
|
116
|
+
`themePreview(spec, rows, themes?, { n, density })` from `@sportsdataverse/sdvtables/html` returns
|
|
117
|
+
`Record<themeName, html>` (first `n` rows, compact).
|
|
118
|
+
|
|
119
|
+
## **Surface**
|
|
120
|
+
|
|
121
|
+
- 29 column kinds on the `c` factory (`text`, `num`, `int`, `pct`, `rank`, `delta`, `tally`, `logo`, `wordmark`,
|
|
122
|
+
`headshot`, `colorPills`, `colorRanks`, `colorResults`, `percentileBar`, `indicatorBox`, `highlight`,
|
|
123
|
+
`highlightNa`, `mergeStackTeamColor`, `teamColorBar`, `teamColorBg`, `image`, and the reactablefmtr formatters
|
|
124
|
+
`dataBar`, `iconSet`, `iconAssign`, `mergeStack`, `bubble`, and the in-cell charts `sparkline`, `sparkbar`, `gauge`
|
|
125
|
+
(inline SVG, array-valued columns for the first two); `fmtRank`/`fmtTally` alias `rank`/`tally`).
|
|
126
|
+
Any column takes `tooltip: true` (the value as a `title` attribute).
|
|
127
|
+
- 25 table decorations as builder methods (titles, legends, cutlines, row accents, snake layout, tiers, watermark, ...).
|
|
128
|
+
- Conditions are data predicates (`{ key, op, value }`), not callbacks, so specs stay serializable.
|
|
129
|
+
- Number cells display negatives with U+2212 (a true minus); `formatValue`-style labels (legends, notes) stay ASCII.
|
|
130
|
+
- Unknown team ids warn once per call per set and render as plain text.
|
|
131
|
+
|
|
132
|
+
## **Interactive**
|
|
133
|
+
|
|
134
|
+
`createTable(spec, rows, options)` is the package's own headless engine (no table library): sort, per-column and
|
|
135
|
+
global text filters, paging, column visibility and row selection, with immutable snapshots and `subscribe`. Every
|
|
136
|
+
column sorts unless `sortable: false`, missing values sort last in both directions, and a column's `compare(a, b)`
|
|
137
|
+
replaces the kind's comparator. `filterable: true` gives a column its own filter box; the search box searches every
|
|
138
|
+
shown column. The page size is `options.pageSize`, else `spec.interactive.pageSize`, else one page.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { createTable, defineTable } from "@sportsdataverse/sdvtables";
|
|
142
|
+
import { hydrate, prepare, renderHTML } from "@sportsdataverse/sdvtables/html";
|
|
143
|
+
|
|
144
|
+
// 2024 AFC regular season (nflverse games)
|
|
145
|
+
const rows = [
|
|
146
|
+
{ team: "KC", division: "West", wins: 15, losses: 2, pf: 385, pa: 326 },
|
|
147
|
+
{ team: "LAC", division: "West", wins: 11, losses: 6, pf: 402, pa: 301 },
|
|
148
|
+
{ team: "DEN", division: "West", wins: 10, losses: 7, pf: 425, pa: 311 },
|
|
149
|
+
{ team: "LV", division: "West", wins: 4, losses: 13, pf: 309, pa: 434 },
|
|
150
|
+
{ team: "BUF", division: "East", wins: 13, losses: 4, pf: 525, pa: 368 },
|
|
151
|
+
{ team: "MIA", division: "East", wins: 8, losses: 9, pf: 345, pa: 364 },
|
|
152
|
+
{ team: "NYJ", division: "East", wins: 5, losses: 12, pf: 338, pa: 404 },
|
|
153
|
+
{ team: "NE", division: "East", wins: 4, losses: 13, pf: 289, pa: 417 },
|
|
154
|
+
];
|
|
155
|
+
type Row = (typeof rows)[number];
|
|
156
|
+
const spec = defineTable<Row>()
|
|
157
|
+
.columns((c) => [
|
|
158
|
+
c.logo("team", { league: "nfl", includeName: true }),
|
|
159
|
+
c.text("division", { filterable: true }),
|
|
160
|
+
c.int("wins"),
|
|
161
|
+
c.int("losses"),
|
|
162
|
+
c.int("pf", { label: "PF" }),
|
|
163
|
+
c.int("pa", { label: "PA" }),
|
|
164
|
+
])
|
|
165
|
+
.title("AFC")
|
|
166
|
+
.subtitle("2024 regular season")
|
|
167
|
+
.rowKey("team")
|
|
168
|
+
.build();
|
|
169
|
+
|
|
170
|
+
await prepare(spec);
|
|
171
|
+
const table = createTable(spec, rows, { pageSize: 4, sort: { col: "wins", dir: "desc" } });
|
|
172
|
+
const host = document.querySelector("#standings")!;
|
|
173
|
+
host.innerHTML = renderHTML(table); // the same string renders on a server or a static site
|
|
174
|
+
const stop = hydrate(host.querySelector(".sdvt")!, table); // sort, filter, page and select without React
|
|
175
|
+
table.setFilter("division", "east"); // BUF, MIA, NYJ, NE; back to the first page
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`renderHTML(table)` draws sort buttons in the headers (`aria-sort` on the sorted one), a search box and the filter
|
|
179
|
+
boxes above the table, and a pager below it. `hydrate(el, table)` attaches delegated listeners and re-renders only the
|
|
180
|
+
table block and the pager, once, on the next animation frame, however many changes land in it; it returns a teardown.
|
|
181
|
+
A click, pointer move or key that arrives while a change to the rows (a sort, filter, page turn or `setRows`) waits for
|
|
182
|
+
that frame draws it first, so the event never acts on an old row at a new row's index; Enter or Space on a row that
|
|
183
|
+
draw replaced is consumed, so the page does not scroll. A change that keeps the rows (a selection, or a live feed
|
|
184
|
+
re-sending a keyed row as a new object) waits for its frame. Render the markup from the same table state you hydrate:
|
|
185
|
+
attaching does not reconcile the two. Hydrating an element again draws any change its previous binding still owed. A
|
|
186
|
+
teardown drops a render still owed; hydrating that element again redraws it first, so React StrictMode's mount,
|
|
187
|
+
cleanup, mount leaves no stale rows.
|
|
188
|
+
|
|
189
|
+
Inside an interactive table: j/k (↓/↑) move between the rows of the page and never turn it (the pager does), h/l
|
|
190
|
+
(←/→) pick a column, s sorts it, / jumps to the search box, Enter or Space toggles the row's selection;
|
|
191
|
+
`interactive: { hotkeys: false }` on the spec keeps only the arrows, Enter and Space. The table is a `role="grid"`
|
|
192
|
+
whose rows share one tab stop, the current row; the current column's header carries
|
|
193
|
+
`aria-current="true"` and a selected row `aria-selected="true"`. Keys pressed with Ctrl, Meta or Alt, during IME
|
|
194
|
+
composition, or in a text field are left alone. Clicking a row toggles its selection too.
|
|
195
|
+
|
|
196
|
+
In an interactive table, colour scales, legends, outlier limits and row-accent palettes are computed from all of the
|
|
197
|
+
table's rows, not the page on screen, so a value keeps its colour while the user pages, filters or sorts. Row
|
|
198
|
+
selectors by index (`.boldRows([0])`) count in the rows passed to `createTable`. `snake` layouts are static only: an
|
|
199
|
+
interactive render of a `.snake()` spec throws `TableSpecError`.
|
|
200
|
+
|
|
201
|
+
Linking a table to a figure: with `.rowKey("team")`, a row's id is `String(row.team)` (without one, its index in the
|
|
202
|
+
input rows). `setSelection(ids)` / `getSelection()`, `setExternalFilter((row) => …)` (ANDed with the table's own
|
|
203
|
+
filters) and `setHover(id)` drive the table from outside, and `subscribe` reports `select`, `hover` and `change`
|
|
204
|
+
events. Setting the same selection or filter again notifies nobody, so a two-way link does not loop.
|
|
205
|
+
`@sportsdataverse/sdvplot/interact`'s `linkSelection` does this wiring against a selection store.
|
|
206
|
+
|
|
207
|
+
Every interactive table, linked or not, underlines the row under the pointer, or the row a linked figure hovers
|
|
208
|
+
(`setHover`): the row gets the `sdvt-hover` class, which re-renders nothing and survives the next re-render. A table
|
|
209
|
+
holds one hover id, so a linked table lights the first id the figure hovers. `getHover()` returns it, so a view
|
|
210
|
+
attached later (a remount, a second hydrate) shows it. Unmounting while the pointer is on a row leaves that hover in
|
|
211
|
+
place, because a removed row fires no `mouseleave`. reactable's `highlight` defaults to `FALSE`; to drop the underline,
|
|
212
|
+
add `tr.sdvt-hover>td.sdvt-cell{background-image:none}` to the page. The built-in rule is wrapped in `:where()`, so
|
|
213
|
+
this one plain rule wins wherever the page puts it. A selection or brush from a linked figure
|
|
214
|
+
rebuilds the table body, so a scroll position inside a cell resets.
|
|
215
|
+
|
|
216
|
+
Engine non-goals: virtualization, column resize/reorder/pin, a grouping UI and server-side paging (slice the rows
|
|
217
|
+
before `createTable`).
|
|
218
|
+
|
|
219
|
+
### React
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
import { fontsLinkFor } from "@sportsdataverse/sdvtables/html";
|
|
223
|
+
import { SdvTable, useTable } from "@sportsdataverse/sdvtables/react"; // peer: react >= 18
|
|
224
|
+
|
|
225
|
+
// spec and rows from above; await prepare(spec) before the first render, server included
|
|
226
|
+
<SdvTable spec={spec} rows={rows} interactive pageSize={4} />; // SSR markup === renderHTML(table, { fonts: false })
|
|
227
|
+
|
|
228
|
+
function Standings() {
|
|
229
|
+
const { table, snapshot } = useTable(spec, rows, { pageSize: 4 });
|
|
230
|
+
return (
|
|
231
|
+
<>
|
|
232
|
+
<p>{snapshot.filteredCount} teams</p>
|
|
233
|
+
<SdvTable table={table} interactive />
|
|
234
|
+
</>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`<SdvTable/>` writes no fonts `<link>`: put the tag `fontsLinkFor(spec)` returns in the page `<head>`. `pageSize` and
|
|
240
|
+
`sort` are initial state, like React's `default*` props. A new `rows` array, even an equal copy, keeps the user's
|
|
241
|
+
sort, filters, page and selection; only a structural `spec` change builds a new engine. Without `interactive` the
|
|
242
|
+
component renders the static table. The `react` entry is marked `"use client"`.
|
|
243
|
+
|
|
244
|
+
### MUI X Data Grid
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
import { DataGrid } from "@mui/x-data-grid"; // peer: @mui/x-data-grid >= 9.15.0 (MIT; never -pro or -premium)
|
|
248
|
+
import { prepare } from "@sportsdataverse/sdvtables/html";
|
|
249
|
+
import { dataGridProps } from "@sportsdataverse/sdvtables/mui";
|
|
250
|
+
|
|
251
|
+
await prepare(spec);
|
|
252
|
+
<DataGrid rows={rows} {...dataGridProps(spec, rows, { teamRows: { key: "team", league: "nfl" } })} autoHeight />;
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`dataGridProps(spec, rows, o)` maps a spec onto MUI X Data Grid: one `GridColDef` per column (label, alignment, px
|
|
256
|
+
width, sdvtables' number formats through `valueFormatter`), logo, wordmark and headshot cells through `renderCell`,
|
|
257
|
+
`teamColorBg` / `teamColorBar` cells and `teamRows` in sdvplot's team palette (`@sportsdataverse/sdvplot/mui`'s
|
|
258
|
+
`teamColorPalette`) with black or white text by contrast, and the theme's fonts, colours, stripes and density as the
|
|
259
|
+
grid's `sx` and `density`. It never imports MUI. Kinds drawn in HTML or SVG (pills, bars, sparklines...) show their
|
|
260
|
+
plain value, with one warning per kind. Guide: [MUI X Data Grid](https://plot.sportsdataverse.org/guides/mui-x-data-grid).
|
|
261
|
+
|
|
262
|
+
### AG Grid
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import { AllCommunityModule, ModuleRegistry, createGrid, themeQuartz } from "ag-grid-community"; // peer: >= 36.2.0 (MIT)
|
|
266
|
+
import { gridOptions } from "@sportsdataverse/sdvtables/aggrid";
|
|
267
|
+
import { prepare } from "@sportsdataverse/sdvtables/html";
|
|
268
|
+
|
|
269
|
+
ModuleRegistry.registerModules([AllCommunityModule]);
|
|
270
|
+
await prepare(spec);
|
|
271
|
+
const el = document.querySelector("#grid")!; // AG Grid fills its container: size it, or use domLayout: "autoHeight"
|
|
272
|
+
createGrid(el, {
|
|
273
|
+
...gridOptions(spec, rows, { theme: themeQuartz, teamRows: { key: "team", league: "nfl" } }),
|
|
274
|
+
domLayout: "autoHeight",
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`gridOptions(spec, rows, o)` maps a spec onto AG Grid Community: one column definition per column (label, alignment,
|
|
279
|
+
px width, sdvtables' number formats through `valueFormatter`, sdvtables' sort with missing values last both ways),
|
|
280
|
+
logo, wordmark and headshot cells through cell renderer classes that work in plain JavaScript and in ag-grid-react,
|
|
281
|
+
`teamColorBg` / `teamColorBar` cells and `teamRows` in sdvplot's team palette with black or white text by contrast
|
|
282
|
+
(kept through AG Grid's hover and selection), unique row ids, and, given AG Grid's `themeQuartz` (or another base), the
|
|
283
|
+
theme's fonts and colours through AG Grid's Theming API. It never imports AG Grid at runtime. Community only: nothing
|
|
284
|
+
here uses or needs `ag-grid-enterprise`. Guide: [AG Grid](https://plot.sportsdataverse.org/guides/ag-grid).
|
|
285
|
+
|
|
286
|
+
### TanStack Table v9
|
|
287
|
+
|
|
288
|
+
`@sportsdataverse/sdvtables/tanstack-table` turns a spec into TanStack Table v9 options:
|
|
289
|
+
`tanstackTable(spec, rows, { features, render })` returns `options` for React's `useTable` or Svelte's `createTable`
|
|
290
|
+
(`features` is your own `tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel() })`, passed through)
|
|
291
|
+
and `columnStyle`, each column's alignment and width. Columns keep the HTML table's labels and number text (a
|
|
292
|
+
`scaleNote` and ordinals included), sort with sdvtables' comparator with missing values last both ways, and take their
|
|
293
|
+
row ids from the spec's `rowKey` (a repeated or blank value throws). Logo, wordmark, headshot and superscript-rank cells
|
|
294
|
+
come to your `render` as plain data (`CellPart`), which you turn into a React element or a Svelte component; a team or
|
|
295
|
+
player with no image keeps its text and warns once. It imports table-core's types only and touches no DOM, so it runs on
|
|
296
|
+
a server. Peer: `@tanstack/table-core` >= 9.3.1, optional (its types are in the declarations, so TypeScript needs it
|
|
297
|
+
installed). Call `await prepare(spec)` first.
|
|
298
|
+
|
|
299
|
+
The guide, [TanStack Table](https://plot.sportsdataverse.org/guides/tanstack-table), shows a sortable React component
|
|
300
|
+
and a Svelte 5 one (`examples/snippets/tanstack-table/SdvTable.tsx` and `examples/snippets/svelte/TanstackStandings.svelte`
|
|
301
|
+
in this repository), both compiled and rendered by the tests.
|
|
302
|
+
|
|
303
|
+
**From the v8 recipe.** This README used to carry an untested recipe for `@tanstack/react-table` 8 (`toTanStackColumns`,
|
|
304
|
+
reading each cell's markup back from `toElement()`). TanStack Table v9 changed its API: `useTable` replaces
|
|
305
|
+
`useReactTable`, features and row models are registered in `tableFeatures` (`createSortedRowModel()` where v8 took
|
|
306
|
+
`getSortedRowModel()`), `sortingFn` is `sortFn`, and `ColumnDef` takes the features' type first. Use `tanstackTable`
|
|
307
|
+
instead; it needs no DOM, so the columns no longer wait for a mount.
|
|
308
|
+
|
|
309
|
+
## **Export (Node; peer: playwright)**
|
|
310
|
+
|
|
311
|
+
`@sportsdataverse/sdvtables/export` renders tables to PNG in headless Chromium. Its functions port sdvplotR's
|
|
312
|
+
`gt_save_crop`, `gt_social_crop`, `gt_save_batch`, `gt_grid` and `gt_stack_tables`, with the Python package's
|
|
313
|
+
defaults. Install the optional peer and its browser once:
|
|
314
|
+
|
|
315
|
+
```sh
|
|
316
|
+
pnpm add -D playwright
|
|
317
|
+
pnpm exec playwright install chromium
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Without `playwright` the functions throw `OptionalDependencyError` (from `@sportsdataverse/sdvplot`), and a missing
|
|
321
|
+
browser says to run `playwright install chromium`. Arguments are checked before any browser starts.
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
import { batchToPNG, gridTables, htmlToPNG, socialCrop, stackTables, tableToPNG } from "@sportsdataverse/sdvtables/export";
|
|
325
|
+
|
|
326
|
+
// spec and rows from the Interactive example; await prepare(spec) first
|
|
327
|
+
await tableToPNG(spec, rows, { file: "afc.png" }); // gt_save_crop
|
|
328
|
+
await socialCrop(spec, rows, { aspect: "4:5", file: "afc-post.png" }); // gt_social_crop
|
|
329
|
+
const west = { spec, rows: rows.filter((r) => r.division === "West") };
|
|
330
|
+
const east = { spec, rows: rows.filter((r) => r.division === "East") };
|
|
331
|
+
await htmlToPNG(gridTables([west, east], { ncol: 2, title: "AFC by division" }), { file: "grid.png" }); // gt_grid
|
|
332
|
+
await htmlToPNG(stackTables([west, east], { title: "AFC by division" }), { file: "stack.png" }); // gt_stack_tables
|
|
333
|
+
await batchToPNG(rows, "division", (groupRows) => ({ spec, rows: groupRows }), "afc-{group}.png", { dir: "out" });
|
|
334
|
+
// gt_save_batch: out/afc-west.png and out/afc-east.png
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
- `tableToPNG`, `socialCrop` and `htmlToPNG` return the PNG bytes and also write `file` when given (it must end in
|
|
338
|
+
`.png`); `batchToPNG` returns the paths it wrote.
|
|
339
|
+
- An image past Chromium's canvas limit (65,535 px a side, 268,435,456 px in all: near 1,000 plain rows at the
|
|
340
|
+
default zoom) throws `SdvplotError` naming its size, and nothing is written. Put fewer rows in each image
|
|
341
|
+
(`rows.slice()` a page at a time, `batchToPNG` by a group column, `gridTables` side by side) or lower
|
|
342
|
+
`deviceScaleFactor`.
|
|
343
|
+
- Options (`PNGOptions`; `SocialCropOptions` and `BatchOptions` build on it) are in image pixels, as in Python:
|
|
344
|
+
`deviceScaleFactor` (the zoom, default 2), `whitespace` (the margin left around the trimmed content, default 50, or
|
|
345
|
+
60 for `socialCrop`), `background` (default white), `width` (the final image width; absent, the rendered width) and
|
|
346
|
+
`fontLinks` (extra stylesheets to wait for).
|
|
347
|
+
- `socialCrop` puts the table on an `aspect` canvas (`"1:1"` by default, `"16:9"`, `"4:5"`, `"9:16"`, `"1.91:1"` or
|
|
348
|
+
a number), placed by `gravity` (one of the nine ImageMagick names, `"center"` by default); the table is never cropped.
|
|
349
|
+
- `batchToPNG` writes one image per value of the group column, with `{group}` replaced by `slug(value)`, all widened
|
|
350
|
+
to the widest (`matchWidth: false` keeps each width). A group whose table fails to build is skipped and named in one
|
|
351
|
+
warning, as in Python.
|
|
352
|
+
- `gridTables` and `stackTables` return HTML: the tables with a page title, subtitle, caption and source note around
|
|
353
|
+
them, styled with the Python defaults. A spec used twice gets distinct ids, so each copy keeps its decorations.
|
|
354
|
+
- For an SVG figure, use `toPNG` and `socialCard` from `@sportsdataverse/sdvplot/export` instead (no browser).
|
|
355
|
+
|
|
356
|
+
## **gtUtils / sdvplotR names**
|
|
357
|
+
|
|
358
|
+
All 74 sdvplotR `gt_*` / `pal_*` / `reactable_sdv_*` exports map as below (`GT_ALIASES` and `aliasFor(name)` expose
|
|
359
|
+
the same table in code). Builder methods are on `defineTable()`, `c.*` inside `.columns(c => [...])`.
|
|
360
|
+
|
|
361
|
+
| sdvplotR / gtUtils | sdvtables | status |
|
|
362
|
+
|---|---|---|
|
|
363
|
+
| `gt_sdv_logos` | `c.logo(key, {league, includeName, season, variant, height})` | ported |
|
|
364
|
+
| `gt_sdv_wordmarks` | `c.wordmark(key, {league, season, variant, height})` | ported |
|
|
365
|
+
| `gt_sdv_headshots` | `c.headshot(key, {league, idSystem, height})` | ported |
|
|
366
|
+
| `gt_sdv_cols_label` | image labels are not ported; use `.titleHeader()` or `subheader` | not ported (labels are text) |
|
|
367
|
+
| `gt_merge_stack_team_color` | `c.mergeStackTeamColor(top, stack, team, {league, fontSizeTop, fontSizeBottom, color, background})` | ported |
|
|
368
|
+
| `gt_theme_sdv` / `gt_theme_sdv_team` | `.theme("sdv", {options:{style}})` / `.theme("sdvTeam", {options:{league, team}})` | ported |
|
|
369
|
+
| `gt_theme_<name>` (18) | `.theme("<name>", {density, options:{accent\|stripe\|paper\|color\|style}})` | ported |
|
|
370
|
+
| `gt_theme_preview` | `themePreview(spec, rows, themes?, {n, density})` from `/html` | ported (dict of HTML, not a grid) |
|
|
371
|
+
| `pal_midnight` | `PAL_MIDNIGHT` (`["#5B8DEF","#3FBF87","#E8E9ED","#9498A3","#24272E"]`) | ported |
|
|
372
|
+
| `gt_538_caption` | `.caption538({top, bottom, ruleColor, ruleWidth, size, align})` | ported |
|
|
373
|
+
| `gt_bold_rows` | `.boldRows(rows, {textColor, highlightColor})` | ported |
|
|
374
|
+
| `gt_border_bars_top/bottom` | `.borderBars("top"\|"bottom", colors, {...})` | ported (bars outside the `<table>`) |
|
|
375
|
+
| `gt_border_grid` | `.borderGrid({color, weight, includeLabels})` | ported |
|
|
376
|
+
| `gt_color_pills` | `c.colorPills(key, {...})` | ported (sRGB ramp, as great_tables) |
|
|
377
|
+
| `gt_color_ranks` | `c.colorRanks(key, {palette, domain, reverse})` | ported |
|
|
378
|
+
| `gt_color_results` | `c.colorResults(key, {...})`: a column kind that fills its row by W/L/T | ported |
|
|
379
|
+
| `gt_column_subheaders` | the `subheader` option of every `c.*` kind | ported |
|
|
380
|
+
| `gt_cutline` | `.cutline(after, {label, color, weight, style, labelColor, labelSize, labelPosition, gap})` | ported |
|
|
381
|
+
| `gt_delta` | `c.delta(from, to, {...})` | ported |
|
|
382
|
+
| `gt_fmt_rank` / `gt_fmt_tally` | `c.rank` / `c.tally` (also `c.fmtRank` / `c.fmtTally`) | ported |
|
|
383
|
+
| `gt_group_stripes` | `.groupBy(key).groupStripes({color, start})` | ported |
|
|
384
|
+
| `gt_highlight_cells` | `c.highlight(key, predicate, {fill, textColor, bold})` | ported (data predicate, not a formula) |
|
|
385
|
+
| `gt_highlight_na` | `c.highlightNa(key, {...})` | ported |
|
|
386
|
+
| `gt_indicator_boxes` | `c.indicatorBox(key, {truthy, fill, neutral, size, showOnly})` | ported (value list, not a rule) |
|
|
387
|
+
| `gt_legend_continuous` / `gt_legend_discrete` (deprecated gtUtils names `gt_color_legend` / `gt_centered_legend`) | `.legendContinuous({...})` / `.legendDiscrete(key\|"recorded", {...})` | ported |
|
|
388
|
+
| `gt_marginalia` | `.marginalia(columns, {...})` | ported |
|
|
389
|
+
| `gt_outliers` | `.outliers(columns, {method, threshold, bounds, side, fill, color, bold, symbol, note})` | ported |
|
|
390
|
+
| `gt_percentile_bar` | `c.percentileBar(key, {...})` | ported |
|
|
391
|
+
| `gt_row_accent` | `.rowAccent(key, {palette, rows, width, side, hide, naColor})` | ported |
|
|
392
|
+
| `gt_scale_note` | `.scaleNote(columns, {divisor, note, where, labelSuffix, decimals})` | ported |
|
|
393
|
+
| `gt_set_font` | `.font(family, {google, weight, style})` | ported |
|
|
394
|
+
| `gt_significance` | `.significance(pairs, {levels, symbols, superscript, note})` | ported |
|
|
395
|
+
| `gt_snake` / `gt_snake_align` | `.snake({nCols, rowsPerCol, gap, fill, cleanGaps})` / `snakeAlign(rows, opts)` | ported |
|
|
396
|
+
| `gt_social_tag` | `.socialTag(accounts, {...})` | ported (inline Font Awesome 6 SVG) |
|
|
397
|
+
| `gt_spotlight` | `.spotlight(rows, {...})` | ported |
|
|
398
|
+
| `gt_tiers` | `.tiers(levels, tierKey, imageColumns, {colors, imgHeight, style})` | ported |
|
|
399
|
+
| `gt_title_header` | `.titleHeader(title, {subtitle, kicker, date, *Style})` | ported |
|
|
400
|
+
| `gt_watermark` | `.watermark({text, image, opacity, size, position, color, angle, font})` | ported |
|
|
401
|
+
| `gt_wrap_labels` | `.wrapLabels({columns, width, balance})` | ported |
|
|
402
|
+
| `gt_save_crop` / `gt_save_batch` | `tableToPNG` / `batchToPNG` (`/export`) | ported |
|
|
403
|
+
| `gt_social_crop` | `socialCrop` (`/export`) | ported |
|
|
404
|
+
| `gt_grid` / `gt_stack_tables` | `gridTables` / `stackTables` (`/export`) | ported |
|
|
405
|
+
| `reactable_sdv_logos/wordmarks/headshots` | `c.logo` / `c.wordmark` / `c.headshot` | ported |
|
|
406
|
+
| `reactable_sdv_cols_label` | as `gt_sdv_cols_label` | not ported |
|
|
407
|
+
| `reactable_sdv_team_color_bar` / `_bg` | `c.teamColorBar(key, {league, which, naColor, barWidth})` / `c.teamColorBg(key, {league, which, alpha, naColor})` | ported |
|
|
408
|
+
|
|
409
|
+
## **Known differences from Python and R**
|
|
410
|
+
|
|
411
|
+
- Indicator boxes take a value list and carry no text; legends draw discrete steps, not a gradient bar.
|
|
412
|
+
- Tier images have an empty `alt`. `tally`'s column-mode share is not ported.
|
|
413
|
+
- `snake` keeps the first block's row styles for every block.
|
|
414
|
+
- A decoration's row fill always beats a kind's own cell background (Python's outcome depends on call order).
|
|
415
|
+
- `socialTag` accepts a fixed alias set (`x`/`twitter`, `ig`, `bsky`, `gh`, `yt`, `fb`, `web`/`website`/`link`,
|
|
416
|
+
`email`/`mail`), not every Font Awesome name; `tiktok`, `threads` and `substack` are unavailable, as in faicons.
|
|
417
|
+
- `kicker` always renders uppercase; `cutline` y positions round half-up.
|
|
418
|
+
- `wrapLabels` keeps internal whitespace of a label as given.
|
|
419
|
+
|
|
420
|
+
## **Visual pass (contributors)**
|
|
421
|
+
|
|
422
|
+
The unit snapshots pin each theme's CSS text, not what it draws. The visual pass renders every registered theme's
|
|
423
|
+
`themePreview` of a 2024 AFC standings table (title, subtitle, division groups, source note) through the `tableToPNG`
|
|
424
|
+
path, one PNG per theme:
|
|
425
|
+
|
|
426
|
+
```sh
|
|
427
|
+
pnpm --filter @sportsdataverse/sdvtables exec playwright install chromium # once
|
|
428
|
+
pnpm --filter @sportsdataverse/sdvtables render:themes # writes packages/sdvtables/theme-pass/<theme>.png
|
|
429
|
+
SDV_RENDER_TESTS=1 pnpm --filter @sportsdataverse/sdvtables test # the unit tests plus the chromium export tests
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Open the PNGs after changing a theme or the base sheet: this pass caught tufte and booktabs running the title and the
|
|
433
|
+
subtitle together on one line. `theme-pass/` is gitignored; `SDV_THEME_PASS_DIR=<dir>` writes elsewhere. An always-on
|
|
434
|
+
test fails if a registered theme is missing from the pass. In CI the `render-tests` job runs the same pass, lists the
|
|
435
|
+
PNGs and any failed theme in the job summary, and uploads them as the `sdvtables-theme-pass` artifact; a failing theme
|
|
436
|
+
never fails the build.
|
|
437
|
+
|
|
438
|
+
## **Documentation**
|
|
439
|
+
|
|
440
|
+
The [**sdvplot-js** documentation website](https://plot.sportsdataverse.org) has the
|
|
441
|
+
[sdvtables gallery](https://plot.sportsdataverse.org/gallery/sdvtables/), the [notebooks](https://plot.sportsdataverse.org/notebooks/) and the
|
|
442
|
+
[sdvtables API reference](https://plot.sportsdataverse.org/api/sdvtables/), one page per subpath, plus:
|
|
443
|
+
|
|
444
|
+
**Guides:**
|
|
445
|
+
[Tables: the spec](https://plot.sportsdataverse.org/guides/tables) ·
|
|
446
|
+
[Table themes](https://plot.sportsdataverse.org/guides/table-themes) ·
|
|
447
|
+
[Cell kinds](https://plot.sportsdataverse.org/guides/cell-kinds) ·
|
|
448
|
+
[Decorations](https://plot.sportsdataverse.org/guides/decorations) ·
|
|
449
|
+
[HTML, SSR and the host page](https://plot.sportsdataverse.org/guides/tables-html) ·
|
|
450
|
+
[Export to PNG](https://plot.sportsdataverse.org/guides/export) ·
|
|
451
|
+
[Node and SSR](https://plot.sportsdataverse.org/guides/node-ssr)
|
|
452
|
+
|
|
453
|
+
**Examples:**
|
|
454
|
+
[Linked figures and tables](https://plot.sportsdataverse.org/examples/linked) ·
|
|
455
|
+
[From sdvplot (Python) and sdvplotR](https://plot.sportsdataverse.org/guides/migrating)
|
|
456
|
+
|
|
457
|
+
The [repository README](https://github.com/sportsdataverse/sdvplot-js#readme) covers all three packages and development.
|
|
458
|
+
|
|
459
|
+
## **Logos, trademarks and data**
|
|
460
|
+
|
|
461
|
+
Team names, logos, wordmarks and player headshots are trademarks or copyrighted works of their respective leagues,
|
|
462
|
+
teams, schools and other rights holders. `@sportsdataverse/sdvtables` is not affiliated with, sponsored by or endorsed
|
|
463
|
+
by any of them, and using it to draw a mark grants no right to use it. The package ships no logo or headshot files:
|
|
464
|
+
it draws them through `@sportsdataverse/sdvplot`, which carries only an index of team names, ids and colors, and
|
|
465
|
+
the marks are fetched at runtime from the [SportsDataverse logo archive](https://github.com/sportsdataverse/sdv-assets),
|
|
466
|
+
headshots from ESPN (or, for NFL gsis ids, the headshot URLs in nflverse's player table). Use of any mark in your own
|
|
467
|
+
work is governed by that owner's terms, and following them is your responsibility.
|
|
468
|
+
|
|
469
|
+
Logos, headshots and team colors come from `@sportsdataverse/sdvplot`; the package makes no network requests of
|
|
470
|
+
its own beyond the optional Google Fonts `<link>` (`fonts: false` omits it). Social icons are Font Awesome Free 6
|
|
471
|
+
paths ([CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), https://fontawesome.com). The table helpers port
|
|
472
|
+
sdvplot's `great_tables` helpers (Python, MIT) and sdvplotR's `gt_*` functions (MIT).
|
|
473
|
+
|
|
474
|
+
The [MIT license](https://github.com/sportsdataverse/sdvplot-js/blob/main/LICENSE) covers the code; team data belongs to its respective owners and sources.
|
|
475
|
+
[NOTICE.md](https://github.com/sportsdataverse/sdvplot-js/blob/main/packages/sdvtables/NOTICE.md) records the sdvplot/sdvplotR and Font Awesome material and its licences.
|
|
476
|
+
|
|
477
|
+
## **The SportsDataverse**
|
|
478
|
+
|
|
479
|
+
`@sportsdataverse/sdvtables` is one of the three [sdvplot-js](https://github.com/sportsdataverse/sdvplot-js#readme) packages, with [`@sportsdataverse/sdvplot`](https://github.com/sportsdataverse/sdvplot-js/tree/main/packages/sdvplot#readme) and [`@sportsdataverse/sporty`](https://github.com/sportsdataverse/sdvplot-js/tree/main/packages/sporty#readme).
|
|
480
|
+
|
|
481
|
+
| Package | Sport / Scope |
|
|
482
|
+
| --- | --- |
|
|
483
|
+
| [**sdvplot**](https://sdvplot.sportsdataverse.org/) | The Python package sdvplot-js ports |
|
|
484
|
+
| [**sdvplotR**](https://sdvplotR.sportsdataverse.org/) | The R package sdvplot mirrors |
|
|
485
|
+
| [**sportyR**](https://github.com/sportsdataverse/sportyR) · [**sportypy**](https://sportypy.sportsdataverse.org/) | Playing-surface plots for R and Python, which `@sportsdataverse/sporty` ports |
|
|
486
|
+
| [**sportsdataverse.js**](https://js.sportsdataverse.org/) | SportsDataverse data for Node.js and TypeScript |
|
|
487
|
+
| [**sportsdataverse-py**](https://py.sportsdataverse.org/) | SportsDataverse data for Python: NFL, CFB, NBA, WNBA, MBB, WBB, MLB, NHL, PWHL, soccer and more |
|
|
488
|
+
| [**sportsdataverse-R**](https://r.sportsdataverse.org/) | The R packages: hoopR, wehoop, cfbfastR, fastRhockey, baseballr and more |
|
|
489
|
+
|
|
490
|
+
See the full ecosystem at [sportsdataverse.org](https://sportsdataverse.org/).
|
|
491
|
+
|
|
492
|
+
## **Follow the [SportsDataverse](https://x.com/SportsDataverse) on X and star this repo**
|
|
493
|
+
|
|
494
|
+
[](https://x.com/SportsDataverse)
|
|
495
|
+
[](https://github.com/sportsdataverse/sdvplot-js/stargazers)
|
|
496
|
+
|
|
497
|
+
## **Our Authors**
|
|
498
|
+
|
|
499
|
+
- [Saiem Gilani](https://x.com/saiemgilani)
|
|
500
|
+
<a href="https://x.com/saiemgilani" target="blank"><img src="https://img.shields.io/twitter/follow/saiemgilani?color=blue&label=%40saiemgilani&logo=x&style=for-the-badge" alt="@saiemgilani" /></a>
|
|
501
|
+
<a href="https://github.com/saiemgilani" target="blank"><img src="https://img.shields.io/github/followers/saiemgilani?color=eee&logo=Github&style=for-the-badge" alt="@saiemgilani" /></a>
|
|
502
|
+
|
|
503
|
+
## **Citations**
|
|
504
|
+
|
|
505
|
+
To cite [**`@sportsdataverse/sdvtables`**](https://plot.sportsdataverse.org) in publications, cite sdvplot-js:
|
|
506
|
+
|
|
507
|
+
BibTeX Citation
|
|
508
|
+
|
|
509
|
+
```bibtex
|
|
510
|
+
@misc{gilani_2026_sdvplot_js,
|
|
511
|
+
author = {Gilani, Saiem},
|
|
512
|
+
title = {sdvplot-js: Team identity, colors, logos, surfaces and tables for JavaScript and TypeScript plots},
|
|
513
|
+
url = {https://plot.sportsdataverse.org},
|
|
514
|
+
year = {2026}
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
## **License**
|
|
519
|
+
|
|
520
|
+
MIT. See [NOTICE.md](https://github.com/sportsdataverse/sdvplot-js/blob/main/packages/sdvtables/NOTICE.md) for the sdvplot/sdvplotR and Font Awesome material.
|