@sportsdataverse/sdvplot 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +15 -0
- package/README.md +84 -8
- package/dist/chartjs-surface.d.ts +2 -2
- package/dist/chartjs-surface.js +2 -2
- package/dist/chartjs-surface.js.map +1 -1
- package/dist/chartjs.d.ts +32 -2
- package/dist/chartjs.js +7 -4
- package/dist/chartjs.js.map +1 -1
- package/dist/chunk-57WFSI4Q.js +128 -0
- package/dist/chunk-57WFSI4Q.js.map +1 -0
- package/dist/{chunk-KKH6ABMA.js → chunk-7ULOCCFL.js} +94 -4
- package/dist/chunk-7ULOCCFL.js.map +1 -0
- package/dist/chunk-AMXASQHT.js +120 -0
- package/dist/chunk-AMXASQHT.js.map +1 -0
- package/dist/chunk-B2MP37SR.js +175 -0
- package/dist/chunk-B2MP37SR.js.map +1 -0
- package/dist/{chunk-NLPH2VZT.js → chunk-DYMUVE6K.js} +4 -4
- package/dist/{chunk-NLPH2VZT.js.map → chunk-DYMUVE6K.js.map} +1 -1
- package/dist/{chunk-QY65DUEF.js → chunk-H2C3FMMA.js} +4 -10
- package/dist/chunk-H2C3FMMA.js.map +1 -0
- package/dist/{chunk-JJFTD4VB.js → chunk-J4P4HDBF.js} +4 -4
- package/dist/{chunk-JJFTD4VB.js.map → chunk-J4P4HDBF.js.map} +1 -1
- package/dist/chunk-M3XD2OSN.js +132 -0
- package/dist/chunk-M3XD2OSN.js.map +1 -0
- package/dist/{chunk-OKEE4AH6.js → chunk-OIILARXI.js} +3 -3
- package/dist/{chunk-OKEE4AH6.js.map → chunk-OIILARXI.js.map} +1 -1
- package/dist/chunk-RPVVJY74.js +170 -0
- package/dist/chunk-RPVVJY74.js.map +1 -0
- package/dist/chunk-UL5M6KDE.js +205 -0
- package/dist/chunk-UL5M6KDE.js.map +1 -0
- package/dist/chunk-WQT4IVLX.js +10 -0
- package/dist/chunk-WQT4IVLX.js.map +1 -0
- package/dist/{chunk-TAXO3VXH.js → chunk-ZAEQXFJM.js} +3 -3
- package/dist/{chunk-TAXO3VXH.js.map → chunk-ZAEQXFJM.js.map} +1 -1
- package/dist/cytoscape.d.ts +188 -0
- package/dist/cytoscape.js +219 -0
- package/dist/cytoscape.js.map +1 -0
- package/dist/d3.d.ts +3 -3
- package/dist/d3.js +6 -6
- package/dist/d3.js.map +1 -1
- package/dist/deckgl.d.ts +232 -0
- package/dist/deckgl.js +197 -0
- package/dist/deckgl.js.map +1 -0
- package/dist/echarts.js +5 -5
- package/dist/g2.d.ts +251 -0
- package/dist/g2.js +102 -0
- package/dist/g2.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -5
- package/dist/lightweight-charts.d.ts +217 -0
- package/dist/lightweight-charts.js +155 -0
- package/dist/lightweight-charts.js.map +1 -0
- package/dist/maplibre.d.ts +271 -0
- package/dist/maplibre.js +175 -0
- package/dist/maplibre.js.map +1 -0
- package/dist/mui.d.ts +338 -0
- package/dist/mui.js +184 -0
- package/dist/mui.js.map +1 -0
- package/dist/nivo.d.ts +193 -0
- package/dist/nivo.js +8 -0
- package/dist/nivo.js.map +1 -0
- package/dist/perspective.d.ts +198 -0
- package/dist/perspective.js +171 -0
- package/dist/perspective.js.map +1 -0
- package/dist/placement-UWkqUzpa.d.ts +32 -0
- package/dist/plot.d.ts +7 -5
- package/dist/plot.js +9 -8
- package/dist/plot.js.map +1 -1
- package/dist/plotly.js +5 -5
- package/dist/react.d.ts +279 -3
- package/dist/react.js +364 -6
- package/dist/react.js.map +1 -1
- package/dist/recharts.d.ts +162 -0
- package/dist/recharts.js +8 -0
- package/dist/recharts.js.map +1 -0
- package/dist/shadcn.d.ts +83 -0
- package/dist/shadcn.js +88 -0
- package/dist/shadcn.js.map +1 -0
- package/dist/shots.js +3 -2
- package/dist/shots.js.map +1 -1
- package/dist/surface-TR7YFJW3.js +4 -0
- package/dist/surface-TR7YFJW3.js.map +1 -0
- package/dist/surfaces-MIG7HOFU.js +6 -0
- package/dist/surfaces-MIG7HOFU.js.map +1 -0
- package/dist/surfaces-W-odWLpT.d.ts +47 -0
- package/dist/svelte/HeadshotMark.svelte +25 -0
- package/dist/svelte/HeadshotMark.svelte.d.ts +10 -0
- package/dist/svelte/HeadshotMark.svelte.d.ts.map +1 -0
- package/dist/svelte/LogoMark.svelte +25 -0
- package/dist/svelte/LogoMark.svelte.d.ts +10 -0
- package/dist/svelte/LogoMark.svelte.d.ts.map +1 -0
- package/dist/svelte/Marks.svelte +25 -0
- package/dist/svelte/Marks.svelte.d.ts +10 -0
- package/dist/svelte/Marks.svelte.d.ts.map +1 -0
- package/dist/svelte/Surface.svelte +29 -0
- package/dist/svelte/Surface.svelte.d.ts +9 -0
- package/dist/svelte/Surface.svelte.d.ts.map +1 -0
- package/dist/svelte/SurfaceBackdrop.svelte +26 -0
- package/dist/svelte/SurfaceBackdrop.svelte.d.ts +11 -0
- package/dist/svelte/SurfaceBackdrop.svelte.d.ts.map +1 -0
- package/dist/svelte/index.d.ts +13 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +11 -0
- package/dist/svelte-core.d.ts +139 -0
- package/dist/svelte-core.js +123 -0
- package/dist/svelte-core.js.map +1 -0
- package/dist/tanstack.d.ts +240 -0
- package/dist/tanstack.js +110 -0
- package/dist/tanstack.js.map +1 -0
- package/dist/testing.js +4 -5
- package/dist/testing.js.map +1 -1
- package/dist/three.d.ts +223 -0
- package/dist/three.js +50 -0
- package/dist/three.js.map +1 -0
- package/dist/{tiers-B6pUBPcE.d.ts → tiers-BN4md-GB.d.ts} +2 -30
- package/dist/unovis.d.ts +127 -0
- package/dist/unovis.js +108 -0
- package/dist/unovis.js.map +1 -0
- package/dist/uplot.d.ts +120 -0
- package/dist/uplot.js +110 -0
- package/dist/uplot.js.map +1 -0
- package/dist/vega.js +5 -5
- package/dist/victory.d.ts +107 -0
- package/dist/victory.js +38 -0
- package/dist/victory.js.map +1 -0
- package/package.json +218 -6
- package/dist/chunk-KKH6ABMA.js.map +0 -1
- package/dist/chunk-QY65DUEF.js.map +0 -1
- package/dist/chunk-RMT2GMFH.js +0 -97
- package/dist/chunk-RMT2GMFH.js.map +0 -1
- package/dist/chunk-SEBO35DV.js +0 -76
- package/dist/chunk-SEBO35DV.js.map +0 -1
- package/dist/chunk-TDNBAEQB.js +0 -105
- package/dist/chunk-TDNBAEQB.js.map +0 -1
- package/dist/surfaces-ChaD9nei.d.ts +0 -38
package/dist/nivo.d.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
import { L as League, W as Which, S as SeasonInput, I as IdSystem, V as Variant, M as MarkType } from './types-C7n6gJF3.js';
|
|
3
|
+
export { R as Row, e as embedSources } from './_web-D7Aa5Rd8.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The nivo adapter: team colours as a chart's `colors`, team logos as a custom layer over a Bump, SwarmPlot or Chord
|
|
7
|
+
* (`logoLayer`), and logos as the nodes themselves (`logoNode`, a SwarmPlot `circleComponent` or a Bump
|
|
8
|
+
* `pointComponent`). Pure: nivo is never imported (the prop types here are structural, read from nivo 0.99); React's
|
|
9
|
+
* `createElement` builds the SVG `<image>` each one returns. Call after `loadLeague(league)` / `preloadAll()`. The
|
|
10
|
+
* ready-made chart is a component, `<LogoBump>` in `sdvplot/react`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** A Bump point as nivo computes it: the subset read here. */
|
|
14
|
+
interface NivoBumpPoint {
|
|
15
|
+
/** The point's centre in px, inside the plot area. */
|
|
16
|
+
x: number;
|
|
17
|
+
y: number | null;
|
|
18
|
+
/** Its diameter in px (the chart's `pointSize`). */
|
|
19
|
+
size?: number;
|
|
20
|
+
/** The series: `id`, and `data`, the series object the chart was given (with any extra fields). */
|
|
21
|
+
serie: {
|
|
22
|
+
id: string;
|
|
23
|
+
data?: object;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** A SwarmPlot node as nivo computes it: the subset read here. */
|
|
27
|
+
interface NivoNode {
|
|
28
|
+
/** The node's id (the chart's `id` accessor). */
|
|
29
|
+
id: string;
|
|
30
|
+
/** The node's centre in px, inside the plot area. */
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
/** Its diameter in px (the chart's `size`). */
|
|
34
|
+
size?: number;
|
|
35
|
+
/** The datum the node was drawn from. */
|
|
36
|
+
data?: unknown;
|
|
37
|
+
}
|
|
38
|
+
/** A Chord arc as nivo computes it: the subset read here. Angles in radians, clockwise from 12 o'clock. */
|
|
39
|
+
interface NivoArc {
|
|
40
|
+
/** The arc's key (the chart's `keys`). */
|
|
41
|
+
id: string;
|
|
42
|
+
startAngle: number;
|
|
43
|
+
endAngle: number;
|
|
44
|
+
}
|
|
45
|
+
/** What nivo hands a custom layer of a Bump (`points`), a SwarmPlot (`nodes`) or a Chord (`arcs`): the subset read. */
|
|
46
|
+
interface NivoLayerProps {
|
|
47
|
+
/** The plot area's height in px (Bump, SwarmPlot). */
|
|
48
|
+
innerHeight?: number;
|
|
49
|
+
/** A Bump's points (only those with a rank: a `null` y has none). */
|
|
50
|
+
points?: readonly NivoBumpPoint[];
|
|
51
|
+
/** A SwarmPlot's nodes. */
|
|
52
|
+
nodes?: readonly NivoNode[];
|
|
53
|
+
/** A Chord's arcs. */
|
|
54
|
+
arcs?: readonly NivoArc[];
|
|
55
|
+
/** The Chord's centre in px: half the plot area's width and height. */
|
|
56
|
+
center?: readonly [number, number];
|
|
57
|
+
/** The Chord ring's outer radius in px. */
|
|
58
|
+
radius?: number;
|
|
59
|
+
}
|
|
60
|
+
/** A nivo custom layer: an SVG group of logos. */
|
|
61
|
+
type NivoLayer = (props: NivoLayerProps) => ReactElement | null;
|
|
62
|
+
/** What nivo hands a SwarmPlot `circleComponent` (`node`) or a Bump `pointComponent` (`point`): the subset read. */
|
|
63
|
+
interface NivoNodeProps {
|
|
64
|
+
/** A SwarmPlot `circleComponent`'s node. */
|
|
65
|
+
node?: NivoNode;
|
|
66
|
+
/** A Bump `pointComponent`'s point. */
|
|
67
|
+
point?: NivoBumpPoint;
|
|
68
|
+
}
|
|
69
|
+
/** A node component: the logo, or an empty group where none can be drawn (nivo's types want an element). */
|
|
70
|
+
type NivoNodeComponent = (props: NivoNodeProps) => ReactElement;
|
|
71
|
+
interface Shared {
|
|
72
|
+
/** The league the teams belong to, e.g. "nfl". */
|
|
73
|
+
league: League;
|
|
74
|
+
/**
|
|
75
|
+
* The field holding the team (abbreviation, name or id; see `idSystem`): read from a Bump series' object or a
|
|
76
|
+
* SwarmPlot node's datum. Default: the series' or node's `id`. A Chord arc is read by its id (the chart's `keys`).
|
|
77
|
+
*/
|
|
78
|
+
team?: string;
|
|
79
|
+
/** A season (number or string) for every team, or the name of the field holding each one's season. */
|
|
80
|
+
season?: SeasonInput;
|
|
81
|
+
/** Opacity in [0, 1]. Default 1. */
|
|
82
|
+
alpha?: number;
|
|
83
|
+
/** Logo variant, e.g. "dark". Default "default". */
|
|
84
|
+
variant?: Variant;
|
|
85
|
+
/** How the team values are read. Default "auto". */
|
|
86
|
+
idSystem?: IdSystem;
|
|
87
|
+
/** "logo" (default) or "wordmark". */
|
|
88
|
+
markType?: MarkType;
|
|
89
|
+
/** url → data URI map from `embedSources`, to inline the images. */
|
|
90
|
+
embed?: ReadonlyMap<string, string>;
|
|
91
|
+
}
|
|
92
|
+
/** `logoLayer` options. */
|
|
93
|
+
interface NivoLayerOptions extends Shared {
|
|
94
|
+
/**
|
|
95
|
+
* Image height as a fraction of the plot area's height, in (0, 1] (nivo hands a layer the plot's `innerHeight`, so
|
|
96
|
+
* a px value is refused). Default 0.08.
|
|
97
|
+
*/
|
|
98
|
+
height?: number;
|
|
99
|
+
/** Which of a Bump series' points get a logo: "last" (default: the end of each line), "first" or "all". */
|
|
100
|
+
at?: "last" | "first" | "all";
|
|
101
|
+
}
|
|
102
|
+
/** `logoNode` options: the logo is as tall as nivo sized the node (the chart's `size` / `pointSize`). */
|
|
103
|
+
type NivoNodeOptions = Shared;
|
|
104
|
+
/**
|
|
105
|
+
* Team logos as a nivo custom layer: add it to a chart's `layers`, after the built-in ones it should sit over.
|
|
106
|
+
* - Bump: a logo at each series' last point (`at` picks the first, or every point), the team read from the series'
|
|
107
|
+
* `id` (or its `team` field).
|
|
108
|
+
* - SwarmPlot: a logo at every node, the team read from the node's `id` (or its datum's `team` field).
|
|
109
|
+
* - Chord: a logo outside the ring at the middle of each arc, the team read from the arc's id (the chart's `keys`).
|
|
110
|
+
*
|
|
111
|
+
* Logos are `height` × the plot height tall. Given your own `layers`, put the logo layer before `"mesh"`; appended after
|
|
112
|
+
* it, it is harmless (its group has `pointer-events: none`, so the mesh still gets the mouse). A team with no logo draws nothing and warns once, as `sdvplot/echarts`
|
|
113
|
+
* does. The logos sit at nivo's final positions: they do not tween with `animate`.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```tsx
|
|
117
|
+
* import { SwarmPlot } from "@nivo/swarmplot";
|
|
118
|
+
* import { loadLeague } from "@sportsdataverse/sdvplot";
|
|
119
|
+
* import { logoLayer, teamColors } from "@sportsdataverse/sdvplot/nivo";
|
|
120
|
+
*
|
|
121
|
+
* await loadLeague("nfl");
|
|
122
|
+
* const rows = [
|
|
123
|
+
* { team: "KC", pf: 385 },
|
|
124
|
+
* { team: "LAC", pf: 402 },
|
|
125
|
+
* { team: "DEN", pf: 425 },
|
|
126
|
+
* ];
|
|
127
|
+
* <SwarmPlot
|
|
128
|
+
* width={500}
|
|
129
|
+
* height={200}
|
|
130
|
+
* data={rows.map((r) => ({ ...r, group: "AFC West" }))}
|
|
131
|
+
* groups={["AFC West"]}
|
|
132
|
+
* id="team"
|
|
133
|
+
* value="pf"
|
|
134
|
+
* size={10}
|
|
135
|
+
* colors={teamColors({ league: "nfl" })}
|
|
136
|
+
* layers={["grid", "axes", "circles", logoLayer({ league: "nfl", height: 0.2 })]}
|
|
137
|
+
* />;
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
declare function logoLayer(o: NivoLayerOptions): NivoLayer;
|
|
141
|
+
/**
|
|
142
|
+
* Team logos as the nodes themselves: a SwarmPlot's `circleComponent` or a Bump's `pointComponent`. Each logo is as
|
|
143
|
+
* tall as nivo sized the node (the chart's `size` or `pointSize`, in px) and replaces its circle; the team is read as
|
|
144
|
+
* `logoLayer` reads it. A team with no logo draws nothing and warns once. The logo sits at the node's final position:
|
|
145
|
+
* it does not tween with `animate`.
|
|
146
|
+
*/
|
|
147
|
+
declare function logoNode(o: NivoNodeOptions): NivoNodeComponent;
|
|
148
|
+
/** `teamColors` options. */
|
|
149
|
+
interface NivoColorOptions {
|
|
150
|
+
/** The league the teams belong to, e.g. "nfl". */
|
|
151
|
+
league: League;
|
|
152
|
+
/**
|
|
153
|
+
* The field holding the team: read from the datum nivo hands `colors` (a Bump series, a Chord arc), else from its
|
|
154
|
+
* `data` (a SwarmPlot node's datum). Default "id".
|
|
155
|
+
*/
|
|
156
|
+
by?: string;
|
|
157
|
+
/** Every team the chart draws, in order. Needed with `background`: the palette keeps them apart from each other. */
|
|
158
|
+
teams?: readonly unknown[];
|
|
159
|
+
/** "primary" (default) or "secondary" colour. */
|
|
160
|
+
which?: Which;
|
|
161
|
+
/** The season the teams are resolved for (a number or string). */
|
|
162
|
+
season?: SeasonInput;
|
|
163
|
+
/** How the team values are read. Default "auto". */
|
|
164
|
+
idSystem?: IdSystem;
|
|
165
|
+
/** The colour of an unresolved team. Default "#808080". */
|
|
166
|
+
fallback?: string;
|
|
167
|
+
/**
|
|
168
|
+
* The chart's background. Given, a team colour under 3:1 contrast against it is swapped for the team's other
|
|
169
|
+
* colour, else moved towards white (on a dark background) or black (a light one) until it reads: dark mode. A
|
|
170
|
+
* colour that would then sit too close to an earlier team's (CIE76 ΔE under 20) takes the team's other colour, or
|
|
171
|
+
* the nearest colour to its own (lightness, hue or chroma stepped) that is distinct and still reads; failing that it
|
|
172
|
+
* is kept, with one warning.
|
|
173
|
+
*/
|
|
174
|
+
background?: string;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* A nivo `colors` accessor: each datum's team colour. A Bump series and a Chord arc are read by `id`; for a SwarmPlot
|
|
178
|
+
* pass `by: "<team field>"`. With `background` (and `teams`), every colour keeps 3:1 contrast against it (dark mode)
|
|
179
|
+
* and aims to stay distinct from the others (when it cannot, it keeps the readable colour and warns once).
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```ts
|
|
183
|
+
* import { loadLeague } from "@sportsdataverse/sdvplot";
|
|
184
|
+
* import { teamColors } from "@sportsdataverse/sdvplot/nivo";
|
|
185
|
+
*
|
|
186
|
+
* await loadLeague("nfl");
|
|
187
|
+
* const colors = teamColors({ league: "nfl", teams: ["KC", "LAC", "DEN", "LV"], background: "#181a1b" });
|
|
188
|
+
* ["KC", "LAC", "DEN", "LV"].map((id) => colors({ id }));
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
declare function teamColors(o: NivoColorOptions): (datum: object) => string;
|
|
192
|
+
|
|
193
|
+
export { type NivoArc, type NivoBumpPoint, type NivoColorOptions, type NivoLayer, type NivoLayerOptions, type NivoLayerProps, type NivoNode, type NivoNodeComponent, type NivoNodeOptions, type NivoNodeProps, logoLayer, logoNode, teamColors };
|
package/dist/nivo.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { logoLayer, logoNode, teamColors } from './chunk-RPVVJY74.js';
|
|
2
|
+
export { embedSources } from './chunk-UL5M6KDE.js';
|
|
3
|
+
import './chunk-7ULOCCFL.js';
|
|
4
|
+
import './chunk-ZAEQXFJM.js';
|
|
5
|
+
import './chunk-J6R2LEJ7.js';
|
|
6
|
+
import './chunk-E7G7VSLV.js';
|
|
7
|
+
//# sourceMappingURL=nivo.js.map
|
|
8
|
+
//# sourceMappingURL=nivo.js.map
|
package/dist/nivo.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"nivo.js"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { L as League, S as SeasonInput, I as IdSystem, V as Variant } from './types-C7n6gJF3.js';
|
|
2
|
+
|
|
3
|
+
/** A row: column name to value, as `perspective.table(rows)` takes them. */
|
|
4
|
+
type PerspectiveRow = Readonly<Record<string, unknown>>;
|
|
5
|
+
/** A Perspective column type, as `await table.schema()` gives it. */
|
|
6
|
+
type PerspectiveType = "integer" | "float" | "string" | "boolean" | "date" | "datetime";
|
|
7
|
+
/** A Perspective schema: column name to type, as `await table.schema()` gives it. */
|
|
8
|
+
type PerspectiveSchema = Readonly<Record<string, PerspectiveType>>;
|
|
9
|
+
/** A `number_format`: Intl.NumberFormat options, e.g. `{ maximumFractionDigits: 2 }`. */
|
|
10
|
+
type PerspectiveNumberFormat = {
|
|
11
|
+
readonly [option: string]: string | number | boolean;
|
|
12
|
+
};
|
|
13
|
+
/** One column's entry in `columns_config`: the keys sdvplot sets. */
|
|
14
|
+
type PerspectiveColumnConfig = {
|
|
15
|
+
/** The Datagrid's cell fill: "series", one palette colour per distinct value. */
|
|
16
|
+
bg_mode?: "series";
|
|
17
|
+
/** The Datagrid's series palette: `linear-gradient(to right, #rrggbb, …)`. */
|
|
18
|
+
bg_color?: string;
|
|
19
|
+
/** A chart's series palette (on a value column) or a scatter's Color slot palette (on the team column). */
|
|
20
|
+
palette?: string;
|
|
21
|
+
number_format?: PerspectiveNumberFormat;
|
|
22
|
+
/** The Datagrid shows a URL column as links. */
|
|
23
|
+
link?: true;
|
|
24
|
+
/** The Datagrid's number colours (a registry theme's): `linear-gradient(to right, <neg> 0%, <pos> 100%)`. */
|
|
25
|
+
fg_color?: string;
|
|
26
|
+
/** The Datagrid's font for a number column: a registry theme's numeric face, when it is not the body face. */
|
|
27
|
+
font_family?: string;
|
|
28
|
+
};
|
|
29
|
+
/** The Datagrid's own settings: a registry theme's body face and size. */
|
|
30
|
+
type PerspectivePluginConfig = {
|
|
31
|
+
font_family: string;
|
|
32
|
+
font_size: number;
|
|
33
|
+
};
|
|
34
|
+
/** What `perspectiveConfig` returns: pass it to `viewer.restore()`, with your own view keys merged in. */
|
|
35
|
+
type PerspectiveConfig = {
|
|
36
|
+
plugin: string;
|
|
37
|
+
/** Perspective's own theme: "Pro Light" or "Pro Dark" (load `@perspective-dev/viewer/dist/css/pro.css` / `pro-dark.css`). */
|
|
38
|
+
theme: "Pro Light" | "Pro Dark";
|
|
39
|
+
/** The Datagrid with a registry `theme` only. */
|
|
40
|
+
plugin_config?: PerspectivePluginConfig;
|
|
41
|
+
columns_config: Record<string, PerspectiveColumnConfig>;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The parts of a registry table theme `perspectiveConfig` reads. A `TableThemeSpec` (sdvtables'
|
|
45
|
+
* `tableThemeSpec(name, mode)`, Python's `table_theme_spec`) is one; sdvplot does not ship the registry itself.
|
|
46
|
+
*/
|
|
47
|
+
interface PerspectiveTheme {
|
|
48
|
+
/** `body`: the grid's face. `numeric`: the number columns' face, when it is not `body`. */
|
|
49
|
+
readonly fonts: {
|
|
50
|
+
readonly body?: string;
|
|
51
|
+
readonly numeric?: string;
|
|
52
|
+
};
|
|
53
|
+
/** `body`: the grid's font size in px. */
|
|
54
|
+
readonly sizes: {
|
|
55
|
+
readonly body: number;
|
|
56
|
+
};
|
|
57
|
+
/** The number colours: `neg` (low) to `pos` (high). */
|
|
58
|
+
readonly palette: {
|
|
59
|
+
readonly neg: string;
|
|
60
|
+
readonly pos: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** `perspectiveConfig` options. */
|
|
64
|
+
interface PerspectiveConfigOptions {
|
|
65
|
+
/** The column holding the team identifiers (any id system `resolve` reads). */
|
|
66
|
+
teamCol: string;
|
|
67
|
+
/** The SDV league key, e.g. "nfl". */
|
|
68
|
+
league: League;
|
|
69
|
+
/** The plugin: "Datagrid" (the default) or a chart's name ("Y Bar", "X Bar", "Y Line", "X/Y Scatter", …). */
|
|
70
|
+
plugin?: string;
|
|
71
|
+
/** "light" (default) or "dark": the Perspective theme, "Pro Light" or "Pro Dark". */
|
|
72
|
+
mode?: "light" | "dark";
|
|
73
|
+
/**
|
|
74
|
+
* A registry table theme: best as a function of the mode, `(m) => tableThemeSpec(name, m)` from
|
|
75
|
+
* `@sportsdataverse/sdvtables`, called with this call's `mode` on every plugin (so a light-only theme in dark mode
|
|
76
|
+
* throws, as in Python). A spec object works too; it does not say its mode, so resolve it in the same `mode`.
|
|
77
|
+
* The Datagrid takes its body face and size (`plugin_config`), its sign colours on every number column (`fg_color`;
|
|
78
|
+
* Perspective reads only `#hex` and `rgb()` colours there) and, when the numeric face is not the body face, that face
|
|
79
|
+
* on the number columns. Perspective takes each face as one family name, so the page loads the font (Google Fonts)
|
|
80
|
+
* or the browser draws its default. A chart takes none of it.
|
|
81
|
+
*/
|
|
82
|
+
theme?: PerspectiveTheme | ((mode: "light" | "dark") => PerspectiveTheme);
|
|
83
|
+
/** A logo URL column (`perspectiveLogoColumn`) to show as links on the Datagrid, which draws no images. */
|
|
84
|
+
logoCol?: string;
|
|
85
|
+
/** The colour the teams must read against. Default: the Perspective theme's (#ffffff Pro Light, #242526 Pro Dark). */
|
|
86
|
+
background?: string;
|
|
87
|
+
/** Per column, a `number_format` to use instead of the default, or null for none. */
|
|
88
|
+
formats?: Readonly<Record<string, PerspectiveNumberFormat | null>>;
|
|
89
|
+
/** One season for every team. */
|
|
90
|
+
season?: SeasonInput;
|
|
91
|
+
/** How the team values are read. Default "auto". */
|
|
92
|
+
idSystem?: IdSystem;
|
|
93
|
+
/** Throw UnresolvedTeamError for a team that does not resolve, instead of one warning and #808080. */
|
|
94
|
+
strict?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* The column types, as `await table.schema()` gives them. Default: worked out from the rows as Perspective does
|
|
97
|
+
* (numbers, and strings that read as numbers, are "integer" or "float"; anything else is not a number). Pass it when
|
|
98
|
+
* you load the table with a schema of your own.
|
|
99
|
+
*/
|
|
100
|
+
schema?: PerspectiveSchema;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A Perspective viewer config that colours each team by its own colours, for the Datagrid or a chart: the argument of
|
|
104
|
+
* `viewer.restore()`, as Python's `perspective_sdv_config` builds it (same keys, same colours, same order).
|
|
105
|
+
*
|
|
106
|
+
* The colours are the shared team palette (every adapter's, and Python's), taken over the teams in sorted order: each
|
|
107
|
+
* team's colour, made 3:1 against the background and ΔE 20 from every earlier team's, so a team keeps one colour
|
|
108
|
+
* whatever the rows' order. Perspective takes a palette, not a colour per value, so each palette lists those colours in
|
|
109
|
+
* the order Perspective hands colours out (checked in Chromium on Perspective 5.5.1):
|
|
110
|
+
*
|
|
111
|
+
* - `plugin: "Datagrid"`: the team column's cells are filled (`bg_mode` "series"), in the order the grid first draws
|
|
112
|
+
* each value: the rows' order. Keep the grid in the rows' order (no `sort`) and to about one row per team. Null
|
|
113
|
+
* cells take no colour and no slot. Only a text team column is filled: Perspective fills no number column, so
|
|
114
|
+
* numeric team ids get none, with one warning.
|
|
115
|
+
* - a chart: split a bar, line or area chart by the team (`split_by: [teamCol]`) and each series takes its colour
|
|
116
|
+
* from every number column's `palette`, in sorted order (Perspective sorts split values; a null series comes first,
|
|
117
|
+
* so the palette then starts with the fallback grey). Put the team column in an "X/Y Scatter"'s Color slot (its
|
|
118
|
+
* third column) and each team's points take theirs from the team column's `palette`, in the rows' order.
|
|
119
|
+
*
|
|
120
|
+
* Every number column also gets a `number_format` (Intl.NumberFormat options): 3 decimals for a float, 1 for a float
|
|
121
|
+
* named as a percentage (`pct`, `percent`), and an integer plain (2024, not 2,024). `formats` overrides them.
|
|
122
|
+
*
|
|
123
|
+
* @param rows - the rows the viewer shows (what `perspective.table(rows)` loads). With no rows and no `schema` no column is
|
|
124
|
+
* known: the config has `plugin` and `theme` and an empty `columns_config`, without an error (pass the table's
|
|
125
|
+
* `schema` for its number formats, as Python's empty frame keeps its columns).
|
|
126
|
+
* @returns `plugin`, `theme` and `columns_config`; merge your own `group_by`, `split_by`, `columns` and the rest in.
|
|
127
|
+
* @throws InputError for a column the rows do not have (`teamCol`, `logoCol` or a `formats` key), a team column that
|
|
128
|
+
* mixes numbers and strings, an unknown `mode`, a `theme` that is not a spec (or a function returning one), or a
|
|
129
|
+
* season outside the league's. UnresolvedTeamError with `strict: true`. A `theme` function's own errors pass through
|
|
130
|
+
* (sdvtables' TableSpecError for a light-only theme in dark mode).
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* import { loadLeague } from "@sportsdataverse/sdvplot";
|
|
135
|
+
* import { perspectiveConfig } from "@sportsdataverse/sdvplot/perspective";
|
|
136
|
+
*
|
|
137
|
+
* await loadLeague("nfl");
|
|
138
|
+
* // 2024 regular season, offence: EPA per play (nflverse play-by-play)
|
|
139
|
+
* const epa = [
|
|
140
|
+
* { posteam: "BAL", epa_per_play: 0.218 },
|
|
141
|
+
* { posteam: "BUF", epa_per_play: 0.1885 },
|
|
142
|
+
* { posteam: "DET", epa_per_play: 0.1592 },
|
|
143
|
+
* ];
|
|
144
|
+
* const grid = perspectiveConfig(epa, { teamCol: "posteam", league: "nfl" });
|
|
145
|
+
* const bars = {
|
|
146
|
+
* ...perspectiveConfig(epa, { teamCol: "posteam", league: "nfl", plugin: "Y Bar" }),
|
|
147
|
+
* group_by: ["posteam"],
|
|
148
|
+
* split_by: ["posteam"],
|
|
149
|
+
* columns: ["epa_per_play"],
|
|
150
|
+
* };
|
|
151
|
+
* // in the browser: await viewer.load(await worker.table(epa)); await viewer.restore(grid)
|
|
152
|
+
* [grid.columns_config.posteam?.bg_color, bars.columns_config.epa_per_play?.palette];
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
declare function perspectiveConfig(rows: readonly PerspectiveRow[], o: PerspectiveConfigOptions): PerspectiveConfig;
|
|
156
|
+
/** `perspectiveLogoColumn` options. */
|
|
157
|
+
interface PerspectiveLogoOptions {
|
|
158
|
+
/** The SDV league key, e.g. "nfl". */
|
|
159
|
+
league: League;
|
|
160
|
+
/** The new column's name. Default: `${teamCol}_logo`. It must not be a column already. */
|
|
161
|
+
name?: string;
|
|
162
|
+
/** One season for every team: the logo of that season (relocations and rebrands). */
|
|
163
|
+
season?: SeasonInput;
|
|
164
|
+
/** "default", "dark", or a named variant from `marks()`. */
|
|
165
|
+
variant?: Variant;
|
|
166
|
+
/** How the team values are read. Default "auto". */
|
|
167
|
+
idSystem?: IdSystem;
|
|
168
|
+
/** Throw UnresolvedTeamError for a team that does not resolve, instead of warning. */
|
|
169
|
+
strict?: boolean;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The rows with a column of each team's logo URL, right after the team column, as Python's `perspective_logo_column`
|
|
173
|
+
* adds it. Each distinct team is resolved once. A team that does not resolve, or has no archived logo, gets a null URL,
|
|
174
|
+
* with one warning for each reason. Show the column as links with `perspectiveConfig(rows, { logoCol })`: Perspective
|
|
175
|
+
* 5.5's Datagrid draws no images. The rows given are not changed.
|
|
176
|
+
*
|
|
177
|
+
* @returns new rows, each with the URL (a string, or null) in `name`.
|
|
178
|
+
* @throws InputError for a `teamCol` no row has, a `name` a row already has, or a season outside the league's.
|
|
179
|
+
* UnresolvedTeamError with `strict: true`.
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```ts
|
|
183
|
+
* import { loadLeague } from "@sportsdataverse/sdvplot";
|
|
184
|
+
* import { perspectiveConfig, perspectiveLogoColumn } from "@sportsdataverse/sdvplot/perspective";
|
|
185
|
+
*
|
|
186
|
+
* await loadLeague("nfl");
|
|
187
|
+
* const epa = [
|
|
188
|
+
* { posteam: "BAL", epa_per_play: 0.218 },
|
|
189
|
+
* { posteam: "BUF", epa_per_play: 0.1885 },
|
|
190
|
+
* ];
|
|
191
|
+
* const rows = perspectiveLogoColumn(epa, "posteam", { league: "nfl" });
|
|
192
|
+
* const config = perspectiveConfig(rows, { teamCol: "posteam", league: "nfl", logoCol: "posteam_logo" });
|
|
193
|
+
* [Object.keys(rows[0] ?? {}), config.columns_config.posteam_logo];
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
declare function perspectiveLogoColumn<R extends PerspectiveRow>(rows: readonly R[], teamCol: string, o: PerspectiveLogoOptions): (R & Record<string, string | null>)[];
|
|
197
|
+
|
|
198
|
+
export { type PerspectiveColumnConfig, type PerspectiveConfig, type PerspectiveConfigOptions, type PerspectiveLogoOptions, type PerspectiveNumberFormat, type PerspectivePluginConfig, type PerspectiveRow, type PerspectiveSchema, type PerspectiveTheme, type PerspectiveType, perspectiveConfig, perspectiveLogoColumn };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { colorPalette } from './chunk-UL5M6KDE.js';
|
|
2
|
+
import { placeSync } from './chunk-7ULOCCFL.js';
|
|
3
|
+
import { normSeason } from './chunk-ZAEQXFJM.js';
|
|
4
|
+
import { hex6 } from './chunk-J6R2LEJ7.js';
|
|
5
|
+
import { InputError, warn } from './chunk-E7G7VSLV.js';
|
|
6
|
+
|
|
7
|
+
// src/perspective.ts
|
|
8
|
+
var PRO = { light: ["Pro Light", "#ffffff"], dark: ["Pro Dark", "#242526"] };
|
|
9
|
+
var FALLBACK = "#808080";
|
|
10
|
+
var PERCENT = /(^|_)(pct|percent|perc)(_|$)/i;
|
|
11
|
+
function numberFormat(name, type) {
|
|
12
|
+
if (type === "integer") return { useGrouping: false, maximumFractionDigits: 0 };
|
|
13
|
+
const n = PERCENT.test(name) ? 1 : 3;
|
|
14
|
+
return { minimumFractionDigits: n, maximumFractionDigits: n };
|
|
15
|
+
}
|
|
16
|
+
var INT = /^\s*-?\d+\s*$/;
|
|
17
|
+
var FLOAT = /^\s*-?(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?\s*$/;
|
|
18
|
+
function kindOf(v) {
|
|
19
|
+
if (v === null || v === void 0) return null;
|
|
20
|
+
if (typeof v === "bigint") return "integer";
|
|
21
|
+
if (typeof v === "number") {
|
|
22
|
+
if (!Number.isFinite(v)) return "other";
|
|
23
|
+
return Number.isInteger(v) && Math.abs(v) < 2 ** 31 ? "integer" : "float";
|
|
24
|
+
}
|
|
25
|
+
if (typeof v === "string") return INT.test(v) ? "integer" : FLOAT.test(v) ? "float" : "other";
|
|
26
|
+
return "other";
|
|
27
|
+
}
|
|
28
|
+
function inferSchema(rows) {
|
|
29
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
30
|
+
for (const r of rows)
|
|
31
|
+
for (const [k, v] of Object.entries(r)) {
|
|
32
|
+
const s = kinds.get(k) ?? kinds.set(k, /* @__PURE__ */ new Set()).get(k);
|
|
33
|
+
const kind = kindOf(v);
|
|
34
|
+
if (kind !== null) s.add(kind);
|
|
35
|
+
}
|
|
36
|
+
return Object.fromEntries(
|
|
37
|
+
[...kinds].map(([k, s]) => [
|
|
38
|
+
k,
|
|
39
|
+
s.size === 0 || s.has("other") ? "string" : s.has("float") ? "float" : "integer"
|
|
40
|
+
])
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
var isNumber = (t) => t === "integer" || t === "float";
|
|
44
|
+
var quote = (s) => `'${s}'`;
|
|
45
|
+
var isObj = (v) => typeof v === "object" && v !== null;
|
|
46
|
+
var isFace = (v) => v === void 0 || typeof v === "string";
|
|
47
|
+
var isTheme = (t) => isObj(t) && isObj(t.fonts) && isFace(t.fonts.body) && isFace(t.fonts.numeric) && isObj(t.sizes) && typeof t.sizes.body === "number" && isObj(t.palette) && typeof t.palette.neg === "string" && typeof t.palette.pos === "string";
|
|
48
|
+
function checkColumns(schema, cols) {
|
|
49
|
+
const names = Object.keys(schema);
|
|
50
|
+
for (const c of cols)
|
|
51
|
+
if (c !== void 0 && !Object.hasOwn(schema, c))
|
|
52
|
+
throw new InputError(`the rows have no column ${quote(c)} (columns: ${names.join(", ")})`);
|
|
53
|
+
}
|
|
54
|
+
function teamOrders(values, numeric, col) {
|
|
55
|
+
const first = [...new Set(values.filter((v) => v !== null && v !== void 0))];
|
|
56
|
+
const types = new Set(first.map((v) => typeof v === "bigint" ? "number" : typeof v));
|
|
57
|
+
if (types.size > 1)
|
|
58
|
+
throw new InputError(
|
|
59
|
+
`the team column ${quote(col)} mixes ${[...types].sort().join(" and ")} values: use one id system`
|
|
60
|
+
);
|
|
61
|
+
const key = numeric ? (v) => Number(v) : (v) => String(v);
|
|
62
|
+
const sorted = [...first].sort((a, b) => {
|
|
63
|
+
const [x, y] = [key(a), key(b)];
|
|
64
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
65
|
+
});
|
|
66
|
+
return [first, sorted];
|
|
67
|
+
}
|
|
68
|
+
var gradient = (colors) => `linear-gradient(to right, ${colors.join(", ")})`;
|
|
69
|
+
function perspectiveConfig(rows, o) {
|
|
70
|
+
const mode = o.mode ?? "light";
|
|
71
|
+
if (mode !== "light" && mode !== "dark")
|
|
72
|
+
throw new InputError(`mode must be 'light' or 'dark', not ${quote(String(mode))}`);
|
|
73
|
+
const resolved = typeof o.theme === "function" ? o.theme(mode) : o.theme;
|
|
74
|
+
if (o.theme !== void 0 && !isTheme(resolved))
|
|
75
|
+
throw new InputError(
|
|
76
|
+
`theme must be a table theme spec or a function of the mode, not ${quote(String(resolved))}: pass (m) => tableThemeSpec(name, m) from @sportsdataverse/sdvtables`
|
|
77
|
+
);
|
|
78
|
+
const schema = o.schema ?? inferSchema(rows);
|
|
79
|
+
const { teamCol, logoCol } = o;
|
|
80
|
+
if (o.schema !== void 0 || rows.length > 0)
|
|
81
|
+
checkColumns(schema, [teamCol, logoCol, ...Object.keys(o.formats ?? {})]);
|
|
82
|
+
const [pro, proBg] = PRO[mode];
|
|
83
|
+
const bg = o.background === void 0 ? proBg : hex6(o.background);
|
|
84
|
+
const season = normSeason(o.season, o.league);
|
|
85
|
+
const text = !isNumber(schema[teamCol]);
|
|
86
|
+
const numeric = Object.keys(schema).filter((c) => c !== teamCol && isNumber(schema[c]));
|
|
87
|
+
const grid = (o.plugin ?? "Datagrid") === "Datagrid";
|
|
88
|
+
const values = rows.map((r) => r[teamCol]);
|
|
89
|
+
const [first, ascending] = teamOrders(values, !text, teamCol);
|
|
90
|
+
const pal = colorPalette(o.league, ascending, {
|
|
91
|
+
...season === null ? {} : { season },
|
|
92
|
+
background: bg,
|
|
93
|
+
idSystem: o.idSystem ?? "auto",
|
|
94
|
+
strict: o.strict ?? false,
|
|
95
|
+
fallback: FALLBACK
|
|
96
|
+
});
|
|
97
|
+
const color = new Map(ascending.map((v, i) => [v, pal[i]]));
|
|
98
|
+
const nulls = values.some((v) => v === null || v === void 0) ? [FALLBACK] : [];
|
|
99
|
+
const byAppearance = gradient(first.map((v) => color.get(v)));
|
|
100
|
+
const byValue = gradient([...nulls, ...pal]);
|
|
101
|
+
let columns = {};
|
|
102
|
+
for (const c of numeric) {
|
|
103
|
+
const fmt = o.formats !== void 0 && Object.hasOwn(o.formats, c) ? o.formats[c] : numberFormat(c, schema[c]);
|
|
104
|
+
columns[c] = fmt === null || fmt === void 0 ? {} : { number_format: fmt };
|
|
105
|
+
}
|
|
106
|
+
if (first.length > 0 && !grid) {
|
|
107
|
+
for (const c of numeric) columns[c].palette = byValue;
|
|
108
|
+
if (text) columns = { [teamCol]: { palette: byAppearance }, ...columns };
|
|
109
|
+
} else if (first.length > 0 && text) {
|
|
110
|
+
columns = { [teamCol]: { bg_mode: "series", bg_color: byAppearance }, ...columns };
|
|
111
|
+
} else if (first.length > 0) {
|
|
112
|
+
warn(
|
|
113
|
+
`perspective:numeric-team:${teamCol}`,
|
|
114
|
+
`no team colors on the Datagrid: the team column ${quote(teamCol)} is numeric, and team colors need a string or categorical team column (Perspective fills only text cells)`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const theme = grid ? resolved : void 0;
|
|
118
|
+
if (theme !== void 0) {
|
|
119
|
+
const { body, numeric: face } = theme.fonts;
|
|
120
|
+
const sign = `linear-gradient(to right, ${theme.palette.neg} 0%, ${theme.palette.pos} 100%)`;
|
|
121
|
+
for (const c of numeric) {
|
|
122
|
+
columns[c].fg_color = sign;
|
|
123
|
+
if (face && face !== body) columns[c].font_family = face;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (grid && logoCol !== void 0) columns[logoCol] = { link: true };
|
|
127
|
+
const gridFace = theme?.fonts.body;
|
|
128
|
+
return {
|
|
129
|
+
plugin: o.plugin ?? "Datagrid",
|
|
130
|
+
theme: pro,
|
|
131
|
+
...theme !== void 0 && gridFace ? { plugin_config: { font_family: gridFace, font_size: theme.sizes.body } } : {},
|
|
132
|
+
columns_config: Object.fromEntries(Object.entries(columns).filter(([, v]) => Object.keys(v).length > 0))
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function perspectiveLogoColumn(rows, teamCol, o) {
|
|
136
|
+
const name = o.name ?? `${teamCol}_logo`;
|
|
137
|
+
if (rows.length > 0 && !rows.some((r) => Object.hasOwn(r, teamCol)))
|
|
138
|
+
throw new InputError(`the rows have no column ${quote(teamCol)}`);
|
|
139
|
+
if (rows.some((r) => Object.hasOwn(r, name)))
|
|
140
|
+
throw new InputError(`the rows already have a column ${quote(name)}: pass another name`);
|
|
141
|
+
const values = [...new Set(rows.map((r) => r[teamCol]).filter((v) => v !== null && v !== void 0))];
|
|
142
|
+
const at = values.map((_, i) => i);
|
|
143
|
+
const placed = placeSync(
|
|
144
|
+
at,
|
|
145
|
+
at.map(() => 0),
|
|
146
|
+
values,
|
|
147
|
+
{
|
|
148
|
+
league: o.league,
|
|
149
|
+
season: normSeason(o.season, o.league),
|
|
150
|
+
kind: "logo",
|
|
151
|
+
variant: o.variant ?? "default",
|
|
152
|
+
idSystem: o.idSystem ?? "auto",
|
|
153
|
+
strict: o.strict ?? false
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
const urls = new Map(placed.map((p) => [values[p.index], p.url]));
|
|
157
|
+
return rows.map((r) => {
|
|
158
|
+
const out = {};
|
|
159
|
+
const url = urls.get(r[teamCol]) ?? null;
|
|
160
|
+
for (const [k, v] of Object.entries(r)) {
|
|
161
|
+
out[k] = v;
|
|
162
|
+
if (k === teamCol) out[name] = url;
|
|
163
|
+
}
|
|
164
|
+
if (!Object.hasOwn(out, name)) out[name] = url;
|
|
165
|
+
return out;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export { perspectiveConfig, perspectiveLogoColumn };
|
|
170
|
+
//# sourceMappingURL=perspective.js.map
|
|
171
|
+
//# sourceMappingURL=perspective.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/perspective.ts"],"names":[],"mappings":"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","file":"perspective.js","sourcesContent":["/**\n * Perspective 5.5 (`@perspective-dev/viewer`, with the `-datagrid` and `-charts` plugins): team colours, number formats,\n * a registry table theme's fonts and sign colours, and a logo column for a `<perspective-viewer>`, the JavaScript side of sdvplot's Python `sdvplot.perspective`. Both\n * functions return plain data: a viewer config (the argument of `viewer.restore()`) and rows with a logo URL column.\n * Given the same rows and options, `perspectiveConfig` returns what Python's `perspective_sdv_config` returns\n * (fixtures/perspective/oracle.json holds the Python answers; packages/sdvplot/test/perspective-parity.test.ts compares).\n * Pure: Perspective is never imported, not even its types. Call after `loadLeague(league)` / `preloadAll()`.\n */\nimport { colorPalette } from \"./_web.js\";\nimport { hex6 } from \"./contrast.js\";\nimport { InputError, warn } from \"./errors.js\";\nimport { normSeason } from \"./normalize.js\";\nimport { placeSync } from \"./placement.js\";\nimport type { Value } from \"./resolve.js\";\nimport type { IdSystem, League, SeasonInput, Variant } from \"./types.js\";\n\n/** A row: column name to value, as `perspective.table(rows)` takes them. */\nexport type PerspectiveRow = Readonly<Record<string, unknown>>;\n/** A Perspective column type, as `await table.schema()` gives it. */\nexport type PerspectiveType = \"integer\" | \"float\" | \"string\" | \"boolean\" | \"date\" | \"datetime\";\n/** A Perspective schema: column name to type, as `await table.schema()` gives it. */\nexport type PerspectiveSchema = Readonly<Record<string, PerspectiveType>>;\n/** A `number_format`: Intl.NumberFormat options, e.g. `{ maximumFractionDigits: 2 }`. */\nexport type PerspectiveNumberFormat = { readonly [option: string]: string | number | boolean };\n/** One column's entry in `columns_config`: the keys sdvplot sets. */\nexport type PerspectiveColumnConfig = {\n /** The Datagrid's cell fill: \"series\", one palette colour per distinct value. */\n bg_mode?: \"series\";\n /** The Datagrid's series palette: `linear-gradient(to right, #rrggbb, …)`. */\n bg_color?: string;\n /** A chart's series palette (on a value column) or a scatter's Color slot palette (on the team column). */\n palette?: string;\n number_format?: PerspectiveNumberFormat;\n /** The Datagrid shows a URL column as links. */\n link?: true;\n /** The Datagrid's number colours (a registry theme's): `linear-gradient(to right, <neg> 0%, <pos> 100%)`. */\n fg_color?: string;\n /** The Datagrid's font for a number column: a registry theme's numeric face, when it is not the body face. */\n font_family?: string;\n};\n/** The Datagrid's own settings: a registry theme's body face and size. */\nexport type PerspectivePluginConfig = { font_family: string; font_size: number };\n/** What `perspectiveConfig` returns: pass it to `viewer.restore()`, with your own view keys merged in. */\nexport type PerspectiveConfig = {\n plugin: string;\n /** Perspective's own theme: \"Pro Light\" or \"Pro Dark\" (load `@perspective-dev/viewer/dist/css/pro.css` / `pro-dark.css`). */\n theme: \"Pro Light\" | \"Pro Dark\";\n /** The Datagrid with a registry `theme` only. */\n plugin_config?: PerspectivePluginConfig;\n columns_config: Record<string, PerspectiveColumnConfig>;\n};\n/**\n * The parts of a registry table theme `perspectiveConfig` reads. A `TableThemeSpec` (sdvtables'\n * `tableThemeSpec(name, mode)`, Python's `table_theme_spec`) is one; sdvplot does not ship the registry itself.\n */\nexport interface PerspectiveTheme {\n /** `body`: the grid's face. `numeric`: the number columns' face, when it is not `body`. */\n readonly fonts: { readonly body?: string; readonly numeric?: string };\n /** `body`: the grid's font size in px. */\n readonly sizes: { readonly body: number };\n /** The number colours: `neg` (low) to `pos` (high). */\n readonly palette: { readonly neg: string; readonly pos: string };\n}\n\n/** `perspectiveConfig` options. */\nexport interface PerspectiveConfigOptions {\n /** The column holding the team identifiers (any id system `resolve` reads). */\n teamCol: string;\n /** The SDV league key, e.g. \"nfl\". */\n league: League;\n /** The plugin: \"Datagrid\" (the default) or a chart's name (\"Y Bar\", \"X Bar\", \"Y Line\", \"X/Y Scatter\", …). */\n plugin?: string;\n /** \"light\" (default) or \"dark\": the Perspective theme, \"Pro Light\" or \"Pro Dark\". */\n mode?: \"light\" | \"dark\";\n /**\n * A registry table theme: best as a function of the mode, `(m) => tableThemeSpec(name, m)` from\n * `@sportsdataverse/sdvtables`, called with this call's `mode` on every plugin (so a light-only theme in dark mode\n * throws, as in Python). A spec object works too; it does not say its mode, so resolve it in the same `mode`.\n * The Datagrid takes its body face and size (`plugin_config`), its sign colours on every number column (`fg_color`;\n * Perspective reads only `#hex` and `rgb()` colours there) and, when the numeric face is not the body face, that face\n * on the number columns. Perspective takes each face as one family name, so the page loads the font (Google Fonts)\n * or the browser draws its default. A chart takes none of it.\n */\n theme?: PerspectiveTheme | ((mode: \"light\" | \"dark\") => PerspectiveTheme);\n /** A logo URL column (`perspectiveLogoColumn`) to show as links on the Datagrid, which draws no images. */\n logoCol?: string;\n /** The colour the teams must read against. Default: the Perspective theme's (#ffffff Pro Light, #242526 Pro Dark). */\n background?: string;\n /** Per column, a `number_format` to use instead of the default, or null for none. */\n formats?: Readonly<Record<string, PerspectiveNumberFormat | null>>;\n /** One season for every team. */\n season?: SeasonInput;\n /** How the team values are read. Default \"auto\". */\n idSystem?: IdSystem;\n /** Throw UnresolvedTeamError for a team that does not resolve, instead of one warning and #808080. */\n strict?: boolean;\n /**\n * The column types, as `await table.schema()` gives them. Default: worked out from the rows as Perspective does\n * (numbers, and strings that read as numbers, are \"integer\" or \"float\"; anything else is not a number). Pass it when\n * you load the table with a schema of your own.\n */\n schema?: PerspectiveSchema;\n}\n\n// Perspective's two built-in themes and their --psp--background-color (5.5.1's pro.css and pro-dark.css)\nconst PRO = { light: [\"Pro Light\", \"#ffffff\"], dark: [\"Pro Dark\", \"#242526\"] } as const;\nconst FALLBACK = \"#808080\";\n// A float column whose name says it is a percentage (fg_pct, pct_complete, win_percent) shows 1 decimal; any other\n// float (a rate, EPA) shows 3; an integer is shown as typed (1039 and 2024, not 1,039 or 2,024)\nconst PERCENT = /(^|_)(pct|percent|perc)(_|$)/i;\n\nfunction numberFormat(name: string, type: PerspectiveType): PerspectiveNumberFormat {\n if (type === \"integer\") return { useGrouping: false, maximumFractionDigits: 0 };\n const n = PERCENT.test(name) ? 1 : 3;\n return { minimumFractionDigits: n, maximumFractionDigits: n };\n}\n\nconst INT = /^\\s*-?\\d+\\s*$/;\nconst FLOAT = /^\\s*-?(\\d+\\.?\\d*|\\.\\d+)([eE][-+]?\\d+)?\\s*$/;\n/** A value's Perspective number type, or \"other\" (null for a missing value). */\nfunction kindOf(v: unknown): \"integer\" | \"float\" | \"other\" | null {\n if (v === null || v === undefined) return null;\n if (typeof v === \"bigint\") return \"integer\";\n if (typeof v === \"number\") {\n if (!Number.isFinite(v)) return \"other\";\n return Number.isInteger(v) && Math.abs(v) < 2 ** 31 ? \"integer\" : \"float\";\n }\n if (typeof v === \"string\") return INT.test(v) ? \"integer\" : FLOAT.test(v) ? \"float\" : \"other\";\n return \"other\";\n}\n\n/**\n * The rows' schema, as `perspective.table(rows)` works it out for the number columns (checked against Perspective\n * 5.5.1 in test/perspective.test.ts). ponytail: a non-number column is \"string\" here, whatever Perspective makes of it\n * (a boolean, a date): sdvplot only asks whether a column is a number.\n */\nfunction inferSchema(rows: readonly PerspectiveRow[]): Record<string, PerspectiveType> {\n const kinds = new Map<string, Set<string>>();\n for (const r of rows)\n for (const [k, v] of Object.entries(r)) {\n const s = kinds.get(k) ?? kinds.set(k, new Set()).get(k)!;\n const kind = kindOf(v);\n if (kind !== null) s.add(kind);\n }\n return Object.fromEntries(\n [...kinds].map(([k, s]) => [\n k,\n s.size === 0 || s.has(\"other\") ? \"string\" : s.has(\"float\") ? \"float\" : \"integer\",\n ]),\n );\n}\n\nconst isNumber = (t: PerspectiveType | undefined): boolean => t === \"integer\" || t === \"float\";\nconst quote = (s: string): string => `'${s}'`;\nconst isObj = (v: unknown): v is Record<string, unknown> => typeof v === \"object\" && v !== null;\nconst isFace = (v: unknown): boolean => v === undefined || typeof v === \"string\";\n/** A given theme (or what its function returned) must have every field perspectiveConfig reads. */\nconst isTheme = (t: unknown): t is PerspectiveTheme =>\n isObj(t) &&\n isObj(t.fonts) &&\n isFace(t.fonts.body) &&\n isFace(t.fonts.numeric) &&\n isObj(t.sizes) &&\n typeof t.sizes.body === \"number\" &&\n isObj(t.palette) &&\n typeof t.palette.neg === \"string\" &&\n typeof t.palette.pos === \"string\";\n\nfunction checkColumns(schema: PerspectiveSchema, cols: readonly (string | undefined)[]): void {\n const names = Object.keys(schema);\n for (const c of cols)\n if (c !== undefined && !Object.hasOwn(schema, c))\n throw new InputError(`the rows have no column ${quote(c)} (columns: ${names.join(\", \")})`);\n}\n\n/**\n * The distinct non-null team values in the order a grid or a scatter's Color slot meets them (first appearance), and in\n * a chart's split order: sorted, numerically for a number column, else by code unit as Perspective sorts strings\n * (upper case first). InputError for numbers and strings mixed.\n */\nfunction teamOrders(values: readonly unknown[], numeric: boolean, col: string): [unknown[], unknown[]] {\n const first = [...new Set(values.filter((v) => v !== null && v !== undefined))];\n const types = new Set(first.map((v) => (typeof v === \"bigint\" ? \"number\" : typeof v)));\n if (types.size > 1)\n throw new InputError(\n `the team column ${quote(col)} mixes ${[...types].sort().join(\" and \")} values: use one id system`,\n );\n const key = numeric ? (v: unknown) => Number(v) : (v: unknown) => String(v);\n const sorted = [...first].sort((a, b) => {\n const [x, y] = [key(a), key(b)];\n return x < y ? -1 : x > y ? 1 : 0;\n });\n return [first, sorted];\n}\n\nconst gradient = (colors: readonly string[]): string => `linear-gradient(to right, ${colors.join(\", \")})`;\n\n/**\n * A Perspective viewer config that colours each team by its own colours, for the Datagrid or a chart: the argument of\n * `viewer.restore()`, as Python's `perspective_sdv_config` builds it (same keys, same colours, same order).\n *\n * The colours are the shared team palette (every adapter's, and Python's), taken over the teams in sorted order: each\n * team's colour, made 3:1 against the background and ΔE 20 from every earlier team's, so a team keeps one colour\n * whatever the rows' order. Perspective takes a palette, not a colour per value, so each palette lists those colours in\n * the order Perspective hands colours out (checked in Chromium on Perspective 5.5.1):\n *\n * - `plugin: \"Datagrid\"`: the team column's cells are filled (`bg_mode` \"series\"), in the order the grid first draws\n * each value: the rows' order. Keep the grid in the rows' order (no `sort`) and to about one row per team. Null\n * cells take no colour and no slot. Only a text team column is filled: Perspective fills no number column, so\n * numeric team ids get none, with one warning.\n * - a chart: split a bar, line or area chart by the team (`split_by: [teamCol]`) and each series takes its colour\n * from every number column's `palette`, in sorted order (Perspective sorts split values; a null series comes first,\n * so the palette then starts with the fallback grey). Put the team column in an \"X/Y Scatter\"'s Color slot (its\n * third column) and each team's points take theirs from the team column's `palette`, in the rows' order.\n *\n * Every number column also gets a `number_format` (Intl.NumberFormat options): 3 decimals for a float, 1 for a float\n * named as a percentage (`pct`, `percent`), and an integer plain (2024, not 2,024). `formats` overrides them.\n *\n * @param rows - the rows the viewer shows (what `perspective.table(rows)` loads). With no rows and no `schema` no column is\n * known: the config has `plugin` and `theme` and an empty `columns_config`, without an error (pass the table's\n * `schema` for its number formats, as Python's empty frame keeps its columns).\n * @returns `plugin`, `theme` and `columns_config`; merge your own `group_by`, `split_by`, `columns` and the rest in.\n * @throws InputError for a column the rows do not have (`teamCol`, `logoCol` or a `formats` key), a team column that\n * mixes numbers and strings, an unknown `mode`, a `theme` that is not a spec (or a function returning one), or a\n * season outside the league's. UnresolvedTeamError with `strict: true`. A `theme` function's own errors pass through\n * (sdvtables' TableSpecError for a light-only theme in dark mode).\n *\n * @example\n * ```ts\n * import { loadLeague } from \"@sportsdataverse/sdvplot\";\n * import { perspectiveConfig } from \"@sportsdataverse/sdvplot/perspective\";\n *\n * await loadLeague(\"nfl\");\n * // 2024 regular season, offence: EPA per play (nflverse play-by-play)\n * const epa = [\n * { posteam: \"BAL\", epa_per_play: 0.218 },\n * { posteam: \"BUF\", epa_per_play: 0.1885 },\n * { posteam: \"DET\", epa_per_play: 0.1592 },\n * ];\n * const grid = perspectiveConfig(epa, { teamCol: \"posteam\", league: \"nfl\" });\n * const bars = {\n * ...perspectiveConfig(epa, { teamCol: \"posteam\", league: \"nfl\", plugin: \"Y Bar\" }),\n * group_by: [\"posteam\"],\n * split_by: [\"posteam\"],\n * columns: [\"epa_per_play\"],\n * };\n * // in the browser: await viewer.load(await worker.table(epa)); await viewer.restore(grid)\n * [grid.columns_config.posteam?.bg_color, bars.columns_config.epa_per_play?.palette];\n * ```\n */\nexport function perspectiveConfig(\n rows: readonly PerspectiveRow[],\n o: PerspectiveConfigOptions,\n): PerspectiveConfig {\n const mode = o.mode ?? \"light\";\n if (mode !== \"light\" && mode !== \"dark\")\n throw new InputError(`mode must be 'light' or 'dark', not ${quote(String(mode))}`);\n // resolved before the plugin is known, as Python resolves theme= on every plugin (a light-only theme in dark throws)\n const resolved = typeof o.theme === \"function\" ? o.theme(mode) : o.theme;\n if (o.theme !== undefined && !isTheme(resolved))\n throw new InputError(\n `theme must be a table theme spec or a function of the mode, not ${quote(String(resolved))}: pass (m) => tableThemeSpec(name, m) from @sportsdataverse/sdvtables`,\n );\n const schema = o.schema ?? inferSchema(rows);\n const { teamCol, logoCol } = o;\n // no rows and no schema: no column is known, so none is missing (Python's empty frame still has its columns)\n if (o.schema !== undefined || rows.length > 0)\n checkColumns(schema, [teamCol, logoCol, ...Object.keys(o.formats ?? {})]);\n const [pro, proBg] = PRO[mode];\n const bg = o.background === undefined ? proBg : hex6(o.background);\n const season = normSeason(o.season, o.league);\n const text = !isNumber(schema[teamCol]); // Perspective fills a string column's cells, never a number column's\n const numeric = Object.keys(schema).filter((c) => c !== teamCol && isNumber(schema[c]));\n const grid = (o.plugin ?? \"Datagrid\") === \"Datagrid\";\n const values = rows.map((r) => r[teamCol]);\n const [first, ascending] = teamOrders(values, !text, teamCol);\n // the shared team palette every adapter uses (Python's perspective._palette): unresolved values grey, out of it\n const pal = colorPalette(o.league, ascending, {\n ...(season === null ? {} : { season }),\n background: bg,\n idSystem: o.idSystem ?? \"auto\",\n strict: o.strict ?? false,\n fallback: FALLBACK,\n });\n const color = new Map(ascending.map((v, i) => [v, pal[i] as string]));\n // a null split series comes first: palette entry 0\n const nulls = values.some((v) => v === null || v === undefined) ? [FALLBACK] : [];\n const byAppearance = gradient(first.map((v) => color.get(v) as string));\n const byValue = gradient([...nulls, ...pal]);\n let columns: Record<string, PerspectiveColumnConfig> = {};\n for (const c of numeric) {\n const fmt =\n o.formats !== undefined && Object.hasOwn(o.formats, c) ? o.formats[c] : numberFormat(c, schema[c]!);\n columns[c] = fmt === null || fmt === undefined ? {} : { number_format: fmt };\n }\n // no teams (no rows) means no palette at all: an empty palette is not a valid CSS value\n if (first.length > 0 && !grid) {\n for (const c of numeric) columns[c]!.palette = byValue;\n if (text) columns = { [teamCol]: { palette: byAppearance }, ...columns }; // a number Color slot is a gradient\n } else if (first.length > 0 && text) {\n columns = { [teamCol]: { bg_mode: \"series\", bg_color: byAppearance }, ...columns };\n } else if (first.length > 0) {\n warn(\n `perspective:numeric-team:${teamCol}`,\n `no team colors on the Datagrid: the team column ${quote(teamCol)} is numeric, and team colors need a string or categorical team column (Perspective fills only text cells)`,\n );\n }\n const theme = grid ? resolved : undefined; // a chart takes no fonts from a config\n if (theme !== undefined) {\n const { body, numeric: face } = theme.fonts;\n const sign = `linear-gradient(to right, ${theme.palette.neg} 0%, ${theme.palette.pos} 100%)`;\n for (const c of numeric) {\n columns[c]!.fg_color = sign;\n if (face && face !== body) columns[c]!.font_family = face;\n }\n }\n if (grid && logoCol !== undefined) columns[logoCol] = { link: true };\n const gridFace = theme?.fonts.body;\n return {\n plugin: o.plugin ?? \"Datagrid\",\n theme: pro,\n ...(theme !== undefined && gridFace\n ? { plugin_config: { font_family: gridFace, font_size: theme.sizes.body } }\n : {}),\n columns_config: Object.fromEntries(Object.entries(columns).filter(([, v]) => Object.keys(v).length > 0)),\n };\n}\n\n/** `perspectiveLogoColumn` options. */\nexport interface PerspectiveLogoOptions {\n /** The SDV league key, e.g. \"nfl\". */\n league: League;\n /** The new column's name. Default: `${teamCol}_logo`. It must not be a column already. */\n name?: string;\n /** One season for every team: the logo of that season (relocations and rebrands). */\n season?: SeasonInput;\n /** \"default\", \"dark\", or a named variant from `marks()`. */\n variant?: Variant;\n /** How the team values are read. Default \"auto\". */\n idSystem?: IdSystem;\n /** Throw UnresolvedTeamError for a team that does not resolve, instead of warning. */\n strict?: boolean;\n}\n\n/**\n * The rows with a column of each team's logo URL, right after the team column, as Python's `perspective_logo_column`\n * adds it. Each distinct team is resolved once. A team that does not resolve, or has no archived logo, gets a null URL,\n * with one warning for each reason. Show the column as links with `perspectiveConfig(rows, { logoCol })`: Perspective\n * 5.5's Datagrid draws no images. The rows given are not changed.\n *\n * @returns new rows, each with the URL (a string, or null) in `name`.\n * @throws InputError for a `teamCol` no row has, a `name` a row already has, or a season outside the league's.\n * UnresolvedTeamError with `strict: true`.\n *\n * @example\n * ```ts\n * import { loadLeague } from \"@sportsdataverse/sdvplot\";\n * import { perspectiveConfig, perspectiveLogoColumn } from \"@sportsdataverse/sdvplot/perspective\";\n *\n * await loadLeague(\"nfl\");\n * const epa = [\n * { posteam: \"BAL\", epa_per_play: 0.218 },\n * { posteam: \"BUF\", epa_per_play: 0.1885 },\n * ];\n * const rows = perspectiveLogoColumn(epa, \"posteam\", { league: \"nfl\" });\n * const config = perspectiveConfig(rows, { teamCol: \"posteam\", league: \"nfl\", logoCol: \"posteam_logo\" });\n * [Object.keys(rows[0] ?? {}), config.columns_config.posteam_logo];\n * ```\n */\nexport function perspectiveLogoColumn<R extends PerspectiveRow>(\n rows: readonly R[],\n teamCol: string,\n o: PerspectiveLogoOptions,\n): (R & Record<string, string | null>)[] {\n const name = o.name ?? `${teamCol}_logo`;\n if (rows.length > 0 && !rows.some((r) => Object.hasOwn(r, teamCol)))\n throw new InputError(`the rows have no column ${quote(teamCol)}`);\n if (rows.some((r) => Object.hasOwn(r, name)))\n throw new InputError(`the rows already have a column ${quote(name)}: pass another name`);\n const values = [...new Set(rows.map((r) => r[teamCol]).filter((v) => v !== null && v !== undefined))];\n const at = values.map((_, i) => i);\n const placed = placeSync(\n at,\n at.map(() => 0),\n values as Value[],\n {\n league: o.league,\n season: normSeason(o.season, o.league),\n kind: \"logo\",\n variant: o.variant ?? \"default\",\n idSystem: o.idSystem ?? \"auto\",\n strict: o.strict ?? false,\n },\n );\n const urls = new Map<unknown, string>(placed.map((p) => [values[p.index], p.url]));\n return rows.map((r) => {\n const out: Record<string, unknown> = {};\n const url = urls.get(r[teamCol]) ?? null;\n for (const [k, v] of Object.entries(r)) {\n out[k] = v;\n if (k === teamCol) out[name] = url;\n }\n if (!Object.hasOwn(out, name)) out[name] = url;\n return out as R & Record<string, string | null>;\n });\n}\n"]}
|