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,2134 @@
1
+ """Table themes for great_tables: the port of sdvplotR's ``gt_theme_*`` functions.
2
+
3
+ Each theme ports its R file's ``opt_table_font``, ``tab_style``, ``tab_options`` and ``opt_css`` calls one to one, with
4
+ the same arguments and defaults (``docs/PARITY_TABLES.md`` lists where great_tables differs).
5
+
6
+ great_tables has no public way to read a table back, so this module reads four private attributes, each pinned by a
7
+ test: ``GT._options`` (the table id and option values), ``GT._tbl_data`` (columns and row count), ``GT._spanners``
8
+ (spanner ids) and ``GT._styles`` (rescaled by ``density``). It also subclasses the private ``GoogleFont`` (``_Font``).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import dataclasses
14
+ import inspect
15
+ import math
16
+ import numbers
17
+ import re
18
+ from typing import Any
19
+
20
+ import narwhals as nw
21
+ from great_tables import GT, html, loc, px, style
22
+ from great_tables._helpers import GoogleFont
23
+
24
+ from sdvplot._contrast import hex6, on_color
25
+ from sdvplot.great_tables._marks import (
26
+ DENSITY,
27
+ _check_gt,
28
+ _density,
29
+ _secondary_on,
30
+ _table_font,
31
+ _table_id,
32
+ important,
33
+ )
34
+
35
+ # sdvplotR's .theme_scale_output(): which density role each styled location and size option scales with
36
+ _STYLE_ROLE: dict[type, str] = {
37
+ loc.title: "title",
38
+ loc.subtitle: "subtitle",
39
+ loc.column_labels: "label",
40
+ loc.spanner_labels: "label",
41
+ loc.row_groups: "group",
42
+ loc.stubhead: "label",
43
+ loc.body: "body",
44
+ loc.stub: "body",
45
+ loc.source_notes: "source",
46
+ loc.footnotes: "source",
47
+ }
48
+ _OPTION_ROLE = {
49
+ "table_font_size": "body",
50
+ "stub_font_size": "body",
51
+ "heading_title_font_size": "title",
52
+ "heading_subtitle_font_size": "subtitle",
53
+ "column_labels_font_size": "label",
54
+ "row_group_font_size": "group",
55
+ "source_notes_font_size": "source",
56
+ "data_row_padding": "pad",
57
+ "row_group_padding": "pad",
58
+ "column_labels_padding": "pad",
59
+ "heading_padding": "pad",
60
+ "source_notes_padding": "pad",
61
+ "summary_row_padding": "pad",
62
+ "grand_summary_row_padding": "pad",
63
+ }
64
+ _PX = re.compile(r"-?[0-9.]+px")
65
+
66
+
67
+ class _Font(GoogleFont):
68
+ """A Google font that loads every weight. great_tables' import asks for the regular face only, so a theme's 600 or
69
+ 700 would be synthesized; a discrete weight list (not a ``100..900`` range) returns the faces a family has."""
70
+
71
+ def make_import_stmt(self) -> str:
72
+ axes = ";".join(f"{i},{w}" for i in (0, 1) for w in range(100, 1000, 100))
73
+ family = self.font.replace(" ", "+")
74
+ return f"@import url('https://fonts.googleapis.com/css2?family={family}:ital,wght@{axes}&display=swap');"
75
+
76
+
77
+ def _font(name: str) -> GoogleFont:
78
+ return _Font(name)
79
+
80
+
81
+ def _text(**kwargs: Any) -> Any:
82
+ """``style.text`` taking sdvplotR's numeric weights (typed as keywords in great_tables; any weight renders)."""
83
+ return style.text(**kwargs)
84
+
85
+
86
+ def _color(value: str, arg: str) -> str:
87
+ try:
88
+ return hex6(value)
89
+ except ValueError:
90
+ raise ValueError(f"{arg} must be a hex color with no transparency, such as '#8C2F1E', not {value!r}") from None
91
+
92
+
93
+ def _shape(gt: GT) -> tuple[list[str], int]:
94
+ """The data's column names and row count."""
95
+ data = nw.from_native(gt._tbl_data, eager_only=True)
96
+ return list(data.columns), len(data)
97
+
98
+
99
+ def _on_spanners(gt: GT, *styles: Any) -> GT:
100
+ """R's ``cells_column_spanners()``: every spanner the table has now (great_tables needs their ids)."""
101
+ ids = [s.spanner_id for s in gt._spanners]
102
+ return gt.tab_style(list(styles), loc.spanner_labels(ids=ids)) if ids else gt
103
+
104
+
105
+ def _row_rules(gt: GT, color: str) -> GT:
106
+ """A rule under every body row but the last: R's ``cells_body(rows = 1:(nrow(data) - 1))`` bottom border.
107
+
108
+ A one-row table gets none; R's ``1:0`` drew one under its only row.
109
+ """
110
+ n = _shape(gt)[1]
111
+ return (
112
+ gt.tab_style(important(style.borders(sides="bottom", color=color)), loc.body(rows=list(range(n - 1))))
113
+ if n > 1
114
+ else gt
115
+ )
116
+
117
+
118
+ def _hide_spanner_row(gt: GT, table_id: str) -> GT:
119
+ """sdvplotR's spanner over every column, hidden by CSS.
120
+
121
+ R hides it by element id; great_tables gives a spanner stacked over another no id, so the label carries a class
122
+ and the CSS hides the cell holding it. A table themed twice already has the spanner (great_tables refuses a second
123
+ one with the same id).
124
+ """
125
+ if "toss_out_spanner_dev" not in [s.spanner_id for s in gt._spanners]:
126
+ label = html('<span class="sdvplot-hidden-spanner"></span>')
127
+ gt = gt.tab_spanner(label, columns=_shape(gt)[0], id="toss_out_spanner_dev")
128
+ return gt.opt_css(_css(table_id, "th:has(.sdvplot-hidden-spanner)", "display: none;"))
129
+
130
+
131
+ def _css(table_id: str, selectors: str | list[str], body: str) -> str:
132
+ """One CSS rule scoped to the table: ``#id sel1, #id sel2 { body }``."""
133
+ sel = [selectors] if isinstance(selectors, str) else selectors
134
+ return ", ".join(f"#{table_id} {s}" for s in sel) + " { " + body + " }"
135
+
136
+
137
+ def _tabular_nums(table_id: str) -> str:
138
+ # not font-feature-settings: 'tnum', which also spaces out commas in some faces
139
+ return _css(table_id, "td", "font-variant-numeric: tabular-nums;")
140
+
141
+
142
+ def _last_row_border(table_id: str, background: str) -> str:
143
+ # gt's hline under the last row doubles up with the table's closing rule
144
+ return _css(table_id, "tbody tr:last-child", f"border-bottom: 2px solid {background};")
145
+
146
+
147
+ def _scale_len(value: Any, k: float) -> Any:
148
+ """A ``"12px"`` length times ``k``, rounded to 0.1 px as R does; any other value (``"90%"``, None) unchanged."""
149
+ if isinstance(value, str) and _PX.fullmatch(value):
150
+ return f"{round(float(value[:-2]) * k, 1):g}px"
151
+ return value
152
+
153
+
154
+ def _scale_output(gt: GT, density: str) -> GT:
155
+ """sdvplotR's ``.theme_scale_output()``: rescale the px sizes a finished table holds by ``density``.
156
+
157
+ For the themes that hard-code their sizes. Every text size set through ``tab_style`` and every size and padding
158
+ option (great_tables' defaults included) scales by its role's ratio to ``"comfortable"``, as in R.
159
+ """
160
+ d, base = _density(density), DENSITY["comfortable"]
161
+ k = {role: d[role] / base[role] for role in d}
162
+ if all(v == 1 for v in k.values()):
163
+ return gt
164
+ styles = []
165
+ for info in gt._styles:
166
+ role = _STYLE_ROLE.get(type(info.locname))
167
+ cells = [
168
+ dataclasses.replace(s, size=_scale_len(s.size, k[role]))
169
+ if role is not None and isinstance(s, style.text) and s.size is not None
170
+ else s
171
+ for s in info.styles
172
+ ]
173
+ styles.append(dataclasses.replace(info, styles=cells))
174
+ gt = gt._replace(_styles=styles)
175
+ options = {name: _scale_len(getattr(gt._options, name).value, k[role]) for name, role in _OPTION_ROLE.items()}
176
+ return gt.tab_options(**options)
177
+
178
+
179
+ def _adjust_luminance(color: str, steps: float) -> str:
180
+ """``gt::adjust_luminance()``: shift a color's HCL luminance ``steps`` along a logistic curve (R's grDevices math).
181
+
182
+ Matches R on 426 of 432 sampled colors and steps (2026-10-04); the other six are pure white, where R returns NA
183
+ and this returns white.
184
+ """
185
+
186
+ def c2to3(x: float, y: float) -> list[float]:
187
+ return [x / y, 1.0, (1 - x - y) / y]
188
+
189
+ # grDevices make.rgb(): sRGB primaries and R's own D65 white point (0.3137, 0.3291)
190
+ p = [c2to3(0.64, 0.33), c2to3(0.30, 0.60), c2to3(0.15, 0.06)]
191
+ white = c2to3(0.3137, 0.3291)
192
+ det = (
193
+ p[0][0] * (p[1][1] * p[2][2] - p[1][2] * p[2][1])
194
+ - p[0][1] * (p[1][0] * p[2][2] - p[1][2] * p[2][0])
195
+ + p[0][2] * (p[1][0] * p[2][1] - p[1][1] * p[2][0])
196
+ )
197
+ inv = [
198
+ [
199
+ (p[(j + 1) % 3][(i + 1) % 3] * p[(j + 2) % 3][(i + 2) % 3]
200
+ - p[(j + 1) % 3][(i + 2) % 3] * p[(j + 2) % 3][(i + 1) % 3]) / det
201
+ for j in range(3)
202
+ ]
203
+ for i in range(3)
204
+ ] # fmt: skip
205
+ s = [sum(white[k] * inv[k][j] for k in range(3)) for j in range(3)]
206
+ m = [[s[r] * p[r][j] for j in range(3)] for r in range(3)]
207
+
208
+ c = hex6(color)
209
+ rgb = [int(c[i : i + 2], 16) / 255 for i in (1, 3, 5)]
210
+ lin = [v / 12.92 if v < 0.04045 else ((v + 0.055) / 1.055) ** 2.4 for v in rgb]
211
+ x, y, z = (sum(lin[k] * m[k][j] for k in range(3)) for j in range(3))
212
+ # convertColor(, "sRGB", "Luv")
213
+ denom, wdenom = x + 15 * y + 3 * z, white[0] + 15 * white[1] + 3 * white[2]
214
+ u1, v1 = (4 * x / denom, 9 * y / denom) if denom else (1.0, 1.0)
215
+ yr = y / white[1]
216
+ lum = 24389 / 27 * yr if yr <= 216 / 24389 else 116 * yr ** (1 / 3) - 16
217
+ u, v = 13 * lum * (u1 - 4 * white[0] / wdenom), 13 * lum * (v1 - 9 * white[1] / wdenom)
218
+ hue, chroma = math.atan2(v, u), math.hypot(u, v)
219
+ frac = lum / 100
220
+ if frac <= 0:
221
+ return "#000000"
222
+ lum = 100.0 if frac >= 1 else 100 / (1 + math.exp(-(math.log(frac / (1 - frac)) + steps)))
223
+ # grDevices hcl(h, c, l): polar Luv back to sRGB, clamped into gamut
224
+ yy = 100 * (((lum + 16) / 116) ** 3 if lum > 7.999592 else lum / 903.3)
225
+ uu = chroma * math.cos(hue) / (13 * lum) + 0.1978398
226
+ vv = chroma * math.sin(hue) / (13 * lum) + 0.4683363
227
+ xx = 9.0 * yy * uu / (4 * vv)
228
+ zz = -xx / 3 - 5 * yy + 3 * yy / vv
229
+
230
+ def gamma(t: float) -> float:
231
+ return 1.055 * t ** (1 / 2.4) - 0.055 if t > 0.00304 else 12.92 * t
232
+
233
+ out = [
234
+ gamma((3.240479 * xx - 1.537150 * yy - 0.498535 * zz) / 100),
235
+ gamma((-0.969256 * xx + 1.875992 * yy + 0.041556 * zz) / 100),
236
+ gamma((0.055648 * xx - 0.204043 * yy + 1.057311 * zz) / 100),
237
+ ]
238
+ return "#" + "".join(f"{min(255, max(0, int(255 * t + 0.5))):02x}" for t in out)
239
+
240
+
241
+ def gt_theme_almanac(
242
+ gt: GT, accent: str = "#8C2F1E", density: str = "compact", stripe: str | None = "#F1F1EF", **options: Any
243
+ ) -> GT:
244
+ """Record-book theme: a slab body, narrow condensed labels, tight rows and banded rows, like a statistical abstract.
245
+
246
+ Args:
247
+ gt: The great_tables ``GT`` to theme.
248
+ accent: Hex color of the row-group labels and the rule above the table.
249
+ density: The type and padding scale: "comfortable" (14px body), "compact" (12px) or "social" (17px, the
250
+ scale saved images use).
251
+ stripe: Hex color of the banded rows; None switches banding off and keeps the rest of the theme.
252
+ **options: Passed to ``GT.tab_options`` last, so they override the theme (great_tables names, e.g.
253
+ ``table_font_size``).
254
+
255
+ Returns:
256
+ GT: A new ``GT`` with the theme applied.
257
+
258
+ Raises:
259
+ TypeError: ``gt`` is not a great_tables ``GT``.
260
+ ValueError: A color is not hex, or ``density`` is not one of the three scales.
261
+
262
+ Example:
263
+ ::
264
+
265
+ from great_tables import GT
266
+ from sdvplot.great_tables import gt_theme_almanac
267
+ import polars as pl
268
+
269
+ df = pl.DataFrame(
270
+ {
271
+ "team": ["KC", "BUF", "BAL"],
272
+ "espn_id": ["3139477", "3918298", "3916387"],
273
+ "wins": [12, 10, 9],
274
+ }
275
+ )
276
+
277
+ gt_theme_almanac(GT(df), stripe=None, accent="#1F3A5F")
278
+
279
+ See Also:
280
+ Ported from sdvplotR ``gt_theme_almanac()``: https://sdvplotR.sportsdataverse.org/reference/gt_theme_almanac.html
281
+ """
282
+ d = _density(density)
283
+ accent = _color(accent, "accent")
284
+ stripe = None if stripe is None else _color(stripe, "stripe")
285
+ ink, secondary, rule = "#1A1A1A", "#6B6B68", "#D8D8D4"
286
+ gt, tid = _table_id(_check_gt(gt))
287
+ narrow = _font("Archivo Narrow")
288
+ label = _text(font=narrow, weight=700, size=px(d["label"] + 1), color=ink, transform="uppercase")
289
+ gt = (
290
+ _table_font(gt, _font("Zilla Slab"))
291
+ .opt_row_striping(row_striping=stripe is not None)
292
+ .tab_style(_text(color=ink, size=px(d["body"])), loc.body())
293
+ .tab_style(_text(weight=700, size=px(d["title"]), color=ink), loc.title())
294
+ .tab_style(_text(weight=400, size=px(d["subtitle"]), color=secondary), loc.subtitle())
295
+ .tab_style(label, loc.column_labels())
296
+ .pipe(_on_spanners, label)
297
+ .tab_style(
298
+ _text(font=narrow, weight=700, size=px(d["group"] + 1), color=accent, transform="uppercase"),
299
+ loc.row_groups(),
300
+ )
301
+ .tab_style(_text(font=narrow, size=px(d["source"] + 1), color=secondary), [loc.source_notes(), loc.footnotes()])
302
+ .tab_options(
303
+ table_background_color="#FFFFFF",
304
+ row_striping_background_color=stripe or "#FFFFFF",
305
+ table_font_size=px(d["body"]),
306
+ data_row_padding=px(d["pad"]),
307
+ table_border_top_style="solid",
308
+ table_border_top_width=px(2),
309
+ table_border_top_color=accent,
310
+ table_border_bottom_style="none",
311
+ heading_align="left",
312
+ heading_border_bottom_style="none",
313
+ heading_padding=px(d["pad"] + 2),
314
+ column_labels_border_top_style="none",
315
+ column_labels_border_bottom_style="solid",
316
+ column_labels_border_bottom_width=px(1.5),
317
+ column_labels_border_bottom_color=ink,
318
+ column_labels_padding=px(d["pad"] + 1),
319
+ # banding does the work, so no row rules
320
+ table_body_border_top_style="none",
321
+ table_body_hlines_style="none",
322
+ table_body_border_bottom_style="solid",
323
+ table_body_border_bottom_width=px(1),
324
+ table_body_border_bottom_color=ink,
325
+ row_group_border_top_style="solid",
326
+ row_group_border_top_width=px(1),
327
+ row_group_border_top_color=rule,
328
+ row_group_border_bottom_style="none",
329
+ row_group_padding=px(max(d["pad"], 3)),
330
+ source_notes_border_lr_style="none",
331
+ source_notes_border_bottom_style="none",
332
+ source_notes_padding=px(d["pad"] + 2),
333
+ )
334
+ .opt_css(
335
+ "\n".join(
336
+ [
337
+ _tabular_nums(tid),
338
+ _last_row_border(tid, "#FFFFFF"),
339
+ _css(tid, [".gt_col_heading", ".gt_column_spanner"], "letter-spacing: 0.05em;"),
340
+ _css(tid, ".gt_group_heading", "letter-spacing: 0.05em;"),
341
+ _css(tid, ".gt_subtitle", f"padding-bottom: {d['pad'] + 6}px !important;"),
342
+ _css(tid, ".gt_title", f"padding-bottom: {math.ceil(d['pad'] / 2)}px !important;"),
343
+ ]
344
+ )
345
+ )
346
+ )
347
+ # the caller's options last, so they win
348
+ return gt.tab_options(**options)
349
+
350
+
351
+ def gt_theme_booktabs(gt: GT, accent: str = "#111111", density: str = "comfortable", **options: Any) -> GT:
352
+ """Academic booktabs theme: three horizontal rules and nothing else, the way LaTeX booktabs draws them.
353
+
354
+ Args:
355
+ gt: The great_tables ``GT`` to theme.
356
+ accent: Hex color of the three rules and the row-group labels.
357
+ density: The type and padding scale: "comfortable", "compact" or "social".
358
+ **options: Passed to ``GT.tab_options`` last, so they override the theme.
359
+
360
+ Returns:
361
+ GT: A new ``GT`` with the theme applied.
362
+
363
+ Raises:
364
+ TypeError: ``gt`` is not a great_tables ``GT``.
365
+ ValueError: ``accent`` is not hex, or ``density`` is not one of the three scales.
366
+
367
+ Example:
368
+ ::
369
+
370
+ from great_tables import GT
371
+ import polars as pl
372
+ from sdvplot.great_tables import gt_theme_booktabs
373
+
374
+ df = pl.DataFrame(
375
+ {
376
+ "team": ["KC", "BUF", "BAL"],
377
+ "espn_id": ["3139477", "3918298", "3916387"],
378
+ "wins": [12, 10, 9],
379
+ }
380
+ )
381
+
382
+ gt_theme_booktabs(GT(df), density="compact")
383
+
384
+ See Also:
385
+ Ported from sdvplotR ``gt_theme_booktabs()``: https://sdvplotR.sportsdataverse.org/reference/gt_theme_booktabs.html
386
+ """
387
+ d = _density(density)
388
+ accent = _color(accent, "accent")
389
+ ink, secondary, bg = "#111111", "#5A5A5A", "#FFFFFF"
390
+ gt, tid = _table_id(_check_gt(gt))
391
+ serif = _font("Tinos")
392
+ label = _text(font=serif, weight=700, size=px(d["label"] + 1), color=ink)
393
+ gt = (
394
+ _table_font(gt, _font("Tinos"))
395
+ .tab_style(_text(color=ink, size=px(d["body"])), loc.body())
396
+ .tab_style(_text(font=serif, weight=700, size=px(d["title"]), color=ink), loc.title())
397
+ .tab_style(
398
+ _text(font=serif, weight=400, style="italic", size=px(d["subtitle"]), color=secondary), loc.subtitle()
399
+ )
400
+ # column labels, same serif, no caps
401
+ .tab_style(label, loc.column_labels())
402
+ .pipe(_on_spanners, label)
403
+ # row groups as a bold italic subheading
404
+ .tab_style(
405
+ _text(font=serif, weight=700, style="italic", size=px(d["group"] + 1), color=accent), loc.row_groups()
406
+ )
407
+ .tab_style(_text(font=serif, size=px(d["source"]), color=secondary), [loc.source_notes(), loc.footnotes()])
408
+ .tab_options(
409
+ table_background_color=bg,
410
+ table_font_size=px(d["body"]),
411
+ data_row_padding=px(d["pad"]),
412
+ heading_align="left",
413
+ heading_border_bottom_style="none",
414
+ heading_padding=px(d["pad"]),
415
+ # top rule, above the column labels
416
+ table_border_top_style="none",
417
+ column_labels_border_top_style="solid",
418
+ column_labels_border_top_width=px(2),
419
+ column_labels_border_top_color=accent,
420
+ # mid rule, under the column labels
421
+ column_labels_border_bottom_style="solid",
422
+ column_labels_border_bottom_width=px(1),
423
+ column_labels_border_bottom_color=accent,
424
+ column_labels_padding=px(max(d["pad"] - 1, 2)),
425
+ # nothing between the data rows
426
+ table_body_border_top_style="none",
427
+ table_body_hlines_style="none",
428
+ # bottom rule, closing the body
429
+ table_body_border_bottom_style="solid",
430
+ table_body_border_bottom_width=px(2),
431
+ table_body_border_bottom_color=accent,
432
+ table_border_bottom_style="none",
433
+ column_labels_vlines_style="none",
434
+ table_body_vlines_style="none",
435
+ stub_border_style="none",
436
+ # a midrule above each row group
437
+ row_group_border_top_style="solid",
438
+ row_group_border_top_width=px(1),
439
+ row_group_border_top_color=accent,
440
+ row_group_border_bottom_style="none",
441
+ row_group_padding=px(max(d["pad"] - 2, 2)),
442
+ source_notes_border_lr_style="none",
443
+ source_notes_border_bottom_style="none",
444
+ source_notes_padding=px(d["pad"]),
445
+ )
446
+ .opt_css("\n".join([_tabular_nums(tid), _css(tid, ".gt_sourcenote", f"padding-top: {d['pad'] + 4}px;")]))
447
+ )
448
+ return gt.tab_options(**options)
449
+
450
+
451
+ def gt_theme_broadsheet(
452
+ gt: GT, accent: str = "#A6081A", density: str = "comfortable", paper: str = "white", **options: Any
453
+ ) -> GT:
454
+ """Newspaper theme: a serif body on warm paper, small letterspaced sans labels, hairlines between rows.
455
+
456
+ Args:
457
+ gt: The great_tables ``GT`` to theme.
458
+ accent: Hex color of the rule above the table and the row-group labels.
459
+ density: The type and padding scale: "comfortable", "compact" or "social".
460
+ paper: The table background: "white" (a warm off-white), "salmon" (the financial-press pink) or any hex
461
+ color, which keeps the neutral hairline.
462
+ **options: Passed to ``GT.tab_options`` last, so they override the theme.
463
+
464
+ Returns:
465
+ GT: A new ``GT`` with the theme applied.
466
+
467
+ Raises:
468
+ TypeError: ``gt`` is not a great_tables ``GT``.
469
+ ValueError: ``accent`` or ``paper`` is not a hex color (or preset), or ``density`` is not one of the three
470
+ scales.
471
+
472
+ Example:
473
+ ::
474
+
475
+ from great_tables import GT
476
+ import polars as pl
477
+ from sdvplot.great_tables import gt_theme_broadsheet
478
+
479
+ df = pl.DataFrame(
480
+ {
481
+ "team": ["KC", "BUF", "BAL"],
482
+ "espn_id": ["3139477", "3918298", "3916387"],
483
+ "wins": [12, 10, 9],
484
+ }
485
+ )
486
+
487
+ gt_theme_broadsheet(GT(df), paper="salmon")
488
+
489
+ See Also:
490
+ Ported from sdvplotR ``gt_theme_broadsheet()``:
491
+ https://sdvplotR.sportsdataverse.org/reference/gt_theme_broadsheet.html
492
+ """
493
+ d = _density(density)
494
+ accent = _color(accent, "accent")
495
+ # each paper preset carries a rule color matched to its ground
496
+ presets = {"white": ("#FBFAF7", "#DEDAD2"), "salmon": ("#FFF1E5", "#EAD9C7")}
497
+ bg, rule = presets.get(paper) or (_color(paper, "paper"), "#DEDAD2")
498
+ ink, secondary = "#16130F", "#5C574F"
499
+ gt, tid = _table_id(_check_gt(gt))
500
+ headline, sans = _font("Newsreader"), _font("Public Sans")
501
+ label = _text(font=sans, weight=600, size=px(d["label"]), color=secondary, transform="uppercase")
502
+ gt = (
503
+ _table_font(gt, _font("Source Serif 4"))
504
+ .tab_style(_text(color=ink, size=px(d["body"])), loc.body())
505
+ .tab_style(_text(font=headline, weight=600, size=px(d["title"]), color=ink), loc.title())
506
+ .tab_style(
507
+ _text(font=headline, weight=400, style="italic", size=px(d["subtitle"]), color=secondary),
508
+ loc.subtitle(),
509
+ )
510
+ # small letterspaced sans labels
511
+ .tab_style(label, loc.column_labels())
512
+ .pipe(_on_spanners, label)
513
+ # row groups get a label over a rule, not a filled band
514
+ .tab_style(
515
+ _text(font=sans, weight=700, size=px(d["group"]), color=accent, transform="uppercase"), loc.row_groups()
516
+ )
517
+ .tab_style(_text(font=sans, size=px(d["source"]), color=secondary), [loc.source_notes(), loc.footnotes()])
518
+ .tab_options(
519
+ table_background_color=bg,
520
+ table_font_size=px(d["body"]),
521
+ data_row_padding=px(d["pad"]),
522
+ # thin accent rule across the top
523
+ table_border_top_style="solid",
524
+ table_border_top_width=px(2),
525
+ table_border_top_color=accent,
526
+ table_border_bottom_style="none",
527
+ heading_align="left",
528
+ heading_border_bottom_style="none",
529
+ heading_padding=px(d["pad"]),
530
+ # the one heavy rule, under the column labels
531
+ column_labels_border_top_style="none",
532
+ column_labels_border_bottom_style="solid",
533
+ column_labels_border_bottom_width=px(1.5),
534
+ column_labels_border_bottom_color=ink,
535
+ column_labels_padding=px(max(d["pad"] - 2, 2)),
536
+ # hairlines between rows, firmer rule to close the body
537
+ table_body_border_top_style="none",
538
+ table_body_hlines_color=rule,
539
+ table_body_hlines_width=px(1),
540
+ table_body_border_bottom_style="solid",
541
+ table_body_border_bottom_width=px(1),
542
+ table_body_border_bottom_color=ink,
543
+ row_group_border_top_style="solid",
544
+ row_group_border_top_width=px(1),
545
+ row_group_border_top_color=ink,
546
+ row_group_border_bottom_style="none",
547
+ row_group_padding=px(max(d["pad"] - 3, 2)),
548
+ source_notes_border_lr_style="none",
549
+ source_notes_border_bottom_style="none",
550
+ source_notes_padding=px(d["pad"]),
551
+ )
552
+ .opt_css(
553
+ "\n".join(
554
+ [
555
+ _tabular_nums(tid),
556
+ _last_row_border(tid, bg),
557
+ # no tab_options equivalent for letter-spacing
558
+ _css(tid, [".gt_col_heading", ".gt_column_spanner"], "letter-spacing: 0.09em;"),
559
+ # gt's .gt_row_group_first: great_tables has no such class, so the row after a group heading
560
+ _css(tid, ".gt_group_heading_row + tr td", f"padding-top: {max(d['pad'] - 2, 2)}px;"),
561
+ _css(tid, ".gt_group_heading", "letter-spacing: 0.08em;"),
562
+ # the heading needs more air under it than the rows have between them
563
+ _css(tid, ".gt_subtitle", f"padding-bottom: {d['pad'] + 8}px !important;"),
564
+ _css(tid, ".gt_title", f"padding-bottom: {math.ceil(d['pad'] / 2)}px !important;"),
565
+ # keep the source note off the closing rule
566
+ _css(tid, ".gt_sourcenote", f"padding-top: {d['pad'] + 4}px;"),
567
+ ]
568
+ )
569
+ )
570
+ )
571
+ return gt.tab_options(**options)
572
+
573
+
574
+ def gt_theme_swiss(gt: GT, accent: str = "#111111", density: str = "comfortable", **options: Any) -> GT:
575
+ """International Typographic Style theme: a grotesque, generous space instead of rules, one accent rule.
576
+
577
+ Args:
578
+ gt: The great_tables ``GT`` to theme.
579
+ accent: Hex color, used once, on the rule beneath the column labels. The default reads as no color at all.
580
+ density: The type and padding scale: "comfortable", "compact" or "social".
581
+ **options: Passed to ``GT.tab_options`` last, so they override the theme.
582
+
583
+ Returns:
584
+ GT: A new ``GT`` with the theme applied.
585
+
586
+ Raises:
587
+ TypeError: ``gt`` is not a great_tables ``GT``.
588
+ ValueError: ``accent`` is not hex, or ``density`` is not one of the three scales.
589
+
590
+ Example:
591
+ ::
592
+
593
+ from great_tables import GT
594
+ import polars as pl
595
+ from sdvplot.great_tables import gt_theme_swiss
596
+
597
+ df = pl.DataFrame(
598
+ {
599
+ "team": ["KC", "BUF", "BAL"],
600
+ "espn_id": ["3139477", "3918298", "3916387"],
601
+ "wins": [12, 10, 9],
602
+ }
603
+ )
604
+
605
+ gt_theme_swiss(GT(df), accent="#E30613")
606
+
607
+ See Also:
608
+ Ported from sdvplotR ``gt_theme_swiss()``: https://sdvplotR.sportsdataverse.org/reference/gt_theme_swiss.html
609
+ """
610
+ d = _density(density)
611
+ accent = _color(accent, "accent")
612
+ ink, secondary = "#111111", "#6B6B6B" # 5.28:1 on white; #7A7A7A missed 4.5
613
+ gt, tid = _table_id(_check_gt(gt))
614
+ label = _text(weight=500, size=px(d["label"]), color=ink, transform="uppercase")
615
+ gt = (
616
+ _table_font(gt, _font("Archivo"))
617
+ .tab_style(_text(color=ink, size=px(d["body"]), weight=400), loc.body())
618
+ .tab_style(_text(weight=700, size=px(d["title"] + 4), color=ink), loc.title())
619
+ .tab_style(_text(weight=400, size=px(d["subtitle"]), color=secondary), loc.subtitle())
620
+ .tab_style(label, loc.column_labels())
621
+ .pipe(_on_spanners, label)
622
+ .tab_style(_text(weight=500, size=px(d["group"]), color=secondary, transform="uppercase"), loc.row_groups())
623
+ .tab_style(_text(size=px(d["source"]), color=secondary), [loc.source_notes(), loc.footnotes()])
624
+ .tab_options(
625
+ table_background_color="#FFFFFF",
626
+ table_font_size=px(d["body"]),
627
+ # padding is the design here
628
+ data_row_padding=px(d["pad"] + 5),
629
+ table_border_top_style="none",
630
+ table_border_bottom_style="none",
631
+ heading_align="left",
632
+ heading_border_bottom_style="none",
633
+ heading_padding=px(d["pad"] + 4),
634
+ column_labels_border_top_style="none",
635
+ column_labels_border_bottom_style="solid",
636
+ column_labels_border_bottom_width=px(1),
637
+ column_labels_border_bottom_color=accent,
638
+ column_labels_padding=px(d["pad"] + 2),
639
+ # no row rules, space does the separating
640
+ table_body_border_top_style="none",
641
+ table_body_hlines_style="none",
642
+ table_body_border_bottom_style="solid",
643
+ table_body_border_bottom_width=px(1),
644
+ table_body_border_bottom_color=ink,
645
+ row_group_border_top_style="none",
646
+ row_group_border_bottom_style="none",
647
+ row_group_padding=px(d["pad"] + 6),
648
+ source_notes_border_lr_style="none",
649
+ source_notes_border_bottom_style="none",
650
+ source_notes_padding=px(d["pad"] + 4),
651
+ )
652
+ .opt_css(
653
+ "\n".join(
654
+ [
655
+ _tabular_nums(tid),
656
+ _last_row_border(tid, "#FFFFFF"),
657
+ _css(tid, [".gt_col_heading", ".gt_column_spanner"], "letter-spacing: 0.12em;"),
658
+ _css(tid, ".gt_group_heading", "letter-spacing: 0.12em;"),
659
+ _css(
660
+ tid,
661
+ ".gt_title",
662
+ f"letter-spacing: -0.02em; padding-bottom: {math.ceil(d['pad'] / 2)}px !important;",
663
+ ),
664
+ _css(tid, ".gt_subtitle", f"padding-bottom: {d['pad'] + 14}px !important;"),
665
+ ]
666
+ )
667
+ )
668
+ )
669
+ return gt.tab_options(**options)
670
+
671
+
672
+ def gt_theme_tufte(gt: GT, accent: str = "#111111", density: str = "comfortable", **options: Any) -> GT:
673
+ """Tufte theme: an old-style serif on cream, italic labels, one hairline under the labels and almost no ink.
674
+
675
+ Args:
676
+ gt: The great_tables ``GT`` to theme.
677
+ accent: Hex color of the header hairline and the row-group labels; a muted red or rust gives Tufte's marginal
678
+ accent.
679
+ density: The type and padding scale: "comfortable", "compact" or "social".
680
+ **options: Passed to ``GT.tab_options`` last, so they override the theme.
681
+
682
+ Returns:
683
+ GT: A new ``GT`` with the theme applied.
684
+
685
+ Raises:
686
+ TypeError: ``gt`` is not a great_tables ``GT``.
687
+ ValueError: ``accent`` is not hex, or ``density`` is not one of the three scales.
688
+
689
+ Example:
690
+ ::
691
+
692
+ from great_tables import GT
693
+ import polars as pl
694
+ from sdvplot.great_tables import gt_theme_tufte
695
+
696
+ df = pl.DataFrame(
697
+ {
698
+ "team": ["KC", "BUF", "BAL"],
699
+ "espn_id": ["3139477", "3918298", "3916387"],
700
+ "wins": [12, 10, 9],
701
+ }
702
+ )
703
+
704
+ gt_theme_tufte(GT(df), accent="#A0522D")
705
+
706
+ See Also:
707
+ Ported from sdvplotR ``gt_theme_tufte()``: https://sdvplotR.sportsdataverse.org/reference/gt_theme_tufte.html
708
+ """
709
+ d = _density(density)
710
+ accent = _color(accent, "accent")
711
+ ink, secondary, hair, bg = "#111111", "#6F6A60", "#C9C4B8", "#FFFFF8"
712
+ gt, tid = _table_id(_check_gt(gt))
713
+ serif = _font("EB Garamond")
714
+ label = _text(font=serif, weight=400, style="italic", size=px(d["label"] + 2), color=secondary)
715
+ gt = (
716
+ _table_font(gt, _font("EB Garamond"))
717
+ .tab_style(_text(color=ink, size=px(d["body"] + 1)), loc.body())
718
+ .tab_style(_text(font=serif, weight=500, size=px(d["title"]), color=ink), loc.title())
719
+ .tab_style(
720
+ _text(font=serif, weight=400, style="italic", size=px(d["subtitle"]), color=secondary), loc.subtitle()
721
+ )
722
+ # italic labels, de-emphasized
723
+ .tab_style(label, loc.column_labels())
724
+ .pipe(_on_spanners, label)
725
+ .tab_style(
726
+ _text(font=serif, weight=600, style="italic", size=px(d["group"] + 2), color=accent), loc.row_groups()
727
+ )
728
+ .tab_style(
729
+ _text(font=serif, style="italic", size=px(d["source"] + 1), color=secondary),
730
+ [loc.source_notes(), loc.footnotes()],
731
+ )
732
+ .tab_options(
733
+ table_background_color=bg,
734
+ table_font_size=px(d["body"] + 1),
735
+ data_row_padding=px(d["pad"]),
736
+ heading_align="left",
737
+ heading_border_bottom_style="none",
738
+ heading_padding=px(d["pad"]),
739
+ # no top rule
740
+ table_border_top_style="none",
741
+ table_border_bottom_style="none",
742
+ column_labels_border_top_style="none",
743
+ # the one rule, a hairline under the labels
744
+ column_labels_border_bottom_style="solid",
745
+ column_labels_border_bottom_width=px(1),
746
+ column_labels_border_bottom_color=accent,
747
+ column_labels_padding=px(max(d["pad"] - 1, 2)),
748
+ # nothing between the rows, a faint hairline to close the body
749
+ table_body_border_top_style="none",
750
+ table_body_hlines_style="none",
751
+ table_body_border_bottom_style="solid",
752
+ table_body_border_bottom_width=px(1),
753
+ table_body_border_bottom_color=hair,
754
+ column_labels_vlines_style="none",
755
+ table_body_vlines_style="none",
756
+ stub_border_style="none",
757
+ # row groups get a label, no band and no rule
758
+ row_group_border_top_style="none",
759
+ row_group_border_bottom_style="none",
760
+ row_group_padding=px(max(d["pad"] - 1, 2)),
761
+ source_notes_border_lr_style="none",
762
+ source_notes_border_bottom_style="none",
763
+ source_notes_padding=px(d["pad"]),
764
+ )
765
+ .opt_css("\n".join([_tabular_nums(tid), _css(tid, ".gt_sourcenote", f"padding-top: {d['pad'] + 4}px;")]))
766
+ )
767
+ return gt.tab_options(**options)
768
+
769
+
770
+ def gt_theme_brutalist(gt: GT, accent: str = "#FF3B00", density: str = "comfortable", **options: Any) -> GT:
771
+ """Brutalist theme: heavy black frame, a knocked-out black label bar and one loud accent.
772
+
773
+ Args:
774
+ gt: The great_tables ``GT`` to theme.
775
+ accent: The single accent color, on the row-group labels (hex).
776
+ density: The type and padding scale: "comfortable", "compact" or "social".
777
+ **options: Passed to ``GT.tab_options`` last, so they override the theme.
778
+
779
+ Returns:
780
+ GT: A new ``GT`` with the theme applied.
781
+
782
+ Raises:
783
+ TypeError: ``gt`` is not a great_tables ``GT``.
784
+ ValueError: ``accent`` is not hex, or ``density`` is not one of the three scales.
785
+
786
+ Example:
787
+ ::
788
+
789
+ from great_tables import GT
790
+ import polars as pl
791
+ from sdvplot.great_tables import gt_theme_brutalist
792
+
793
+ df = pl.DataFrame(
794
+ {
795
+ "team": ["KC", "BUF", "BAL"],
796
+ "espn_id": ["3139477", "3918298", "3916387"],
797
+ "wins": [12, 10, 9],
798
+ }
799
+ )
800
+
801
+ gt_theme_brutalist(GT(df), accent="#0047FF")
802
+
803
+ See Also:
804
+ Ported from sdvplotR ``gt_theme_brutalist()``:
805
+ https://sdvplotR.sportsdataverse.org/reference/gt_theme_brutalist.html
806
+ """
807
+ d = _density(density)
808
+ accent = _color(accent, "accent")
809
+ ink = "#000000"
810
+ gt, tid = _table_id(_check_gt(gt))
811
+ gt = (
812
+ _table_font(gt, _font("Archivo"), weight=500)
813
+ .tab_style(_text(color=ink, size=px(d["body"]), weight=500), loc.body())
814
+ .tab_style(
815
+ _text(font=_font("Archivo Black"), size=px(d["title"] + 6), color=ink, transform="uppercase"), loc.title()
816
+ )
817
+ .tab_style(_text(weight=600, size=px(d["subtitle"]), color=ink), loc.subtitle())
818
+ # label row is a solid black bar, knocked out white
819
+ .tab_style(
820
+ _text(weight=700, size=px(d["label"] + 1), color="#FFFFFF", transform="uppercase"), loc.column_labels()
821
+ )
822
+ .pipe(_on_spanners, _text(weight=700, size=px(d["label"] + 1), color=ink, transform="uppercase"))
823
+ .tab_style(_text(weight=700, size=px(d["group"] + 1), color=accent, transform="uppercase"), loc.row_groups())
824
+ .tab_style(_text(size=px(d["source"]), color=ink, weight=500), [loc.source_notes(), loc.footnotes()])
825
+ .tab_options(
826
+ table_background_color="#FFFFFF",
827
+ column_labels_background_color=ink,
828
+ table_font_size=px(d["body"]),
829
+ data_row_padding=px(d["pad"]),
830
+ table_border_top_style="solid",
831
+ table_border_top_width=px(3),
832
+ table_border_top_color=ink,
833
+ table_border_bottom_style="solid",
834
+ table_border_bottom_width=px(3),
835
+ table_border_bottom_color=ink,
836
+ table_border_left_style="solid",
837
+ table_border_left_width=px(3),
838
+ table_border_left_color=ink,
839
+ table_border_right_style="solid",
840
+ table_border_right_width=px(3),
841
+ table_border_right_color=ink,
842
+ heading_align="left",
843
+ heading_border_bottom_style="solid",
844
+ heading_border_bottom_width=px(3),
845
+ heading_border_bottom_color=ink,
846
+ heading_padding=px(d["pad"] + 2),
847
+ column_labels_border_top_style="none",
848
+ column_labels_border_bottom_style="none",
849
+ column_labels_padding=px(d["pad"]),
850
+ table_body_border_top_style="none",
851
+ table_body_hlines_color=ink,
852
+ table_body_hlines_width=px(1),
853
+ table_body_border_bottom_style="none",
854
+ row_group_border_top_style="solid",
855
+ row_group_border_top_width=px(2),
856
+ row_group_border_top_color=ink,
857
+ row_group_border_bottom_style="none",
858
+ row_group_padding=px(max(d["pad"] - 1, 3)),
859
+ source_notes_border_lr_style="none",
860
+ source_notes_border_bottom_style="none",
861
+ source_notes_padding=px(d["pad"] + 2),
862
+ )
863
+ .opt_css(
864
+ "\n".join(
865
+ [
866
+ _tabular_nums(tid),
867
+ _last_row_border(tid, "#FFFFFF"),
868
+ _css(tid, ".gt_col_heading", "letter-spacing: 0.04em;"),
869
+ _css(tid, ".gt_group_heading", "letter-spacing: 0.06em;"),
870
+ _css(
871
+ tid,
872
+ ".gt_title",
873
+ "letter-spacing: -0.02em; line-height: 1.05; "
874
+ f"padding-bottom: {math.ceil(d['pad'] / 2)}px !important;",
875
+ ),
876
+ _css(tid, ".gt_subtitle", f"padding-bottom: {d['pad'] + 2}px !important;"),
877
+ ]
878
+ )
879
+ )
880
+ )
881
+ return gt.tab_options(**options)
882
+
883
+
884
+ def gt_theme_drench(gt: GT, color: str = "#123F5E", density: str = "comfortable", **options: Any) -> GT:
885
+ """Drenched theme: the whole table in one color, with rules, bands and muted text all derived from it.
886
+
887
+ The type is black or white, whichever reads better on ``color``; the muted text blends the type into the ground
888
+ until it clears 4.5:1 contrast; the rules and the row-group band shift the ground's luminance.
889
+
890
+ Args:
891
+ gt: The great_tables ``GT`` to theme.
892
+ color: The hex color the table is drenched in, from near-black to a mid-saturation brand color.
893
+ density: The type and padding scale: "comfortable", "compact" or "social".
894
+ **options: Passed to ``GT.tab_options`` last, so they override the theme.
895
+
896
+ Returns:
897
+ GT: A new ``GT`` with the theme applied.
898
+
899
+ Raises:
900
+ TypeError: ``gt`` is not a great_tables ``GT``.
901
+ ValueError: ``color`` is not hex, or ``density`` is not one of the three scales.
902
+
903
+ Example:
904
+ ::
905
+
906
+ from great_tables import GT
907
+ import polars as pl
908
+ from sdvplot.great_tables import gt_theme_drench
909
+
910
+ df = pl.DataFrame(
911
+ {
912
+ "team": ["KC", "BUF", "BAL"],
913
+ "espn_id": ["3139477", "3918298", "3916387"],
914
+ "wins": [12, 10, 9],
915
+ }
916
+ )
917
+
918
+ gt_theme_drench(GT(df), color="#E31837")
919
+
920
+ See Also:
921
+ Ported from sdvplotR ``gt_theme_drench()``: https://sdvplotR.sportsdataverse.org/reference/gt_theme_drench.html
922
+ """
923
+ d = _density(density)
924
+ color = _color(color, "color")
925
+ # everything derives from the ground so any hue holds together
926
+ ink = on_color(color)
927
+ dark_type = ink == "#000000"
928
+ # shift the ground rather than laying gray over it
929
+ rule = _adjust_luminance(color, -0.6 if dark_type else 0.9)
930
+ surface = _adjust_luminance(color, 0.5 if dark_type else -0.7)
931
+ # a fixed luminance step fails on some hues, so blend until it clears 4.5:1
932
+ secondary = _secondary_on(color, ink, 4.5)
933
+ gt, tid = _table_id(_check_gt(gt))
934
+ label = _text(weight=700, size=px(d["label"]), color=secondary, transform="uppercase")
935
+ gt = (
936
+ _table_font(gt, _font("Gabarito"))
937
+ .tab_style(_text(color=ink, size=px(d["body"]), weight=500), loc.body())
938
+ .tab_style(_text(weight=700, size=px(d["title"] + 2), color=ink), loc.title())
939
+ .tab_style(_text(weight=400, size=px(d["subtitle"]), color=secondary), loc.subtitle())
940
+ .tab_style(label, loc.column_labels())
941
+ .pipe(_on_spanners, label)
942
+ .tab_style(
943
+ [
944
+ _text(weight=700, size=px(d["group"]), color=ink, transform="uppercase"),
945
+ important(style.fill(color=surface)),
946
+ ],
947
+ loc.row_groups(),
948
+ )
949
+ .tab_style(_text(size=px(d["source"]), color=secondary), [loc.source_notes(), loc.footnotes()])
950
+ .tab_options(
951
+ table_background_color=color,
952
+ heading_background_color=color,
953
+ column_labels_background_color=color,
954
+ row_group_background_color=color,
955
+ stub_background_color=color,
956
+ source_notes_background_color=color,
957
+ table_font_size=px(d["body"]),
958
+ data_row_padding=px(d["pad"] + 1),
959
+ table_border_top_style="none",
960
+ table_border_bottom_style="none",
961
+ heading_align="left",
962
+ heading_border_bottom_style="none",
963
+ heading_padding=px(d["pad"] + 2),
964
+ column_labels_border_top_style="none",
965
+ column_labels_border_bottom_style="solid",
966
+ column_labels_border_bottom_width=px(1),
967
+ column_labels_border_bottom_color=rule,
968
+ column_labels_padding=px(d["pad"] + 1),
969
+ table_body_border_top_style="none",
970
+ table_body_hlines_color=rule,
971
+ table_body_hlines_width=px(1),
972
+ table_body_border_bottom_style="none",
973
+ row_group_border_top_style="none",
974
+ row_group_border_bottom_style="none",
975
+ row_group_padding=px(max(d["pad"] - 1, 3)),
976
+ source_notes_border_lr_style="none",
977
+ source_notes_border_bottom_style="none",
978
+ source_notes_padding=px(d["pad"] + 2),
979
+ )
980
+ .opt_css(
981
+ "\n".join(
982
+ [
983
+ _tabular_nums(tid),
984
+ _last_row_border(tid, color),
985
+ # light type on a saturated ground reads lighter than it is
986
+ *([] if dark_type else [_css(tid, ["td", "th"], "line-height: 1.55;")]),
987
+ _css(tid, [".gt_col_heading", ".gt_column_spanner"], "letter-spacing: 0.08em;"),
988
+ _css(tid, ".gt_group_heading", "letter-spacing: 0.06em;"),
989
+ _css(tid, ".gt_subtitle", f"padding-bottom: {d['pad'] + 8}px !important;"),
990
+ _css(tid, ".gt_title", f"padding-bottom: {math.ceil(d['pad'] / 2)}px !important;"),
991
+ ]
992
+ )
993
+ )
994
+ )
995
+ return gt.tab_options(**options)
996
+
997
+
998
+ def gt_theme_midnight(gt: GT, accent: str = "#5B8DEF", density: str = "comfortable", **options: Any) -> GT:
999
+ """Dark theme: light type on a near-black ground, a raised label band and one cool accent.
1000
+
1001
+ Args:
1002
+ gt: The great_tables ``GT`` to theme.
1003
+ accent: Hex color of the row-group labels and the rule above the table.
1004
+ density: The type and padding scale: "comfortable", "compact" or "social".
1005
+ **options: Passed to ``GT.tab_options`` last, so they override the theme.
1006
+
1007
+ Returns:
1008
+ GT: A new ``GT`` with the theme applied.
1009
+
1010
+ Raises:
1011
+ TypeError: ``gt`` is not a great_tables ``GT``.
1012
+ ValueError: ``accent`` is not hex, or ``density`` is not one of the three scales.
1013
+
1014
+ Example:
1015
+ ::
1016
+
1017
+ from great_tables import GT
1018
+ import polars as pl
1019
+ from sdvplot.great_tables import gt_theme_midnight
1020
+
1021
+ df = pl.DataFrame(
1022
+ {
1023
+ "team": ["KC", "BUF", "BAL"],
1024
+ "espn_id": ["3139477", "3918298", "3916387"],
1025
+ "wins": [12, 10, 9],
1026
+ }
1027
+ )
1028
+
1029
+ gt_theme_midnight(GT(df), accent="#3FBF87")
1030
+
1031
+ See Also:
1032
+ Ported from sdvplotR ``gt_theme_midnight()``:
1033
+ https://sdvplotR.sportsdataverse.org/reference/gt_theme_midnight.html
1034
+ """
1035
+ d = _density(density)
1036
+ accent = _color(accent, "accent")
1037
+ ground, surface, primary, secondary, rule = "#0C0D10", "#16181D", "#E8E9ED", "#9498A3", "#24272E"
1038
+ gt, tid = _table_id(_check_gt(gt))
1039
+ # sentence case; uppercase tracking is too loud on this ground
1040
+ label = _text(weight=600, size=px(d["label"] + 1), color=secondary)
1041
+ gt = (
1042
+ _table_font(gt, _font("Chivo"))
1043
+ .tab_style(_text(color=primary, size=px(d["body"])), loc.body())
1044
+ .tab_style(_text(weight=700, size=px(d["title"]), color=primary), loc.title())
1045
+ .tab_style(_text(weight=400, size=px(d["subtitle"]), color=secondary), loc.subtitle())
1046
+ .tab_style(label, loc.column_labels())
1047
+ .pipe(_on_spanners, label)
1048
+ .tab_style(_text(weight=700, size=px(d["group"]), color=accent), loc.row_groups())
1049
+ .tab_style(_text(size=px(d["source"]), color=secondary), [loc.source_notes(), loc.footnotes()])
1050
+ .tab_options(
1051
+ table_background_color=ground,
1052
+ heading_background_color=ground,
1053
+ column_labels_background_color=surface,
1054
+ row_group_background_color=ground,
1055
+ stub_background_color=ground,
1056
+ source_notes_background_color=ground,
1057
+ table_font_size=px(d["body"]),
1058
+ data_row_padding=px(d["pad"]),
1059
+ table_border_top_style="solid",
1060
+ table_border_top_width=px(2),
1061
+ table_border_top_color=accent,
1062
+ table_border_bottom_style="none",
1063
+ heading_align="left",
1064
+ heading_border_bottom_style="none",
1065
+ heading_padding=px(d["pad"]),
1066
+ column_labels_border_top_style="none",
1067
+ column_labels_border_bottom_style="solid",
1068
+ column_labels_border_bottom_width=px(1),
1069
+ column_labels_border_bottom_color=rule,
1070
+ column_labels_padding=px(max(d["pad"] - 1, 3)),
1071
+ table_body_border_top_style="none",
1072
+ table_body_hlines_color=rule,
1073
+ table_body_hlines_width=px(1),
1074
+ table_body_border_bottom_style="solid",
1075
+ table_body_border_bottom_width=px(1),
1076
+ table_body_border_bottom_color=rule,
1077
+ row_group_border_top_style="solid",
1078
+ row_group_border_top_width=px(1),
1079
+ row_group_border_top_color=rule,
1080
+ row_group_border_bottom_style="none",
1081
+ row_group_padding=px(max(d["pad"] - 2, 2)),
1082
+ source_notes_border_lr_style="none",
1083
+ source_notes_border_bottom_style="none",
1084
+ source_notes_padding=px(d["pad"]),
1085
+ )
1086
+ .opt_css(
1087
+ "\n".join(
1088
+ [
1089
+ _tabular_nums(tid),
1090
+ _last_row_border(tid, ground),
1091
+ # light on dark reads lighter than it is, so open the leading
1092
+ _css(tid, ["td", "th"], "line-height: 1.55;"),
1093
+ _css(tid, ".gt_subtitle", f"padding-bottom: {d['pad'] + 8}px !important;"),
1094
+ _css(tid, ".gt_title", f"padding-bottom: {math.ceil(d['pad'] / 2)}px !important;"),
1095
+ ]
1096
+ )
1097
+ )
1098
+ )
1099
+ return gt.tab_options(**options)
1100
+
1101
+
1102
+ # sdvplotR's pal_midnight: a rank palette for dark backgrounds, five colors running best (green) to worst (red), its
1103
+ # luminance lifted so every step clears 4.5:1 on gt_theme_midnight's #0C0D10 ground (and gt_theme_terminal's). The
1104
+ # usual green-to-red ramp is built for white paper; its mid-tones sink into a near-black ground.
1105
+ pal_midnight: tuple[str, ...] = ("#3FBF87", "#8FD9A8", "#D8D6A0", "#E8996B", "#E0645C")
1106
+
1107
+
1108
+ def gt_theme_scoreboard(gt: GT, accent: str = "#0E1621", density: str = "compact", **options: Any) -> GT:
1109
+ """Scoreboard theme: condensed uppercase type under a solid header band, like a broadcast stat panel.
1110
+
1111
+ Args:
1112
+ gt: The great_tables ``GT`` to theme.
1113
+ accent: Hex color of the header band; the label color (black or white) adapts to it.
1114
+ density: The type and padding scale: "comfortable", "compact" or "social".
1115
+ **options: Passed to ``GT.tab_options`` last, so they override the theme.
1116
+
1117
+ Returns:
1118
+ GT: A new ``GT`` with the theme applied.
1119
+
1120
+ Raises:
1121
+ TypeError: ``gt`` is not a great_tables ``GT``.
1122
+ ValueError: ``accent`` is not hex, or ``density`` is not one of the three scales.
1123
+
1124
+ Example:
1125
+ ::
1126
+
1127
+ from great_tables import GT
1128
+ import polars as pl
1129
+ from sdvplot.great_tables import gt_theme_scoreboard
1130
+
1131
+ df = pl.DataFrame(
1132
+ {
1133
+ "team": ["KC", "BUF", "BAL"],
1134
+ "espn_id": ["3139477", "3918298", "3916387"],
1135
+ "wins": [12, 10, 9],
1136
+ }
1137
+ )
1138
+
1139
+ gt_theme_scoreboard(GT(df), accent="#FFC20E")
1140
+
1141
+ See Also:
1142
+ Ported from sdvplotR ``gt_theme_scoreboard()``:
1143
+ https://sdvplotR.sportsdataverse.org/reference/gt_theme_scoreboard.html
1144
+ """
1145
+ d = _density(density)
1146
+ accent = _color(accent, "accent")
1147
+ ink, rule, muted = "#141719", "#E6E9ED", "#5A6069"
1148
+ # a pale accent needs dark labels, so measure it
1149
+ on_accent = on_color(accent)
1150
+ gt, tid = _table_id(_check_gt(gt))
1151
+ condensed = _font("Barlow Condensed")
1152
+ label = _text(font=condensed, weight=700, size=px(d["label"] + 2), color=on_accent, transform="uppercase")
1153
+ gt = (
1154
+ _table_font(gt, _font("Barlow"))
1155
+ .tab_style(_text(color=ink, size=px(d["body"]), weight=500), loc.body())
1156
+ .tab_style(
1157
+ _text(font=condensed, weight=700, size=px(d["title"] + 4), color=ink, transform="uppercase"), loc.title()
1158
+ )
1159
+ .tab_style(_text(font=condensed, weight=500, size=px(d["subtitle"] + 1), color=muted), loc.subtitle())
1160
+ .tab_style(label, loc.column_labels())
1161
+ .pipe(_on_spanners, label)
1162
+ .tab_style(
1163
+ [
1164
+ _text(font=condensed, weight=700, size=px(d["group"] + 1), color=accent, transform="uppercase"),
1165
+ important(style.fill(color="#F2F4F6")),
1166
+ ],
1167
+ loc.row_groups(),
1168
+ )
1169
+ .tab_style(_text(size=px(d["source"]), color=muted), [loc.source_notes(), loc.footnotes()])
1170
+ .tab_options(
1171
+ table_background_color="#FFFFFF",
1172
+ column_labels_background_color=accent,
1173
+ table_font_size=px(d["body"]),
1174
+ data_row_padding=px(d["pad"]),
1175
+ table_border_top_style="none",
1176
+ table_border_bottom_style="none",
1177
+ heading_align="left",
1178
+ heading_border_bottom_style="none",
1179
+ heading_padding=px(d["pad"] + 1),
1180
+ column_labels_border_top_style="none",
1181
+ column_labels_border_bottom_style="none",
1182
+ column_labels_padding=px(d["pad"] + 2),
1183
+ table_body_border_top_style="none",
1184
+ table_body_hlines_color=rule,
1185
+ table_body_hlines_width=px(1),
1186
+ table_body_border_bottom_style="solid",
1187
+ table_body_border_bottom_width=px(2),
1188
+ table_body_border_bottom_color=accent,
1189
+ row_group_border_top_style="none",
1190
+ row_group_border_bottom_style="none",
1191
+ row_group_padding=px(max(d["pad"], 3)),
1192
+ source_notes_border_lr_style="none",
1193
+ source_notes_border_bottom_style="none",
1194
+ source_notes_padding=px(d["pad"] + 2),
1195
+ )
1196
+ .opt_css(
1197
+ "\n".join(
1198
+ [
1199
+ _tabular_nums(tid),
1200
+ _last_row_border(tid, "#FFFFFF"),
1201
+ _css(tid, [".gt_col_heading", ".gt_column_spanner"], "letter-spacing: 0.06em;"),
1202
+ _css(tid, ".gt_group_heading", "letter-spacing: 0.06em;"),
1203
+ _css(tid, ".gt_heading", "letter-spacing: 0.01em;"),
1204
+ _css(tid, ".gt_subtitle", f"padding-bottom: {d['pad'] + 6}px !important;"),
1205
+ ]
1206
+ )
1207
+ )
1208
+ )
1209
+ return gt.tab_options(**options)
1210
+
1211
+
1212
+ def gt_theme_terminal(gt: GT, accent: str = "#FFB86C", density: str = "compact", **options: Any) -> GT:
1213
+ """Terminal theme: a monospaced readout on a near-black ground, with a rule on every row.
1214
+
1215
+ Args:
1216
+ gt: The great_tables ``GT`` to theme.
1217
+ accent: Hex color of the column labels, row groups and the top rule. The default is amber; "#7EE787" gives a
1218
+ green-phosphor variant.
1219
+ density: The type and padding scale: "comfortable", "compact" or "social".
1220
+ **options: Passed to ``GT.tab_options`` last, so they override the theme.
1221
+
1222
+ Returns:
1223
+ GT: A new ``GT`` with the theme applied.
1224
+
1225
+ Raises:
1226
+ TypeError: ``gt`` is not a great_tables ``GT``.
1227
+ ValueError: ``accent`` is not hex, or ``density`` is not one of the three scales.
1228
+
1229
+ Example:
1230
+ ::
1231
+
1232
+ from great_tables import GT
1233
+ import polars as pl
1234
+ from sdvplot.great_tables import gt_theme_terminal
1235
+
1236
+ df = pl.DataFrame(
1237
+ {
1238
+ "team": ["KC", "BUF", "BAL"],
1239
+ "espn_id": ["3139477", "3918298", "3916387"],
1240
+ "wins": [12, 10, 9],
1241
+ }
1242
+ )
1243
+
1244
+ gt_theme_terminal(GT(df), accent="#7EE787")
1245
+
1246
+ See Also:
1247
+ Ported from sdvplotR ``gt_theme_terminal()``:
1248
+ https://sdvplotR.sportsdataverse.org/reference/gt_theme_terminal.html
1249
+ """
1250
+ d = _density(density)
1251
+ accent = _color(accent, "accent")
1252
+ ground, primary, secondary, rule = "#0F1115", "#C9D1D9", "#7D8590", "#262B33"
1253
+ gt, tid = _table_id(_check_gt(gt))
1254
+ label = _text(weight=700, size=px(d["label"]), color=accent, transform="uppercase")
1255
+ gt = (
1256
+ _table_font(gt, _font("JetBrains Mono"))
1257
+ .tab_style(_text(color=primary, size=px(d["body"])), loc.body())
1258
+ .tab_style(_text(weight=700, size=px(d["title"] - 2), color=primary, transform="uppercase"), loc.title())
1259
+ .tab_style(_text(weight=400, size=px(d["subtitle"] - 1), color=secondary), loc.subtitle())
1260
+ .tab_style(label, loc.column_labels())
1261
+ .pipe(_on_spanners, label)
1262
+ .tab_style(_text(weight=700, size=px(d["group"]), color=accent, transform="uppercase"), loc.row_groups())
1263
+ .tab_style(_text(size=px(d["source"]), color=secondary), [loc.source_notes(), loc.footnotes()])
1264
+ .tab_options(
1265
+ table_background_color=ground,
1266
+ heading_background_color=ground,
1267
+ column_labels_background_color=ground,
1268
+ row_group_background_color=ground,
1269
+ stub_background_color=ground,
1270
+ source_notes_background_color=ground,
1271
+ table_font_size=px(d["body"]),
1272
+ data_row_padding=px(d["pad"]),
1273
+ table_border_top_style="solid",
1274
+ table_border_top_width=px(1),
1275
+ table_border_top_color=accent,
1276
+ table_border_bottom_style="solid",
1277
+ table_border_bottom_width=px(1),
1278
+ table_border_bottom_color=rule,
1279
+ heading_align="left",
1280
+ heading_border_bottom_style="none",
1281
+ heading_padding=px(d["pad"] + 2),
1282
+ column_labels_border_top_style="none",
1283
+ column_labels_border_bottom_style="solid",
1284
+ column_labels_border_bottom_width=px(1),
1285
+ column_labels_border_bottom_color=accent,
1286
+ column_labels_padding=px(d["pad"] + 1),
1287
+ # rule on every row, it's a readout
1288
+ table_body_border_top_style="none",
1289
+ table_body_hlines_color=rule,
1290
+ table_body_hlines_width=px(1),
1291
+ table_body_border_bottom_style="none",
1292
+ row_group_border_top_style="solid",
1293
+ row_group_border_top_width=px(1),
1294
+ row_group_border_top_color=rule,
1295
+ row_group_border_bottom_style="none",
1296
+ row_group_padding=px(max(d["pad"], 3)),
1297
+ source_notes_border_lr_style="none",
1298
+ source_notes_border_bottom_style="none",
1299
+ source_notes_padding=px(d["pad"] + 2),
1300
+ )
1301
+ .opt_css(
1302
+ "\n".join(
1303
+ [
1304
+ _tabular_nums(tid),
1305
+ _last_row_border(tid, ground),
1306
+ _css(tid, ["td", "th"], "line-height: 1.5;"),
1307
+ _css(tid, ".gt_col_heading", "letter-spacing: 0.08em;"),
1308
+ _css(tid, ".gt_title", "letter-spacing: 0.04em; padding-bottom: 2px !important;"),
1309
+ _css(tid, ".gt_subtitle", f"padding-bottom: {d['pad'] + 6}px !important;"),
1310
+ ]
1311
+ )
1312
+ )
1313
+ )
1314
+ return gt.tab_options(**options)
1315
+
1316
+
1317
+ def gt_theme_athletic(gt: GT, density: str = "comfortable", **options: Any) -> GT:
1318
+ """The Athletic's table look: a monospaced body, uppercase sans labels, dotted row rules and thin column rules.
1319
+
1320
+ The row-group band is solid black with knocked-out white labels, and every column is centered. The theme sets its
1321
+ sizes directly, so ``density`` rescales the finished table (as sdvplotR does).
1322
+
1323
+ Args:
1324
+ gt: The great_tables ``GT`` to theme.
1325
+ density: The type and padding scale: "comfortable" keeps the theme's sizes, "compact" scales them down and
1326
+ "social" up.
1327
+ **options: Passed to ``GT.tab_options`` last, so they override the theme.
1328
+
1329
+ Returns:
1330
+ GT: A new ``GT`` with the theme applied.
1331
+
1332
+ Raises:
1333
+ TypeError: ``gt`` is not a great_tables ``GT``.
1334
+ ValueError: ``density`` is not one of the three scales.
1335
+
1336
+ Example:
1337
+ ::
1338
+
1339
+ from great_tables import GT
1340
+ import polars as pl
1341
+ from sdvplot.great_tables import gt_theme_athletic
1342
+
1343
+ df = pl.DataFrame(
1344
+ {
1345
+ "team": ["KC", "BUF", "BAL"],
1346
+ "espn_id": ["3139477", "3918298", "3916387"],
1347
+ "wins": [12, 10, 9],
1348
+ }
1349
+ )
1350
+
1351
+ gt_theme_athletic(GT(df), density="compact")
1352
+
1353
+ See Also:
1354
+ Ported from sdvplotR ``gt_theme_athletic()``:
1355
+ https://sdvplotR.sportsdataverse.org/reference/gt_theme_athletic.html
1356
+ """
1357
+ _density(density)
1358
+ gt, tid = _table_id(_check_gt(gt))
1359
+ columns = _shape(gt)[0]
1360
+ work = _font("Work Sans")
1361
+ table = (
1362
+ _table_font(gt, _font("Spline Sans Mono"), weight=500)
1363
+ .tab_style(_text(font=work, weight=650, size=px(12), transform="uppercase"), loc.column_labels())
1364
+ .tab_style(_text(font=work, weight=650, size=px(22)), loc.title())
1365
+ .tab_style(_text(font=work, weight=500, size=px(14)), loc.subtitle())
1366
+ .tab_style(
1367
+ [_text(weight=650, size=px(12), color="white"), important(style.fill(color="black"))], loc.row_groups()
1368
+ )
1369
+ # column rules: a left border on every column but the first, so the stub reads without a leading rule
1370
+ .tab_style(important(style.borders(sides="left", weight=px(0.5), color="black")), loc.body(columns=columns[1:]))
1371
+ .tab_style(important(style.borders(sides="top", color="black", weight=px(1.5), style="dotted")), loc.body())
1372
+ .cols_align("center")
1373
+ .tab_options(
1374
+ table_font_size=px(12),
1375
+ column_labels_border_top_style="none",
1376
+ column_labels_border_bottom_style="solid",
1377
+ column_labels_border_bottom_width=px(1),
1378
+ column_labels_border_bottom_color="black",
1379
+ table_border_top_style="none",
1380
+ table_border_bottom_style="none",
1381
+ table_body_border_top_style="none",
1382
+ heading_border_bottom_style="none",
1383
+ heading_align="left",
1384
+ heading_title_font_size=px(26),
1385
+ source_notes_border_lr_style="none",
1386
+ source_notes_font_size=px(10),
1387
+ row_group_border_top_style="none",
1388
+ row_group_border_top_color="black",
1389
+ row_group_border_bottom_width=px(1),
1390
+ row_group_border_bottom_color="black",
1391
+ row_group_border_bottom_style="solid",
1392
+ row_group_padding=px(1.5),
1393
+ )
1394
+ .opt_css(
1395
+ "\n".join(
1396
+ [
1397
+ _last_row_border(tid, "#FFFFFF"),
1398
+ _css(tid, ".gt_subtitle", "padding-top: 0px !important; padding-bottom: 4px !important;"),
1399
+ _css(tid, ".gt_sourcenote", "border-bottom-color: #FFFDF5 !important;"),
1400
+ _css(tid, ".gt_heading", "padding-bottom: 0px; padding-top: 6px;"),
1401
+ ]
1402
+ )
1403
+ )
1404
+ )
1405
+ return _scale_output(table, density).tab_options(**options)
1406
+
1407
+
1408
+ def gt_theme_gtutils(gt: GT, density: str = "comfortable", **options: Any) -> GT:
1409
+ """The gtUtils look: Almarai and Signika Negative on cream, taupe row rules and a taupe row-group band.
1410
+
1411
+ Args:
1412
+ gt: The great_tables ``GT`` to theme.
1413
+ density: The type and padding scale: "comfortable" keeps the theme's sizes, "compact" scales them down and
1414
+ "social" up.
1415
+ **options: Passed to ``GT.tab_options`` last, so they override the theme.
1416
+
1417
+ Returns:
1418
+ GT: A new ``GT`` with the theme applied.
1419
+
1420
+ Raises:
1421
+ TypeError: ``gt`` is not a great_tables ``GT``.
1422
+ ValueError: ``density`` is not one of the three scales.
1423
+
1424
+ Example:
1425
+ ::
1426
+
1427
+ from great_tables import GT
1428
+ import polars as pl
1429
+ from sdvplot.great_tables import gt_theme_gtutils
1430
+
1431
+ df = pl.DataFrame(
1432
+ {
1433
+ "team": ["KC", "BUF", "BAL"],
1434
+ "espn_id": ["3139477", "3918298", "3916387"],
1435
+ "wins": [12, 10, 9],
1436
+ }
1437
+ )
1438
+
1439
+ gt_theme_gtutils(GT(df))
1440
+
1441
+ See Also:
1442
+ Ported from sdvplotR ``gt_theme_gtutils()``: https://sdvplotR.sportsdataverse.org/reference/gt_theme_gtutils.html
1443
+ """
1444
+ _density(density)
1445
+ gt, tid = _table_id(_check_gt(gt))
1446
+ signika, almarai = _font("Signika Negative"), _font("Almarai")
1447
+ table = (
1448
+ _table_font(gt, _font("Almarai"), weight=500)
1449
+ .tab_style(_text(font=signika, weight=650), loc.title())
1450
+ .tab_style(_text(font=signika, weight=500), loc.subtitle())
1451
+ .tab_style(_text(font=signika, weight=650, size=px(14)), loc.column_labels())
1452
+ .pipe(_on_spanners, _text(font=signika, weight=650, size=px(13)))
1453
+ .tab_style(
1454
+ [_text(font=signika, weight=650, size=px(14), color="#FFFDF5"), important(style.fill(color="#8A817C"))],
1455
+ loc.row_groups(),
1456
+ )
1457
+ .tab_style(_text(font=almarai, size=px(12)), [loc.source_notes(), loc.footnotes()])
1458
+ .pipe(_row_rules, "#8A817C")
1459
+ .cols_align("center")
1460
+ .tab_options(
1461
+ data_row_padding=px(1),
1462
+ table_body_hlines_color="transparent",
1463
+ column_labels_border_top_style="none",
1464
+ column_labels_border_bottom_style="solid",
1465
+ column_labels_border_bottom_width=px(1),
1466
+ column_labels_border_bottom_color="black",
1467
+ row_group_border_top_style="none",
1468
+ row_group_border_top_color="black",
1469
+ row_group_border_bottom_width=px(1),
1470
+ row_group_border_bottom_color="black",
1471
+ row_group_border_bottom_style="solid",
1472
+ row_group_padding=px(1.5),
1473
+ heading_align="left",
1474
+ heading_border_bottom_style="none",
1475
+ table_body_border_top_style="none",
1476
+ table_border_bottom_style="none",
1477
+ table_border_top_style="none",
1478
+ source_notes_border_lr_style="none",
1479
+ table_background_color="#FFFDF5",
1480
+ table_border_top_color="#FFFDF5",
1481
+ table_border_right_color="#FFFDF5",
1482
+ table_border_bottom_color="#FFFDF5",
1483
+ table_border_left_color="#FFFDF5",
1484
+ )
1485
+ .opt_css(
1486
+ "\n".join(
1487
+ [
1488
+ _last_row_border(tid, "#FFFDF5"),
1489
+ _css(tid, ".gt_col_heading", "padding-bottom: 2px; padding-top: 2px;"),
1490
+ _css(tid, ".gt_subtitle", "padding-top: 0px !important; padding-bottom: 4px !important;"),
1491
+ _css(tid, ".gt_sourcenote", "border-bottom-color: #FFFDF5 !important;"),
1492
+ _css(tid, ".gt_heading", "padding-bottom: 0px; padding-top: 6px;"),
1493
+ _css(tid, ".gt_column_spanner", "padding-bottom: 2px;"),
1494
+ ]
1495
+ )
1496
+ )
1497
+ )
1498
+ return _scale_output(table, density).tab_options(**options)
1499
+
1500
+
1501
+ # a body cell of a data row, the rows counted as drawn: row group headings and summary rows (whose cells, not rows,
1502
+ # carry the class) are neither counted nor painted
1503
+ _KENPOM_ROWS = (
1504
+ ".gt_table_body > tr:nth-child({} of :not(.gt_group_heading_row, :has(> .gt_summary_row, > .gt_grand_summary_row)))"
1505
+ " > td.gt_row"
1506
+ )
1507
+
1508
+
1509
+ def gt_theme_kenpom(gt: GT, density: str = "comfortable", **options: Any) -> GT:
1510
+ """KenPom's table look: blue-banded rows, a pale-blue label band with blue labels, black row rules.
1511
+
1512
+ The bands are a stylesheet rule, so a cell fill (``data_color``, ``tab_style(style.fill(...))``,
1513
+ ``gt_color_results``, ...) shows over them whether it is applied before or after the theme. They alternate over
1514
+ the data rows as drawn, so a grouped table bands by display order (sdvplotR bands by data row).
1515
+
1516
+ Args:
1517
+ gt: The great_tables ``GT`` to theme.
1518
+ density: The type and padding scale: "comfortable" keeps the theme's sizes, "compact" scales them down and
1519
+ "social" up.
1520
+ **options: Passed to ``GT.tab_options`` last, so they override the theme.
1521
+
1522
+ Returns:
1523
+ GT: A new ``GT`` with the theme applied.
1524
+
1525
+ Raises:
1526
+ TypeError: ``gt`` is not a great_tables ``GT``.
1527
+ ValueError: ``density`` is not one of the three scales.
1528
+
1529
+ Example:
1530
+ ::
1531
+
1532
+ from great_tables import GT
1533
+ import polars as pl
1534
+ from sdvplot.great_tables import gt_theme_kenpom
1535
+
1536
+ df = pl.DataFrame(
1537
+ {
1538
+ "team": ["KC", "BUF", "BAL"],
1539
+ "espn_id": ["3139477", "3918298", "3916387"],
1540
+ "wins": [12, 10, 9],
1541
+ }
1542
+ )
1543
+
1544
+ gt_theme_kenpom(GT(df))
1545
+
1546
+ See Also:
1547
+ Ported from sdvplotR ``gt_theme_kenpom()``: https://sdvplotR.sportsdataverse.org/reference/gt_theme_kenpom.html
1548
+ """
1549
+ _density(density)
1550
+ gt, tid = _table_id(_check_gt(gt))
1551
+ helvetica = _font("Helvetica Neue")
1552
+ band = [_text(font=helvetica, weight=650, size=px(14), color="#02b"), important(style.fill(color="#c3d9ff"))]
1553
+ table = (
1554
+ _table_font(gt, _font("Helvetica Neue"), weight=500)
1555
+ .tab_style(band, loc.column_labels())
1556
+ .tab_style(_text(font=helvetica, weight=650, size=px(18), align="left"), loc.title())
1557
+ .tab_style(_text(font=helvetica, weight=500, size=px(14), align="left"), loc.subtitle())
1558
+ .pipe(_on_spanners, _text(font=helvetica, weight=650, size=px(12)))
1559
+ .tab_style(band, loc.row_groups())
1560
+ .tab_style(_text(font=helvetica, size=px(12)), loc.source_notes())
1561
+ .tab_style(_text(weight="bold", font=helvetica, size=px(14)), loc.row_groups())
1562
+ .tab_style(_text(font=helvetica, size=px(12)), loc.footnotes())
1563
+ .pipe(_row_rules, "#000000")
1564
+ # uh this is kinda hacky but it works
1565
+ .pipe(_hide_spanner_row, tid)
1566
+ .tab_options(
1567
+ data_row_padding=px(2),
1568
+ table_body_hlines_color="transparent",
1569
+ column_labels_border_top_style="none",
1570
+ column_labels_border_bottom_style="none",
1571
+ row_group_border_top_style="none",
1572
+ row_group_border_top_color="black",
1573
+ row_group_border_bottom_width=px(1),
1574
+ row_group_border_bottom_color="black",
1575
+ row_group_border_bottom_style="solid",
1576
+ row_group_padding=px(1.5),
1577
+ heading_align="center",
1578
+ heading_border_bottom_style="none",
1579
+ table_body_border_top_style="none",
1580
+ table_body_border_bottom_color="white",
1581
+ table_border_bottom_style="none",
1582
+ table_border_top_style="none",
1583
+ source_notes_border_lr_style="none",
1584
+ )
1585
+ .opt_css(
1586
+ "\n".join(
1587
+ [
1588
+ _last_row_border(tid, "#FFFFFF"),
1589
+ # R's odd and even rows (1-based), banded by a stylesheet rule: a cell's own fill is an inline
1590
+ # style, so it shows whichever was applied first. great_tables never marks opt_css rules
1591
+ # !important (its VS Code and Positron repr marks only its own stylesheet's)
1592
+ _css(tid, _KENPOM_ROWS.format("odd"), "background-color: #F2FAFD;"),
1593
+ _css(tid, _KENPOM_ROWS.format("even"), "background-color: #e5ecf9;"),
1594
+ _css(tid, ".gt_col_heading", "padding-bottom: 2px; padding-top: 2px;"),
1595
+ _css(tid, ".gt_subtitle", "padding-top: 0px !important; padding-bottom: 4px !important;"),
1596
+ _css(tid, ".gt_heading", "padding-bottom: 0px; padding-top: 6px;"),
1597
+ _css(tid, ".gt_column_spanner", "text-decoration: underline;"),
1598
+ ]
1599
+ )
1600
+ )
1601
+ )
1602
+ return _scale_output(table, density).tab_options(**options)
1603
+
1604
+
1605
+ def gt_theme_ncaa(gt: GT, density: str = "comfortable", **options: Any) -> GT:
1606
+ """NCAA stats-site look: Open Sans, a black label band with white uppercase labels, striped rows, wide left inset.
1607
+
1608
+ Args:
1609
+ gt: The great_tables ``GT`` to theme.
1610
+ density: The type and padding scale: "comfortable" keeps the theme's sizes, "compact" scales them down and
1611
+ "social" up.
1612
+ **options: Passed to ``GT.tab_options`` last, so they override the theme.
1613
+
1614
+ Returns:
1615
+ GT: A new ``GT`` with the theme applied.
1616
+
1617
+ Raises:
1618
+ TypeError: ``gt`` is not a great_tables ``GT``.
1619
+ ValueError: ``density`` is not one of the three scales.
1620
+
1621
+ Example:
1622
+ ::
1623
+
1624
+ from great_tables import GT
1625
+ import polars as pl
1626
+ from sdvplot.great_tables import gt_theme_ncaa
1627
+
1628
+ df = pl.DataFrame(
1629
+ {
1630
+ "team": ["KC", "BUF", "BAL"],
1631
+ "espn_id": ["3139477", "3918298", "3916387"],
1632
+ "wins": [12, 10, 9],
1633
+ }
1634
+ )
1635
+
1636
+ gt_theme_ncaa(GT(df))
1637
+
1638
+ See Also:
1639
+ Ported from sdvplotR ``gt_theme_ncaa()``: https://sdvplotR.sportsdataverse.org/reference/gt_theme_ncaa.html
1640
+ """
1641
+ _density(density)
1642
+ gt, tid = _table_id(_check_gt(gt))
1643
+ sans, almarai = _font("Open Sans"), _font("Almarai")
1644
+ table = (
1645
+ gt.tab_style(_text(font=sans, size=px(14)), loc.body())
1646
+ .tab_style(
1647
+ [
1648
+ _text(font=sans, size=px(14), transform="uppercase", color="white", align="left"),
1649
+ important(style.fill(color="#000000")),
1650
+ ],
1651
+ loc.column_labels(),
1652
+ )
1653
+ .tab_style(_text(weight="bold", font=sans, size=px(14)), loc.row_groups())
1654
+ .tab_style(_text(font=sans, size=px(12)), loc.footnotes())
1655
+ .tab_style(_text(weight="bold", font=sans, size=px(18)), loc.title())
1656
+ .tab_style(_text(font=sans, size=px(14)), loc.subtitle())
1657
+ .tab_style(_text(font=sans, size=px(10)), loc.source_notes())
1658
+ .cols_align("left")
1659
+ .pipe(_on_spanners, _text(font=sans, weight=650, size=px(13)))
1660
+ .tab_style(
1661
+ [_text(font=sans, weight=650, size=px(14), color="#ffffff"), important(style.fill(color="#3C3A40"))],
1662
+ loc.row_groups(),
1663
+ )
1664
+ .tab_style(_text(font=almarai, size=px(12)), [loc.source_notes(), loc.footnotes()])
1665
+ .pipe(_hide_spanner_row, tid)
1666
+ .tab_options(
1667
+ data_row_padding=px(2),
1668
+ table_body_hlines_color="transparent",
1669
+ column_labels_border_top_color="black",
1670
+ column_labels_border_top_width=px(1),
1671
+ column_labels_border_bottom_style="none",
1672
+ row_group_border_top_style="none",
1673
+ row_group_border_top_color="black",
1674
+ row_group_border_bottom_width=px(1),
1675
+ row_group_border_bottom_color="black",
1676
+ row_group_border_bottom_style="solid",
1677
+ row_group_padding=px(1.5),
1678
+ heading_align="left",
1679
+ heading_border_bottom_style="none",
1680
+ table_body_border_top_style="none",
1681
+ table_body_border_bottom_color="white",
1682
+ table_border_bottom_style="none",
1683
+ table_border_top_style="none",
1684
+ source_notes_border_lr_style="none",
1685
+ )
1686
+ .opt_row_striping()
1687
+ .opt_css(
1688
+ "\n".join(
1689
+ [
1690
+ _last_row_border(tid, "#FFFFFF"),
1691
+ _css(tid, ".gt_col_heading", "padding: 5px 5px 5px 25px;"),
1692
+ _css(tid, ".gt_row", "padding: 5px 5px 5px 25px;"),
1693
+ _css(tid, ".gt_subtitle", "padding-top: 0px !important; padding-bottom: 4px !important;"),
1694
+ _css(tid, ".gt_heading", "padding-bottom: 0px; padding-top: 6px;"),
1695
+ _css(tid, ".gt_column_spanner", "text-decoration: underline;"),
1696
+ ]
1697
+ )
1698
+ )
1699
+ )
1700
+ return _scale_output(table, density).tab_options(**options)
1701
+
1702
+
1703
+ def gt_theme_pl(gt: GT, density: str = "comfortable", **options: Any) -> GT:
1704
+ """Premier League look: DM Sans in the league's deep purple, purple rules, a lilac row-group band.
1705
+
1706
+ Args:
1707
+ gt: The great_tables ``GT`` to theme.
1708
+ density: The type and padding scale: "comfortable" keeps the theme's sizes, "compact" scales them down and
1709
+ "social" up.
1710
+ **options: Passed to ``GT.tab_options`` last, so they override the theme.
1711
+
1712
+ Returns:
1713
+ GT: A new ``GT`` with the theme applied.
1714
+
1715
+ Raises:
1716
+ TypeError: ``gt`` is not a great_tables ``GT``.
1717
+ ValueError: ``density`` is not one of the three scales.
1718
+
1719
+ Example:
1720
+ ::
1721
+
1722
+ from great_tables import GT
1723
+ import polars as pl
1724
+ from sdvplot.great_tables import gt_theme_pl
1725
+
1726
+ df = pl.DataFrame(
1727
+ {
1728
+ "team": ["KC", "BUF", "BAL"],
1729
+ "espn_id": ["3139477", "3918298", "3916387"],
1730
+ "wins": [12, 10, 9],
1731
+ }
1732
+ )
1733
+
1734
+ gt_theme_pl(GT(df))
1735
+
1736
+ See Also:
1737
+ Ported from sdvplotR ``gt_theme_pl()``: https://sdvplotR.sportsdataverse.org/reference/gt_theme_pl.html
1738
+ """
1739
+ _density(density)
1740
+ gt, tid = _table_id(_check_gt(gt))
1741
+ n = _shape(gt)[1]
1742
+ dm = _font("DM Sans")
1743
+ purple = "#37003c"
1744
+ table = (
1745
+ gt.tab_style(_text(font=dm, color=purple, size=px(14)), loc.body())
1746
+ .tab_style(_text(font=dm, color="#87668a", weight=650, size=px(13)), loc.column_labels())
1747
+ .tab_style(_text(font=dm, weight=650), loc.title())
1748
+ .tab_style(_text(font=dm, weight=500), loc.subtitle())
1749
+ .pipe(_on_spanners, _text(font=dm, weight=650, size=px(12), color=purple))
1750
+ .tab_style(
1751
+ [_text(font=dm, weight=650, size=px(12), color="#ffffff"), important(style.fill(color="#C0BACA"))],
1752
+ loc.row_groups(),
1753
+ )
1754
+ .tab_style(_text(font=dm, size=px(12)), [loc.footnotes(), loc.source_notes()])
1755
+ .pipe(_row_rules, purple)
1756
+ )
1757
+ if n:
1758
+ table = table.tab_style(important(style.borders(sides="top", color=purple)), loc.body(rows=[0]))
1759
+ table = table.tab_options(
1760
+ heading_align="left",
1761
+ column_labels_border_top_style="none",
1762
+ table_border_top_style="none",
1763
+ table_body_border_top_style="solid",
1764
+ table_body_border_top_width=px(1),
1765
+ table_body_border_top_color=purple,
1766
+ table_body_border_bottom_color="white",
1767
+ heading_border_bottom_style="none",
1768
+ data_row_padding=px(2),
1769
+ row_group_padding=px(1.5),
1770
+ row_group_border_top_style="none",
1771
+ row_group_border_bottom_width=px(1),
1772
+ row_group_border_bottom_color=purple,
1773
+ row_group_border_bottom_style="solid",
1774
+ table_border_bottom_style="none",
1775
+ source_notes_border_lr_style="none",
1776
+ column_labels_border_bottom_style="solid",
1777
+ column_labels_border_bottom_width=px(1),
1778
+ column_labels_border_bottom_color=purple,
1779
+ ).opt_css(
1780
+ "\n".join(
1781
+ [
1782
+ _last_row_border(tid, "#FFFFFF"),
1783
+ _css(tid, ".gt_col_heading", "padding-bottom: 3px;"),
1784
+ _css(tid, ".gt_heading", "padding-bottom: 0px; padding-top: 6px;"),
1785
+ _css(tid, ".gt_subtitle", "padding-top: 2px; padding-bottom: 6px;"),
1786
+ _css(tid, ".gt_column_spanner", "font-size: 13px; font-weight: bold; padding-bottom: 2px;"),
1787
+ _css(tid, ".gt_sourcenote", "line-height: 1.2;"),
1788
+ ]
1789
+ )
1790
+ )
1791
+ return _scale_output(table, density).tab_options(**options)
1792
+
1793
+
1794
+ def gt_theme_savant(gt: GT, density: str = "comfortable", **options: Any) -> GT:
1795
+ """Baseball Savant's table look: Roboto Condensed, striped rows, a black row-group band, a centered heading.
1796
+
1797
+ Args:
1798
+ gt: The great_tables ``GT`` to theme.
1799
+ density: The type and padding scale: "comfortable" keeps the theme's sizes, "compact" scales them down and
1800
+ "social" up.
1801
+ **options: Passed to ``GT.tab_options`` last, so they override the theme.
1802
+
1803
+ Returns:
1804
+ GT: A new ``GT`` with the theme applied.
1805
+
1806
+ Raises:
1807
+ TypeError: ``gt`` is not a great_tables ``GT``.
1808
+ ValueError: ``density`` is not one of the three scales.
1809
+
1810
+ Example:
1811
+ ::
1812
+
1813
+ from great_tables import GT
1814
+ import polars as pl
1815
+ from sdvplot.great_tables import gt_theme_savant
1816
+
1817
+ df = pl.DataFrame(
1818
+ {
1819
+ "team": ["KC", "BUF", "BAL"],
1820
+ "espn_id": ["3139477", "3918298", "3916387"],
1821
+ "wins": [12, 10, 9],
1822
+ }
1823
+ )
1824
+
1825
+ gt_theme_savant(GT(df))
1826
+
1827
+ See Also:
1828
+ Ported from sdvplotR ``gt_theme_savant()``: https://sdvplotR.sportsdataverse.org/reference/gt_theme_savant.html
1829
+ """
1830
+ _density(density)
1831
+ gt, tid = _table_id(_check_gt(gt))
1832
+ roboto = _font("Roboto Condensed")
1833
+ table = (
1834
+ gt.tab_style(_text(font=roboto, size=px(14)), loc.body())
1835
+ .tab_style(_text(weight="bold", font=roboto, size=px(14)), loc.column_labels())
1836
+ .tab_style(
1837
+ [_text(font=roboto, weight=650, size=px(14), color="#FFFDF5"), important(style.fill(color="#000000"))],
1838
+ loc.row_groups(),
1839
+ )
1840
+ .tab_style(_text(font=roboto, size=px(12)), [loc.footnotes(), loc.source_notes()])
1841
+ .tab_style(_text(weight="bold", font=roboto, size=px(18)), loc.title())
1842
+ .tab_style(_text(font=roboto, size=px(14)), loc.subtitle())
1843
+ .pipe(_on_spanners, _text(font=roboto, weight=650, size=px(8)))
1844
+ .tab_options(
1845
+ data_row_padding=px(1),
1846
+ table_body_hlines_color="transparent",
1847
+ column_labels_border_top_color="black",
1848
+ column_labels_border_top_width=px(1),
1849
+ column_labels_border_bottom_style="none",
1850
+ row_group_border_top_style="none",
1851
+ row_group_border_top_color="black",
1852
+ row_group_border_bottom_width=px(1),
1853
+ row_group_border_bottom_color="black",
1854
+ row_group_border_bottom_style="solid",
1855
+ row_group_padding=px(1.5),
1856
+ heading_align="center",
1857
+ heading_border_bottom_style="none",
1858
+ table_body_border_top_style="none",
1859
+ table_body_border_bottom_color="white",
1860
+ table_border_bottom_style="none",
1861
+ table_border_top_style="none",
1862
+ source_notes_border_lr_style="none",
1863
+ )
1864
+ .opt_row_striping()
1865
+ .opt_css(
1866
+ "\n".join(
1867
+ [
1868
+ _last_row_border(tid, "#FFFFFF"),
1869
+ _css(tid, ".gt_col_heading", "padding-bottom: 2px; padding-top: 2px;"),
1870
+ _css(tid, ".gt_subtitle", "padding-top: 0px !important; padding-bottom: 4px !important;"),
1871
+ _css(tid, ".gt_heading", "padding-bottom: 0px; padding-top: 6px;"),
1872
+ _css(tid, ".gt_column_spanner", "font-size: 12px; font-weight: bold; text-decoration: underline;"),
1873
+ ]
1874
+ )
1875
+ )
1876
+ )
1877
+ return _scale_output(table, density).tab_options(**options)
1878
+
1879
+
1880
+ def gt_theme_sofa(gt: GT, style: str = "light", density: str = "comfortable", **options: Any) -> GT:
1881
+ """SofaScore's table look: Sofia Sans Condensed on a warm cream (or dark navy) ground, no rules between rows.
1882
+
1883
+ Args:
1884
+ gt: The great_tables ``GT`` to theme.
1885
+ style: "light" for the cream ground or "dark" for the navy one; on navy great_tables switches the text to
1886
+ white.
1887
+ density: The type and padding scale: "comfortable" keeps the theme's sizes, "compact" scales them down and
1888
+ "social" up.
1889
+ **options: Passed to ``GT.tab_options`` last, so they override the theme.
1890
+
1891
+ Returns:
1892
+ GT: A new ``GT`` with the theme applied.
1893
+
1894
+ Raises:
1895
+ TypeError: ``gt`` is not a great_tables ``GT``.
1896
+ ValueError: ``style`` is not "light" or "dark", or ``density`` is not one of the three scales.
1897
+
1898
+ Example:
1899
+ ::
1900
+
1901
+ from great_tables import GT
1902
+ import polars as pl
1903
+ from sdvplot.great_tables import gt_theme_sofa
1904
+
1905
+ df = pl.DataFrame(
1906
+ {
1907
+ "team": ["KC", "BUF", "BAL"],
1908
+ "espn_id": ["3139477", "3918298", "3916387"],
1909
+ "wins": [12, 10, 9],
1910
+ }
1911
+ )
1912
+
1913
+ gt_theme_sofa(GT(df), style="dark")
1914
+
1915
+ See Also:
1916
+ Ported from sdvplotR ``gt_theme_sofa()``: https://sdvplotR.sportsdataverse.org/reference/gt_theme_sofa.html
1917
+ """
1918
+ _density(density)
1919
+ if style not in ("light", "dark"):
1920
+ raise ValueError(f"style must be 'light' or 'dark', not {style!r}")
1921
+ base = "#F0EAD6" if style == "light" else "#1c2632"
1922
+ gt, tid = _table_id(_check_gt(gt))
1923
+ sofia = _font("Sofia Sans Condensed")
1924
+ table = (
1925
+ gt.tab_style(_text(font=sofia, size=px(14)), loc.body())
1926
+ .tab_style(_text(weight="bold", font=sofia, size=px(14)), [loc.column_labels(), loc.row_groups()])
1927
+ .tab_style(_text(font=sofia, size=px(12)), loc.footnotes())
1928
+ .tab_style(_text(weight="bold", font=sofia, size=px(22)), loc.title())
1929
+ .tab_style(_text(font=sofia, size=px(14)), loc.subtitle())
1930
+ .pipe(_on_spanners, _text(font=sofia, weight=650, size=px(12)))
1931
+ .tab_style(_text(font=sofia, size=px(10)), loc.source_notes())
1932
+ .tab_options(
1933
+ data_row_padding=px(1),
1934
+ table_body_hlines_color="transparent",
1935
+ column_labels_border_top_style="none",
1936
+ column_labels_border_bottom_style="none",
1937
+ row_group_border_top_style="none",
1938
+ row_group_border_top_color="black",
1939
+ row_group_border_bottom_width=px(1),
1940
+ row_group_border_bottom_color="black",
1941
+ row_group_border_bottom_style="solid",
1942
+ row_group_padding=px(1.5),
1943
+ heading_align="left",
1944
+ heading_border_bottom_style="none",
1945
+ table_body_border_top_style="none",
1946
+ table_border_bottom_style="none",
1947
+ table_border_top_style="none",
1948
+ source_notes_border_lr_style="none",
1949
+ table_background_color=base,
1950
+ table_border_top_color=base,
1951
+ table_border_right_color=base,
1952
+ table_border_bottom_color=base,
1953
+ table_border_left_color=base,
1954
+ )
1955
+ .opt_css(
1956
+ "\n".join(
1957
+ [
1958
+ _last_row_border(tid, base),
1959
+ _css(tid, ".gt_col_heading", "padding-bottom: 2px; padding-top: 2px;"),
1960
+ _css(tid, ".gt_subtitle", "padding-top: 0px !important; padding-bottom: 4px !important;"),
1961
+ _css(tid, ".gt_sourcenote", f"border-bottom-color: {base} !important;"),
1962
+ _css(tid, ".gt_heading", "padding-bottom: 0px; padding-top: 6px;"),
1963
+ _css(tid, ".gt_column_spanner", "font-size: 12px; font-weight: bold; text-decoration: underline;"),
1964
+ ]
1965
+ )
1966
+ )
1967
+ )
1968
+ return _scale_output(table, density).tab_options(**options)
1969
+
1970
+
1971
+ def gt_theme_tier(gt: GT, style: str = "dark", density: str = "comfortable", **options: Any) -> GT:
1972
+ """Tier-list look: Oswald on a near-black (or white) ground, centered columns, a rule under every row.
1973
+
1974
+ Args:
1975
+ gt: The great_tables ``GT`` to theme.
1976
+ style: "dark" for the near-black ground (great_tables switches the text to white) or "light" for white.
1977
+ density: The type and padding scale: "comfortable" keeps the theme's sizes, "compact" scales them down and
1978
+ "social" up.
1979
+ **options: Passed to ``GT.tab_options`` last, so they override the theme.
1980
+
1981
+ Returns:
1982
+ GT: A new ``GT`` with the theme applied.
1983
+
1984
+ Raises:
1985
+ TypeError: ``gt`` is not a great_tables ``GT``.
1986
+ ValueError: ``style`` is not "light" or "dark", or ``density`` is not one of the three scales.
1987
+
1988
+ Example:
1989
+ ::
1990
+
1991
+ from great_tables import GT
1992
+ import polars as pl
1993
+ from sdvplot.great_tables import gt_theme_tier
1994
+
1995
+ df = pl.DataFrame(
1996
+ {
1997
+ "team": ["KC", "BUF", "BAL"],
1998
+ "espn_id": ["3139477", "3918298", "3916387"],
1999
+ "wins": [12, 10, 9],
2000
+ }
2001
+ )
2002
+
2003
+ gt_theme_tier(GT(df), style="light")
2004
+
2005
+ See Also:
2006
+ Ported from sdvplotR ``gt_theme_tier()``: https://sdvplotR.sportsdataverse.org/reference/gt_theme_tier.html
2007
+ """
2008
+ _density(density)
2009
+ if style not in ("light", "dark"):
2010
+ raise ValueError(f"style must be 'light' or 'dark', not {style!r}")
2011
+ base = "#1a1a17" if style == "dark" else "#ffffff"
2012
+ gt, tid = _table_id(_check_gt(gt))
2013
+ oswald = _font("Oswald")
2014
+ table = (
2015
+ _table_font(gt, _font("Oswald"), weight=500)
2016
+ .tab_style(_text(font=oswald, weight=650), loc.title())
2017
+ .tab_style(_text(font=oswald, weight=500), loc.subtitle())
2018
+ .pipe(_row_rules, "black")
2019
+ .cols_align("center")
2020
+ .tab_options(
2021
+ data_row_padding=px(1),
2022
+ table_body_hlines_color="transparent",
2023
+ column_labels_border_top_style="none",
2024
+ column_labels_border_bottom_style="none",
2025
+ row_group_border_top_style="none",
2026
+ row_group_border_top_color="black",
2027
+ row_group_border_bottom_width=px(1),
2028
+ row_group_border_bottom_color="black",
2029
+ row_group_border_bottom_style="solid",
2030
+ row_group_padding=px(1.5),
2031
+ heading_align="left",
2032
+ heading_border_bottom_style="none",
2033
+ table_body_border_top_style="none",
2034
+ table_border_bottom_style="none",
2035
+ table_border_top_style="none",
2036
+ source_notes_border_lr_style="none",
2037
+ table_background_color=base,
2038
+ table_border_top_color=base,
2039
+ table_border_right_color=base,
2040
+ table_border_bottom_color=base,
2041
+ table_border_left_color=base,
2042
+ )
2043
+ .opt_css(
2044
+ "\n".join(
2045
+ [
2046
+ _last_row_border(tid, base),
2047
+ _css(tid, ".gt_col_heading", "padding-bottom: 2px; padding-top: 2px;"),
2048
+ _css(tid, ".gt_subtitle", "padding-top: 0px !important; padding-bottom: 4px !important;"),
2049
+ _css(tid, ".gt_sourcenote", f"border-bottom-color: {base} !important;"),
2050
+ _css(tid, ".gt_heading", "padding-bottom: 0px; padding-top: 6px;"),
2051
+ ]
2052
+ )
2053
+ )
2054
+ )
2055
+ return _scale_output(table, density).tab_options(**options)
2056
+
2057
+
2058
+ def gt_theme_preview(
2059
+ data: Any, themes: str | list[str] | None = None, *, n: int = 5, density: str | None = "compact"
2060
+ ) -> dict[str, GT]:
2061
+ """The same few rows in every table theme, one ``GT`` per theme, to compare them side by side.
2062
+
2063
+ Each theme is called at its defaults, except ``density``. ``gt_theme_sdv_team`` is shown with ``league="nfl"``
2064
+ and no team, i.e. the SportsDataverse colors, as R shows it.
2065
+
2066
+ Args:
2067
+ data: A pandas or polars DataFrame, or a ``GT`` (its data is used).
2068
+ themes: Theme function names (e.g. "gt_theme_kenpom"); None shows every ``gt_theme_*`` in
2069
+ ``sdvplot.great_tables``, sorted.
2070
+ n: How many rows of ``data`` each table shows: a positive whole number (numpy integers count).
2071
+ density: The density passed to every theme that takes one, so the tables compare; None leaves each theme at
2072
+ its own default.
2073
+
2074
+ Returns:
2075
+ dict[str, GT]: ``{theme name: themed GT}``, in the order of ``themes``.
2076
+
2077
+ Raises:
2078
+ TypeError: ``data`` is not a DataFrame or a ``GT``.
2079
+ ValueError: ``data`` has no rows, a name in ``themes`` is not a theme, ``n`` is not a positive whole number,
2080
+ or ``density`` is not a scale.
2081
+
2082
+ Example:
2083
+ ::
2084
+
2085
+ from sdvplot.great_tables import gt_theme_preview
2086
+ import polars as pl
2087
+ from sdvplot.great_tables import gt_theme_kenpom, gt_theme_athletic
2088
+
2089
+ df = pl.DataFrame(
2090
+ {
2091
+ "team": ["KC", "BUF", "BAL"],
2092
+ "espn_id": ["3139477", "3918298", "3916387"],
2093
+ "wins": [12, 10, 9],
2094
+ }
2095
+ )
2096
+
2097
+ tables = gt_theme_preview(df, themes=["gt_theme_kenpom", "gt_theme_athletic"])
2098
+ tables["gt_theme_kenpom"]
2099
+
2100
+ See Also:
2101
+ Ported from sdvplotR ``gt_theme_preview()``, which lays the tables out with ``gt_grid()``:
2102
+ https://sdvplotR.sportsdataverse.org/reference/gt_theme_preview.html
2103
+ """
2104
+ import sdvplot.great_tables as sgt # the package imports this module, so import it at call time
2105
+
2106
+ if isinstance(data, GT):
2107
+ data = data._tbl_data
2108
+ try:
2109
+ frame = nw.from_native(data, eager_only=True)
2110
+ except TypeError:
2111
+ raise TypeError(f"data must be a pandas or polars DataFrame or a GT, not {type(data).__name__}") from None
2112
+ if len(frame) == 0:
2113
+ raise ValueError("data has no rows")
2114
+ if isinstance(n, bool) or not isinstance(n, numbers.Integral) or n < 1:
2115
+ raise ValueError(f"n must be a positive whole number of rows, got {n!r}")
2116
+ if density is not None:
2117
+ _density(density)
2118
+ available = sorted(name for name in sgt.__all__ if name.startswith("gt_theme_") and name != "gt_theme_preview")
2119
+ names = available if themes is None else [themes] if isinstance(themes, str) else list(themes)
2120
+ missing = [name for name in names if name not in available]
2121
+ if missing:
2122
+ raise ValueError(f"No such theme: {', '.join(missing)}. Themes: {', '.join(available)}")
2123
+ rows = frame.head(int(n)).to_native()
2124
+ out = {}
2125
+ for name in names:
2126
+ fn = getattr(sgt, name)
2127
+ params = inspect.signature(fn).parameters
2128
+ kwargs: dict[str, Any] = {}
2129
+ if density is not None and "density" in params:
2130
+ kwargs["density"] = density
2131
+ if "league" in params and params["league"].default is inspect.Parameter.empty:
2132
+ kwargs["league"] = "nfl"
2133
+ out[name] = fn(GT(rows), **kwargs)
2134
+ return out