sdvplot 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. sdvplot/__init__.py +68 -0
  2. sdvplot/_cache.py +484 -0
  3. sdvplot/_colors.py +206 -0
  4. sdvplot/_contrast.py +60 -0
  5. sdvplot/_court.py +110 -0
  6. sdvplot/_deprecate.py +39 -0
  7. sdvplot/_dispatch.py +342 -0
  8. sdvplot/_errors.py +93 -0
  9. sdvplot/_headshots.py +131 -0
  10. sdvplot/_images.py +292 -0
  11. sdvplot/_index.py +178 -0
  12. sdvplot/_lazy.py +15 -0
  13. sdvplot/_manifest.py +73 -0
  14. sdvplot/_marks.py +296 -0
  15. sdvplot/_normalize.py +82 -0
  16. sdvplot/_placement.py +168 -0
  17. sdvplot/_resolve.py +363 -0
  18. sdvplot/_surface.py +220 -0
  19. sdvplot/_tables.py +87 -0
  20. sdvplot/_tiers.py +135 -0
  21. sdvplot/_types.py +38 -0
  22. sdvplot/_versions.py +30 -0
  23. sdvplot/_web.py +61 -0
  24. sdvplot/altair.py +652 -0
  25. sdvplot/bokeh.py +323 -0
  26. sdvplot/data/INDEX_VERSION +1 -0
  27. sdvplot/data/aliases.parquet +0 -0
  28. sdvplot/data/teams.parquet +0 -0
  29. sdvplot/folium.py +316 -0
  30. sdvplot/great_tables/__init__.py +267 -0
  31. sdvplot/great_tables/_cells.py +1814 -0
  32. sdvplot/great_tables/_export.py +844 -0
  33. sdvplot/great_tables/_layout.py +1802 -0
  34. sdvplot/great_tables/_marks.py +798 -0
  35. sdvplot/great_tables/_themes.py +2134 -0
  36. sdvplot/holoviews.py +303 -0
  37. sdvplot/matplotlib.py +934 -0
  38. sdvplot/plotly.py +595 -0
  39. sdvplot/plotnine.py +1104 -0
  40. sdvplot/plottable.py +141 -0
  41. sdvplot/py.typed +0 -0
  42. sdvplot/pygal.py +416 -0
  43. sdvplot/reactable.py +488 -0
  44. sdvplot/testing.py +579 -0
  45. sdvplot/typing.py +21 -0
  46. sdvplot-0.1.0.dist-info/METADATA +188 -0
  47. sdvplot-0.1.0.dist-info/RECORD +49 -0
  48. sdvplot-0.1.0.dist-info/WHEEL +4 -0
  49. sdvplot-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,798 @@
1
+ """Team marks and team identity in great_tables: ports of sdvplotR's R/gt_sdv.R and R/gt_theme_sdv.R."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import html
7
+ from collections.abc import Callable
8
+ from typing import Any
9
+
10
+ import narwhals as nw
11
+ from great_tables import GT, google_font, loc, px, random_id, style
12
+ from great_tables import html as gt_html
13
+ from great_tables._tbl_data import get_column_names
14
+
15
+ from sdvplot._colors import team_colors
16
+ from sdvplot._contrast import contrast, mix, on_color, solid
17
+ from sdvplot._errors import InputError, warn
18
+ from sdvplot._placement import KINDS, _missing
19
+ from sdvplot._resolve import one_team, resolve
20
+ from sdvplot._tables import check_px, mark_html
21
+ from sdvplot._types import HeadshotIdSystem, IdSystem
22
+
23
+
24
+ def _check_gt(gt: Any, arg: str = "gt") -> GT:
25
+ """sdvplotR's ``.check_gt``: every public table function takes a GT, never raw data; returns ``gt``."""
26
+ if isinstance(gt, GT):
27
+ return gt
28
+ if hasattr(gt, "columns"):
29
+ hint = "It looks like raw data: wrap it in great_tables.GT() first."
30
+ else:
31
+ hint = "Build a table with great_tables.GT() and pass that in."
32
+ raise TypeError(f"{arg} must be a great_tables GT, not {type(gt).__name__}. {hint}")
33
+
34
+
35
+ def _check_columns(gt: GT, columns: Any) -> None:
36
+ """Column names the table lacks are a ValueError on every backend (great_tables raised polars' own error and
37
+ matched nothing on pandas). Selectors and other expressions pass through to great_tables."""
38
+ names = [columns] if isinstance(columns, str) else columns
39
+ if isinstance(names, (list, tuple)) and all(isinstance(n, str) for n in names):
40
+ have = [str(c) for c in get_column_names(gt._tbl_data)] # pandas, polars or pyarrow
41
+ if missing := [n for n in names if n not in have]:
42
+ raise ValueError(f"column(s) {missing} not in the table; its columns are {have}")
43
+
44
+
45
+ def _cell_texts(gt: GT, locations: Any) -> list[str]:
46
+ """The distinct texts great_tables hands a text_transform at ``locations``, read by rendering once."""
47
+ seen: list[str] = []
48
+
49
+ def record(text: str) -> str:
50
+ seen.append(text)
51
+ return text
52
+
53
+ gt.text_transform(locations, record).as_raw_html()
54
+ return list(dict.fromkeys(seen))
55
+
56
+
57
+ def _image_cells(
58
+ gt: GT,
59
+ columns: Any,
60
+ locations: Any,
61
+ *,
62
+ kind: str,
63
+ league: str,
64
+ height: Any,
65
+ season: Any = None,
66
+ include_name: bool = False,
67
+ id_system: str = "auto",
68
+ strict: bool = False,
69
+ ) -> GT:
70
+ """Cells at ``locations`` (default: the body of ``columns``) whose text resolves become their mark's <img>.
71
+
72
+ Resolution happens here, at call time, so unknown values warn once now rather than at every render (spec T4).
73
+ great_tables passes cells as escaped HTML text ("A&amp;M"), so values are unescaped before resolving.
74
+ """
75
+ _check_gt(gt)
76
+ h = check_px(height)
77
+ if locations is None:
78
+ _check_columns(gt, columns)
79
+ locs = loc.body(columns) if locations is None else locations
80
+ for where in locs if isinstance(locs, list) else [locs]:
81
+ if isinstance(where, loc.body): # loc.body(columns=...) names columns too
82
+ _check_columns(gt, where.columns)
83
+ # the locations great_tables' text_transform reaches; it escapes column labels and ignores every other one
84
+ if not isinstance(where, (loc.body, loc.stub, loc.row_groups)):
85
+ hint = (
86
+ "; for marks in the column labels use gt_sdv_cols_label()"
87
+ if isinstance(where, loc.column_labels)
88
+ else ""
89
+ )
90
+ raise ValueError(
91
+ "locations must be loc.body(), loc.stub() or loc.row_groups(), or a list of them, "
92
+ f"not {type(where).__name__}{hint}"
93
+ )
94
+ texts = _cell_texts(gt, locs)
95
+ imgs = mark_html([html.unescape(t) for t in texts], league=league, kind=kind, height=h, season=season,
96
+ include_name=include_name, id_system=id_system, strict=strict) # fmt: skip
97
+ lookup = {t: img + (t if include_name else "") for t, img in zip(texts, imgs, strict=True) if img is not None}
98
+ return gt.text_transform(locs, lambda text: lookup.get(text, text))
99
+
100
+
101
+ def gt_sdv_logos(
102
+ gt: GT,
103
+ columns: Any,
104
+ *,
105
+ league: str,
106
+ height: Any = 30,
107
+ locations: Any = None,
108
+ include_name: bool = False,
109
+ season: Any = None,
110
+ id_system: IdSystem = "auto",
111
+ strict: bool = False,
112
+ ) -> GT:
113
+ """Show each cell's team as its logo in a great_tables table.
114
+
115
+ Values are resolved when you call this (team abbreviations, names and provider ids, as in ``resolve()``), so
116
+ unknown values warn once, now. They keep their text. The cell text is read as it renders now, so apply any
117
+ ``fmt_*`` to the same column before this.
118
+
119
+ Args:
120
+ gt: A great_tables ``GT``.
121
+ columns: The columns whose body cells become logos: a name, a list of names, or a polars selector. Ignored when
122
+ ``locations`` is given (pass None).
123
+ league: The SDV league key, e.g. "nfl".
124
+ height: The image height in pixels.
125
+ locations: Instead of the body of ``columns``: ``loc.body()``, ``loc.stub()`` or ``loc.row_groups()``, or a
126
+ list of them (the locations great_tables' ``text_transform`` reaches). For marks in the column labels,
127
+ use ``gt_sdv_cols_label``.
128
+ include_name: Keep the cell's text after the logo.
129
+ season: One season whose marks every cell shows (the ending year for the NHL, NBA, MBB and WBB); None for
130
+ today's.
131
+ id_system: The id system of the cell values, as in ``resolve``: "auto" tries each in order; NHL stats ids
132
+ need "nhl_id".
133
+ strict: Raise UnresolvedTeamError instead of warning when a value does not resolve.
134
+
135
+ Returns:
136
+ GT: A new table; ``gt`` is unchanged.
137
+
138
+ Raises:
139
+ TypeError: If ``gt`` is not a great_tables GT.
140
+ InputError: (a ValueError) If ``height`` is not a number of pixels of at least 1, ``league`` or ``id_system`` is
141
+ unknown, or ``season`` is not one year or is outside the seasons sdvplot knows for the league.
142
+ ValueError: If ``columns`` names a column the table lacks, or ``locations`` holds another location.
143
+ UnresolvedTeamError: (a ValueError) If ``strict=True`` and a value does not resolve.
144
+ OfflineError: If the logo manifest cannot be downloaded and no cached copy exists (a DownloadError, also an
145
+ OSError, when the CDN answers with an error status).
146
+ UnsafeDownloadError: (an OSError) If the manifest download is refused: larger than the byte cap, past the
147
+ deadline, or redirected away from https.
148
+
149
+ Example:
150
+ ::
151
+
152
+ from great_tables import GT
153
+ from sdvplot.great_tables import gt_sdv_logos
154
+ import polars as pl
155
+
156
+ df = pl.DataFrame(
157
+ {
158
+ "team": ["KC", "BUF", "BAL"],
159
+ "espn_id": ["3139477", "3918298", "3916387"],
160
+ "wins": [12, 10, 9],
161
+ }
162
+ )
163
+
164
+ gt_sdv_logos(GT(df), "team", league="nfl", height=24)
165
+
166
+ See Also:
167
+ Ported from sdvplotR ``gt_sdv_logos()``: https://sdvplotR.sportsdataverse.org/reference/gt_sdv_logos.html ;
168
+ great_tables: https://posit-dev.github.io/great-tables/
169
+ """
170
+ return _image_cells(gt, columns, locations, kind="logo", league=league, height=height, season=season,
171
+ include_name=include_name, id_system=id_system, strict=strict) # fmt: skip
172
+
173
+
174
+ def gt_sdv_wordmarks(
175
+ gt: GT,
176
+ columns: Any,
177
+ *,
178
+ league: str,
179
+ height: Any = 30,
180
+ locations: Any = None,
181
+ season: Any = None,
182
+ id_system: IdSystem = "auto",
183
+ strict: bool = False,
184
+ ) -> GT:
185
+ """Show each cell's team as its wordmark in a great_tables table.
186
+
187
+ Args:
188
+ gt: A great_tables ``GT``.
189
+ columns: The columns whose body cells become wordmarks. Ignored when ``locations`` is given (pass None).
190
+ league: The SDV league key, e.g. "nfl".
191
+ height: The image height in pixels.
192
+ locations: Instead of the body of ``columns``: ``loc.body()``, ``loc.stub()`` or ``loc.row_groups()``, or a
193
+ list of them (the locations great_tables' ``text_transform`` reaches). For marks in the column labels,
194
+ use ``gt_sdv_cols_label``.
195
+ season: One season whose marks every cell shows; None for today's.
196
+ id_system: The id system of the cell values, as in ``resolve``: "auto" tries each in order; NHL stats ids
197
+ need "nhl_id".
198
+ strict: Raise UnresolvedTeamError instead of warning when a value does not resolve.
199
+
200
+ Returns:
201
+ GT: A new table; unknown values keep their text, with one SdvplotWarning now.
202
+
203
+ Raises:
204
+ TypeError: If ``gt`` is not a great_tables GT.
205
+ InputError: (a ValueError) If ``height`` is not a number of pixels of at least 1, ``league`` or ``id_system`` is
206
+ unknown, or ``season`` is not one year or is outside the seasons sdvplot knows for the league.
207
+ ValueError: If ``columns`` names a column the table lacks, or ``locations`` holds another location.
208
+ UnresolvedTeamError: (a ValueError) If ``strict=True`` and a value does not resolve.
209
+ OfflineError: If the logo manifest cannot be downloaded and no cached copy exists (a DownloadError, also an
210
+ OSError, when the CDN answers with an error status).
211
+ UnsafeDownloadError: (an OSError) If the manifest download is refused: larger than the byte cap, past the
212
+ deadline, or redirected away from https.
213
+
214
+ Example:
215
+ ::
216
+
217
+ from great_tables import GT
218
+ from sdvplot.great_tables import gt_sdv_wordmarks
219
+ import polars as pl
220
+
221
+ df = pl.DataFrame(
222
+ {
223
+ "team": ["KC", "BUF", "BAL"],
224
+ "espn_id": ["3139477", "3918298", "3916387"],
225
+ "wins": [12, 10, 9],
226
+ }
227
+ )
228
+
229
+ gt_sdv_wordmarks(GT(df), "team", league="nfl")
230
+
231
+ See Also:
232
+ Ported from sdvplotR ``gt_sdv_wordmarks()``: https://sdvplotR.sportsdataverse.org/reference/gt_sdv_wordmarks.html
233
+ """
234
+ return _image_cells(gt, columns, locations, kind="wordmark", league=league, height=height, season=season,
235
+ id_system=id_system, strict=strict) # fmt: skip
236
+
237
+
238
+ def gt_sdv_headshots(
239
+ gt: GT, columns: Any, *, league: str, height: Any = 30, locations: Any = None, id_system: str = "espn"
240
+ ) -> GT:
241
+ """Show each cell's player id as the player's headshot in a great_tables table.
242
+
243
+ Args:
244
+ gt: A great_tables ``GT``.
245
+ columns: The columns of player ids. Ignored when ``locations`` is given (pass None).
246
+ league: The SDV league key, e.g. "nfl".
247
+ height: The image height in pixels.
248
+ locations: Instead of the body of ``columns``: ``loc.body()``, ``loc.stub()`` or ``loc.row_groups()``, or a
249
+ list of them (the locations great_tables' ``text_transform`` reaches). For marks in the column labels,
250
+ use ``gt_sdv_cols_label``.
251
+ id_system: "espn" (ESPN athlete ids, any ESPN league) or "gsis" (NFL), as in ``headshot_url``.
252
+
253
+ Returns:
254
+ GT: A new table; ids without a headshot keep their text, with one SdvplotWarning now.
255
+
256
+ Raises:
257
+ TypeError: If ``gt`` is not a great_tables GT.
258
+ InputError: (a ValueError) If ``height`` is not a number of pixels of at least 1, ``league`` has no ESPN
259
+ headshots, or ``id_system`` is not valid for ``league``.
260
+ ValueError: If ``columns`` names a column the table lacks, or ``locations`` holds another location.
261
+ OfflineError: If ``id_system`` is "gsis" and the nflverse player table cannot be downloaded and no cached copy
262
+ exists (a DownloadError, also an OSError, when GitHub answers with an error status).
263
+ UnsafeDownloadError: (an OSError) If ``id_system`` is "gsis" and the player table download is refused: larger
264
+ than the byte cap, past the deadline, or redirected away from https.
265
+
266
+ Example:
267
+ ::
268
+
269
+ from great_tables import GT
270
+ from sdvplot.great_tables import gt_sdv_headshots
271
+ import polars as pl
272
+
273
+ df = pl.DataFrame(
274
+ {
275
+ "team": ["KC", "BUF", "BAL"],
276
+ "espn_id": ["3139477", "3918298", "3916387"],
277
+ "wins": [12, 10, 9],
278
+ }
279
+ )
280
+
281
+ gt_sdv_headshots(GT(df), "espn_id", league="nfl", height=40)
282
+
283
+ See Also:
284
+ Ported from sdvplotR ``gt_sdv_headshots()``: https://sdvplotR.sportsdataverse.org/reference/gt_sdv_headshots.html
285
+ """
286
+ return _image_cells(gt, columns, locations, kind="headshot", league=league, height=height, id_system=id_system)
287
+
288
+
289
+ def gt_sdv_cols_label(
290
+ gt: GT,
291
+ columns: Any = None,
292
+ *,
293
+ league: str,
294
+ height: Any = 30,
295
+ season: Any = None,
296
+ mark_type: str = "logo",
297
+ id_system: IdSystem | HeadshotIdSystem | None = None,
298
+ strict: bool = False,
299
+ ) -> GT:
300
+ """Replace the labels of team-named columns (a ``KC`` column, a ``BUF`` column, ...) with their marks.
301
+
302
+ Args:
303
+ gt: A great_tables ``GT``.
304
+ columns: The columns whose labels to replace: a name, a list, a polars selector, or None for every column.
305
+ The column *names* are resolved (not labels set earlier with ``cols_label``).
306
+ league: The SDV league key, e.g. "nfl".
307
+ height: The image height in pixels.
308
+ season: One season whose marks to show; None for today's.
309
+ mark_type: "logo", "wordmark", or "headshot" (the column names are player ids).
310
+ id_system: The id system of the column names: for logos and wordmarks one of ``resolve``'s (None means
311
+ "auto"; NHL stats ids need "nhl_id"), for headshots "espn" or "gsis" as in ``headshot_url`` (None means
312
+ "espn").
313
+ strict: Raise UnresolvedTeamError instead of warning when a column name does not resolve to a team.
314
+
315
+ Returns:
316
+ GT: A new table; columns whose names do not resolve keep their labels, with one SdvplotWarning now.
317
+
318
+ Raises:
319
+ TypeError: If ``gt`` is not a great_tables GT.
320
+ InputError: (a ValueError) If ``mark_type`` is not "logo", "wordmark" or "headshot", ``height`` is not a number
321
+ of pixels of at least 1, ``league`` or ``id_system`` is unknown (for headshots: ``league`` has no ESPN
322
+ headshots, or ``id_system`` is not valid for it), or ``season`` is not one year or is outside the seasons
323
+ sdvplot knows for the league.
324
+ UnresolvedTeamError: (a ValueError) If ``strict=True`` and a column name does not resolve.
325
+ OfflineError: If the logo manifest (logos and wordmarks), or for "gsis" headshots the nflverse player table,
326
+ cannot be downloaded and no cached copy exists (a DownloadError, also an OSError, for an HTTP error status).
327
+ UnsafeDownloadError: (an OSError) If that download is refused: larger than the byte cap, past the deadline, or
328
+ redirected away from https.
329
+
330
+ Example:
331
+ ::
332
+
333
+ from great_tables import GT
334
+ from sdvplot.great_tables import gt_sdv_cols_label
335
+ import polars as pl
336
+
337
+ df = pl.DataFrame({"KC": [12], "BUF": [10], "SF": [9]})
338
+
339
+ gt_sdv_cols_label(GT(df), ["KC", "BUF", "SF"], league="nfl")
340
+
341
+ See Also:
342
+ Ported from sdvplotR ``gt_sdv_cols_label()``: https://sdvplotR.sportsdataverse.org/reference/gt_sdv_cols_label.html
343
+ """
344
+ _check_gt(gt)
345
+ h = check_px(height)
346
+ if mark_type not in KINDS:
347
+ raise InputError(f"mark_type must be one of {list(KINDS)}, got {mark_type!r}")
348
+ if id_system is None:
349
+ id_system = "espn" if mark_type == "headshot" else "auto"
350
+ names: list[str] = []
351
+
352
+ def record(name: str) -> str:
353
+ names.append(name)
354
+ return name
355
+
356
+ gt.cols_label_with(columns=columns, fn=record) # great_tables' own column selection; the result is discarded
357
+ imgs = mark_html(names, league=league, kind=mark_type, height=h, season=season, id_system=id_system,
358
+ strict=strict) # fmt: skip
359
+ cases: dict[str, Any] = {n: gt_html(img) for n, img in zip(names, imgs, strict=True) if img is not None}
360
+ return gt.cols_label(cases=cases) if cases else gt
361
+
362
+
363
+ def _constant(value: str) -> Callable[[Any], str]:
364
+ """A great_tables ``fmt`` function that ignores the cell and returns ``value`` (sdvplotR's ``.constant``)."""
365
+ return lambda _: value
366
+
367
+
368
+ def _text(value: Any) -> str:
369
+ return "" if _missing(value) else html.escape(str(value))
370
+
371
+
372
+ def gt_merge_stack_team_color(
373
+ gt: GT,
374
+ col1: str,
375
+ col2: str,
376
+ team_col: str,
377
+ *,
378
+ league: str,
379
+ font_size_top: float = 14,
380
+ font_size_bottom: float = 12,
381
+ color: str = "black",
382
+ id_system: IdSystem = "auto",
383
+ strict: bool = False,
384
+ ) -> GT:
385
+ """Stack ``col1`` over ``col2`` in one cell: the top in bold small caps, the bottom in the team's primary color.
386
+
387
+ Args:
388
+ gt: A great_tables ``GT``.
389
+ col1: The column shown on top (bold small caps, in ``color``); it holds the merged cell.
390
+ col2: The column shown below, smaller and in the row's team color; it is hidden.
391
+ team_col: The column of teams whose primary colors color the bottom line.
392
+ league: The SDV league key, e.g. "nfl".
393
+ font_size_top: The top line's font size in pixels.
394
+ font_size_bottom: The bottom line's font size in pixels.
395
+ color: The top line's CSS color.
396
+ id_system: The id system of ``team_col``, as in ``resolve``: "auto" tries each in order; NHL stats ids need
397
+ "nhl_id".
398
+ strict: Raise UnresolvedTeamError instead of warning when a team does not resolve.
399
+
400
+ Returns:
401
+ GT: A new table. A team that does not resolve, or has no color, gets grey, with one SdvplotWarning.
402
+
403
+ Raises:
404
+ TypeError: If ``gt`` is not a great_tables GT.
405
+ InputError: (a ValueError) If ``league`` or ``id_system`` is unknown.
406
+ ValueError: If ``col1``, ``col2`` or ``team_col`` is not a column of the table's data.
407
+ UnresolvedTeamError: (a ValueError) If ``strict=True`` and a team does not resolve.
408
+
409
+ Example:
410
+ ::
411
+
412
+ from great_tables import GT
413
+ from sdvplot.great_tables import gt_merge_stack_team_color
414
+ import polars as pl
415
+
416
+ df = pl.DataFrame({"team": ["KC", "BUF"], "mascot": ["Chiefs", "Bills"]})
417
+
418
+ gt_merge_stack_team_color(GT(df), "team", "mascot", "team", league="nfl")
419
+
420
+ See Also:
421
+ Ported from sdvplotR ``gt_merge_stack_team_color()``:
422
+ https://sdvplotR.sportsdataverse.org/reference/gt_merge_stack_team_color.html
423
+ """
424
+ _check_gt(gt)
425
+ frame = nw.from_native(gt._tbl_data, eager_only=True) # the GT's own data, positional (spec T4)
426
+ for name in (col1, col2, team_col):
427
+ if name not in frame.columns:
428
+ raise ValueError(f"{name!r} is not a column of the table's data; columns are {frame.columns}")
429
+ colors = team_colors(league, frame[team_col].to_list(), id_system=id_system, strict=strict)
430
+ top_style = f"font-weight:bold;font-variant:small-caps;color:{color};font-size:{font_size_top}px"
431
+ for row, (top, bottom, team_color) in enumerate(
432
+ zip(frame[col1].to_list(), frame[col2].to_list(), colors, strict=True)
433
+ ):
434
+ cell = (
435
+ f"<div style='line-height:{font_size_top - 2}px'><span style='{top_style}'>{_text(top)}</span></div>\n"
436
+ f"<div style='line-height:{font_size_bottom - 2}px'><span style='font-weight:bold;"
437
+ f"color:{team_color or 'grey'};font-size:{font_size_bottom}px'>{_text(bottom)}</span></div>"
438
+ )
439
+ # fmt() takes data rows, so row groups (which reorder the display) cannot shuffle the pairs
440
+ # one fmt() per row keeps it simple; group identical cells into one call if long tables get slow
441
+ gt = gt.fmt(_constant(cell), columns=col1, rows=[row])
442
+ return gt.cols_hide(col2)
443
+
444
+
445
+ # --- themes (R/gt_theme_sdv.R) ---
446
+
447
+ DENSITY: dict[str, dict[str, int]] = { # sdvplotR's .theme_density(): type sizes and row padding per density
448
+ "comfortable": {"body": 14, "pad": 6, "title": 26, "subtitle": 15, "label": 10, "group": 11, "source": 11},
449
+ "compact": {"body": 12, "pad": 3, "title": 22, "subtitle": 13, "label": 9, "group": 10, "source": 10},
450
+ "social": {"body": 17, "pad": 9, "title": 34, "subtitle": 19, "label": 12, "group": 13, "source": 13},
451
+ }
452
+
453
+
454
+ def _density(density: str) -> dict[str, int]:
455
+ """The type and padding scale of ``density``; ValueError for an unknown one."""
456
+ if density not in DENSITY:
457
+ raise ValueError(f"density must be 'comfortable', 'compact' or 'social', not {density!r}")
458
+ return DENSITY[density]
459
+
460
+
461
+ # gt::default_fonts() (gt 1.3.0): the fallback stack R puts under every theme font
462
+ R_FONTS = (
463
+ "system-ui",
464
+ "Segoe UI",
465
+ "Roboto",
466
+ "Helvetica",
467
+ "Arial",
468
+ "sans-serif",
469
+ "Apple Color Emoji",
470
+ "Segoe UI Emoji",
471
+ "Segoe UI Symbol",
472
+ "Noto Color Emoji",
473
+ )
474
+
475
+
476
+ def _record(gt: GT, name: str, value: Any) -> GT:
477
+ """A copy of ``gt`` carrying ``value`` as attribute ``name`` (``_sdvplot_scale``, ``_sdvplot_key``,
478
+ ``_sdvplot_theme_fonts``); great_tables' own methods copy it along."""
479
+ out = copy.copy(gt)
480
+ out.__dict__[name] = value
481
+ return out
482
+
483
+
484
+ def _table_font(gt: GT, font: Any, weight: Any = None) -> GT:
485
+ """R's ``opt_table_font(font = list(google_font(x), default_fonts()))`` for a theme's ``font`` (a GoogleFont).
486
+
487
+ As in R, the theme's fonts go in front of the table's, so fonts the caller set stay behind them (a browser falls
488
+ back font by font for each character it cannot draw, which is why gt lists emoji fonts after ``sans-serif``).
489
+ Unlike R, the run of fonts an earlier sdvplot theme put in front (recorded as ``_sdvplot_theme_fonts``) is taken
490
+ out first, so re-theming swaps it instead of stacking. great_tables writes each font once in the CSS.
491
+ """
492
+ ours = [font.get_font_name(), *R_FONTS]
493
+ fonts = list(gt._options.table_font_names.value)
494
+ earlier = list(gt.__dict__.get("_sdvplot_theme_fonts", ()))
495
+ at = next(
496
+ (i for i in range(len(fonts) - len(earlier) + 1) if earlier and fonts[i : i + len(earlier)] == earlier), None
497
+ )
498
+ if at is not None: # still in the list (fonts the caller added since sit around it, untouched)
499
+ del fonts[at : at + len(earlier)]
500
+ out = gt.opt_table_font(font=[font, *R_FONTS, *fonts], add=False, weight=weight)
501
+ return _record(out, "_sdvplot_theme_fonts", ours)
502
+
503
+
504
+ # great_tables' default paddings that sdvplotR's density also scales (gt has the same defaults)
505
+ _DEFAULT_PADDING = {
506
+ "row_group_padding": 8,
507
+ "source_notes_padding": 4,
508
+ "summary_row_padding": 8,
509
+ "grand_summary_row_padding": 8,
510
+ }
511
+ SDV_NAVY, SDV_CYAN = "#0B1A33", "#7FE6DC"
512
+ SDV_HORIZON = "linear-gradient(90deg, #3346F0, #7FE6DC)"
513
+
514
+
515
+ def important(*styles: Any) -> Any:
516
+ """great_tables cell styles as one inline rule with every declaration ``!important``. In VS Code and Positron
517
+ notebooks (great_tables' ``_repr_html_`` finds ``VSCODE_PID`` or ``POSITRON_VERSION`` and turns on its
518
+ ``all_important``), every declaration of great_tables' own stylesheet is ``!important`` (``td, th {border-style:
519
+ none}``, the stub's, row groups' and stripes' ``background-color``), and a stylesheet ``!important`` beats a plain
520
+ inline style: without this, a border or fill shows in Jupyter, Quarto, Databricks and a saved image but not in VS
521
+ Code or Positron. Elsewhere the repr's stylesheet has no ``!important`` at all."""
522
+ rule = "".join(s._to_html_style() for s in styles)
523
+ return style.css(rule=" ".join(f"{d.strip()} !important;" for d in rule.split(";") if d.strip()))
524
+
525
+
526
+ def _warn_striped(gt: GT, helper: str, covered: str) -> None:
527
+ """One SdvplotWarning when ``gt`` stripes its body rows and great_tables' repr here marks its stylesheet
528
+ ``!important`` (VS Code and Positron; not Jupyter, Quarto, Databricks or saved files). The stripes' background and
529
+ text color then beat a plain inline style on every other row, such as ``data_color``'s fills, which sdvplot cannot
530
+ wrap in ``important()``."""
531
+ from great_tables._render import infer_render_env_defaults
532
+
533
+ if gt._options.row_striping_include_table_body.value and infer_render_env_defaults()["all_important"]:
534
+ warn(
535
+ f"{helper}: row striping is on, and in VS Code and Positron notebooks great_tables marks the stripes "
536
+ f"!important, so every other row shows the stripe's colors instead of the {covered} this draws; turn "
537
+ "striping off with opt_row_striping(row_striping=False)"
538
+ )
539
+
540
+
541
+ def _table_id(gt: GT) -> tuple[GT, str]:
542
+ """The table's id, so CSS can be scoped to ``#id``, assigning a random one when it has none or an empty one
543
+ (sdvplotR's ``.table_id``; ``"#"`` alone would scope nothing)."""
544
+ table_id = gt._options.table_id.value
545
+ if table_id:
546
+ return gt, str(table_id)
547
+ table_id = random_id()
548
+ return gt.with_id(table_id), table_id
549
+
550
+
551
+ def _light_palette(horizon: str) -> dict[str, str]:
552
+ return {
553
+ "bg": "#FFFFFF",
554
+ "heading_bg": "#FFFFFF",
555
+ "title": SDV_NAVY,
556
+ "muted": "#4A5A75",
557
+ "label": "#16305C",
558
+ "text": SDV_NAVY,
559
+ "rule": "#E3E8F1",
560
+ "group_bg": "#EEF3FA",
561
+ "horizon": horizon,
562
+ }
563
+
564
+
565
+ def _dark_palette() -> dict[str, str]:
566
+ return {
567
+ "bg": SDV_NAVY,
568
+ "heading_bg": SDV_NAVY,
569
+ "title": "#FFFFFF",
570
+ "muted": "#A9B8D0",
571
+ "label": "#9CCBFF",
572
+ "text": "#EAEBEC",
573
+ "rule": "#1D3A66",
574
+ "group_bg": "#16305C",
575
+ "horizon": SDV_HORIZON,
576
+ }
577
+
578
+
579
+ def _background(gt: GT) -> str:
580
+ """The table background as ``#rrggbb``, a best-effort read for picking ink: a translucent hex shows over the
581
+ (assumed white) page, so it is blended onto white; white when it is unset or not a hex color (a CSS name)."""
582
+ try:
583
+ return solid(str(gt._options.table_background_color.value))
584
+ except ValueError:
585
+ return "#ffffff"
586
+
587
+
588
+ def _secondary_on(bg: str, fg: str, target: float = 4.5) -> str:
589
+ """sdvplotR's ``.theme_secondary_on()``: muted but legible, ``fg`` blended toward ``bg`` as far as still clears
590
+ ``target`` contrast."""
591
+ for i in range(12):
592
+ # R's seq(0.45, 1, by = 0.05) is 0.45 + i * 0.05 unrounded (0.6000000000000001, ...): rounding the weight
593
+ # flips a channel's rounding for some colors
594
+ cand = mix(bg, fg, 0.45 + i * 0.05)
595
+ if contrast(cand, bg) >= target:
596
+ return cand
597
+ return fg
598
+
599
+
600
+ def _build_theme(gt: GT, pal: dict[str, str], density: str, tab_options: dict[str, Any]) -> GT:
601
+ """sdvplotR's .sdv_theme_build(): fonts, styles, options and the scoped CSS, at ``density``."""
602
+ d = _density(density)
603
+ k = {role: d[role] / DENSITY["comfortable"][role] for role in DENSITY["comfortable"]}
604
+
605
+ def size(n: float, role: str) -> str:
606
+ return f"{round(n * k[role], 1):g}px"
607
+
608
+ gt, tid = _table_id(gt)
609
+ chivo, lato = google_font("Chivo"), google_font("Lato")
610
+ weight: Any = "800" # CSS numeric weights; great_tables types only the keywords
611
+ medium: Any = "500"
612
+ gt = (
613
+ _table_font(gt, lato)
614
+ .tab_style(style.text(font=chivo, weight=weight, size=size(22, "title"), color=pal["title"]), loc.title())
615
+ .tab_style(
616
+ style.text(font=lato, size=size(14, "subtitle"), color=pal.get("subtitle", pal["muted"])), loc.subtitle()
617
+ )
618
+ .tab_style(
619
+ style.text(font=chivo, weight=medium, size=size(13, "label"), color=pal["label"]), loc.column_labels()
620
+ )
621
+ .tab_style(
622
+ [
623
+ style.text(font=chivo, weight=medium, size=size(13, "group"), color=pal["label"]),
624
+ important(style.fill(color=pal["group_bg"])),
625
+ ],
626
+ loc.row_groups(),
627
+ )
628
+ .tab_style(style.text(size=size(12, "source"), color=pal["muted"]), [loc.source_notes(), loc.footnotes()])
629
+ )
630
+ padding: dict[str, Any] = {}
631
+ if density != "comfortable": # sdvplotR scales great_tables' default paddings too
632
+ padding = {option: size(v, "pad") for option, v in _DEFAULT_PADDING.items()}
633
+ gt = gt.tab_options(
634
+ table_background_color=pal["bg"],
635
+ table_font_color=pal["text"],
636
+ table_font_size=size(15, "body"),
637
+ heading_align="left",
638
+ heading_background_color=pal["heading_bg"],
639
+ heading_padding=size(4, "pad"),
640
+ heading_border_bottom_style="none",
641
+ column_labels_background_color=pal["bg"],
642
+ column_labels_border_top_style="none",
643
+ column_labels_border_bottom_style="none",
644
+ column_labels_padding=size(6, "pad"),
645
+ table_body_hlines_color=pal["rule"],
646
+ table_body_hlines_width=px(1),
647
+ table_body_border_top_style="none",
648
+ table_body_border_bottom_style="none",
649
+ row_group_border_top_style="none",
650
+ row_group_border_bottom_style="none",
651
+ data_row_padding=size(7, "pad"),
652
+ table_border_top_style="none",
653
+ table_border_bottom_style="none",
654
+ source_notes_border_bottom_style="none", # great_tables 1.0 has no footnotes border options
655
+ **padding,
656
+ )
657
+ s = f"#{tid}"
658
+ css = [
659
+ # the horizon: one line under the column labels, drawn over the thead so a gradient spans the whole table
660
+ f"{s} thead {{position: relative;}}",
661
+ f'{s} thead::after {{content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 4px; '
662
+ f"background: {pal['horizon']};}}",
663
+ f"{s} .gt_col_headings th {{padding-bottom: 10px;}}",
664
+ # spanners as column labels (great_tables 1.0's loc.spanner_labels() needs explicit ids)
665
+ f"{s} .gt_column_spanner {{font-family: Chivo, sans-serif; font-weight: 500; font-size: {size(13, 'label')}; "
666
+ f"color: {pal['label']};}}",
667
+ # one 14px inset shared by the heading, the outer columns and the notes
668
+ f"{s} .gt_heading, {s} .gt_sourcenote, {s} .gt_footnote, {s} .gt_col_headings th:first-child, "
669
+ f"{s} tbody td:first-child {{padding-left: 14px !important;}}",
670
+ f"{s} .gt_heading, {s} .gt_sourcenote, {s} .gt_footnote, {s} .gt_col_headings th:last-child, "
671
+ f"{s} tbody td:last-child {{padding-right: 14px !important;}}",
672
+ f"{s} .gt_title {{padding-top: 12px !important;}}",
673
+ f"{s} .gt_subtitle {{padding-bottom: 12px !important;}}",
674
+ f"{s} tbody tr:last-child {{border-bottom: 2px solid {pal['bg']};}}", # no double rule under the last row
675
+ f"{s} td {{ font-variant-numeric: tabular-nums; }}", # digits line up between rows
676
+ ]
677
+ gt = gt.opt_css("\n".join(css))
678
+ return gt.tab_options(**tab_options) if tab_options else gt # the caller's options last, so they win
679
+
680
+
681
+ def gt_theme_sdv(gt: GT, style: str = "light", density: str = "comfortable", **tab_options: Any) -> GT:
682
+ """The SportsDataverse house table: Chivo labels, a Lato body, and the SDV gradient under the column labels.
683
+
684
+ Args:
685
+ gt: A great_tables ``GT``.
686
+ style: "light" (a white table) or "dark" (the SportsDataverse navy).
687
+ density: "comfortable" (as set), "compact" (smaller type and padding) or "social" (larger, for saved images).
688
+ **tab_options: Passed to ``GT.tab_options`` last, so they override the theme.
689
+
690
+ Returns:
691
+ GT: A new, themed table. The gradient line is CSS (``thead::after``) scoped to the table's id; an id is
692
+ assigned when the table has none.
693
+
694
+ Raises:
695
+ TypeError: If ``gt`` is not a great_tables GT.
696
+ ValueError: If ``style`` or ``density`` is unknown.
697
+
698
+ Example:
699
+ ::
700
+
701
+ from great_tables import GT
702
+ from sdvplot.great_tables import gt_sdv_logos, gt_theme_sdv
703
+ import polars as pl
704
+
705
+ df = pl.DataFrame(
706
+ {
707
+ "team": ["KC", "BUF", "BAL"],
708
+ "espn_id": ["3139477", "3918298", "3916387"],
709
+ "wins": [12, 10, 9],
710
+ }
711
+ )
712
+
713
+ gt_theme_sdv(gt_sdv_logos(GT(df), "team", league="nfl").tab_header("AFC West"))
714
+ gt_theme_sdv(GT(df), style="dark", density="social")
715
+
716
+ See Also:
717
+ Ported from sdvplotR ``gt_theme_sdv()``: https://sdvplotR.sportsdataverse.org/reference/gt_theme_sdv.html
718
+ """
719
+ _check_gt(gt)
720
+ if style not in ("light", "dark"):
721
+ raise ValueError(f"style must be 'light' or 'dark', got {style!r}")
722
+ pal = _dark_palette() if style == "dark" else _light_palette(SDV_HORIZON)
723
+ return _build_theme(gt, pal, density, tab_options)
724
+
725
+
726
+ def gt_theme_sdv_team(
727
+ gt: GT,
728
+ team: Any = None,
729
+ *,
730
+ league: str,
731
+ density: str = "comfortable",
732
+ id_system: IdSystem = "auto",
733
+ **tab_options: Any,
734
+ ) -> GT:
735
+ """``gt_theme_sdv`` in one team's colors: a title block in the primary color, the line in the secondary.
736
+
737
+ Ink on the title block is black or white, whichever reads better, and the subtitle is blended toward it while it
738
+ keeps 4.5:1 contrast. A secondary color too pale for a white table gives way to the primary for the line, and a
739
+ primary too light to read on white gives way to the SportsDataverse navy for the column labels.
740
+
741
+ Args:
742
+ gt: A great_tables ``GT``.
743
+ team: One team (an abbreviation, name or provider id); None for the SportsDataverse navy and cyan.
744
+ league: The SDV league key, e.g. "nfl".
745
+ density: "comfortable", "compact" or "social", as in ``gt_theme_sdv``.
746
+ id_system: The id system of ``team``, as in ``resolve``: "auto" tries each in order; NHL stats ids need
747
+ "nhl_id".
748
+ **tab_options: Passed to ``GT.tab_options`` last, so they override the theme.
749
+
750
+ Returns:
751
+ GT: A new, themed table. A team with no colors on file gets the SportsDataverse colors, with an
752
+ SdvplotWarning.
753
+
754
+ Raises:
755
+ TypeError: If ``gt`` is not a great_tables GT, or ``team`` is not one value.
756
+ InputError: (a ValueError) If ``team`` is given and ``league`` or ``id_system`` is unknown.
757
+ UnresolvedTeamError: (a ValueError) If ``team`` does not resolve to one team of ``league``.
758
+ ValueError: If ``density`` is unknown.
759
+
760
+ Example:
761
+ ::
762
+
763
+ from great_tables import GT
764
+ from sdvplot.great_tables import gt_theme_sdv_team
765
+ import polars as pl
766
+
767
+ df = pl.DataFrame(
768
+ {
769
+ "team": ["KC", "BUF", "BAL"],
770
+ "espn_id": ["3139477", "3918298", "3916387"],
771
+ "wins": [12, 10, 9],
772
+ }
773
+ )
774
+
775
+ gt_theme_sdv_team(GT(df).tab_header("Chiefs leaders"), "KC", league="nfl")
776
+
777
+ See Also:
778
+ Ported from sdvplotR ``gt_theme_sdv_team()``:
779
+ https://sdvplotR.sportsdataverse.org/reference/gt_theme_sdv_team.html
780
+ """
781
+ _check_gt(gt)
782
+ primary, secondary = SDV_NAVY, SDV_CYAN
783
+ if team is not None:
784
+ team_id = resolve(one_team(team, "gt_theme_sdv_team"), league, id_system=id_system, strict=True)
785
+ p, s = team_colors(league, team_id, which="primary"), team_colors(league, team_id, which="secondary")
786
+ if p:
787
+ primary, secondary = p, s or p
788
+ else:
789
+ warn(f"no colors on file for {league} team {team!r}; using the SportsDataverse colors")
790
+ title = on_color(primary)
791
+ pal = _light_palette(secondary if contrast(secondary, "#ffffff") >= 1.5 else primary)
792
+ pal.update(
793
+ heading_bg=primary,
794
+ title=title,
795
+ subtitle=_secondary_on(primary, title),
796
+ label=primary if contrast(primary, "#ffffff") >= 3 else SDV_NAVY,
797
+ )
798
+ return _build_theme(gt, pal, density, tab_options)