sdvplot 0.1.0__tar.gz

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 (161) hide show
  1. sdvplot-0.1.0/.gitattributes +44 -0
  2. sdvplot-0.1.0/.gitignore +18 -0
  3. sdvplot-0.1.0/CHANGELOG.md +561 -0
  4. sdvplot-0.1.0/CITATION.cff +18 -0
  5. sdvplot-0.1.0/CODE_OF_CONDUCT.md +83 -0
  6. sdvplot-0.1.0/CONTRIBUTING.md +237 -0
  7. sdvplot-0.1.0/LICENSE +21 -0
  8. sdvplot-0.1.0/PKG-INFO +188 -0
  9. sdvplot-0.1.0/README.md +107 -0
  10. sdvplot-0.1.0/SECURITY.md +34 -0
  11. sdvplot-0.1.0/pyproject.toml +171 -0
  12. sdvplot-0.1.0/src/sdvplot/__init__.py +68 -0
  13. sdvplot-0.1.0/src/sdvplot/_cache.py +484 -0
  14. sdvplot-0.1.0/src/sdvplot/_colors.py +206 -0
  15. sdvplot-0.1.0/src/sdvplot/_contrast.py +60 -0
  16. sdvplot-0.1.0/src/sdvplot/_court.py +110 -0
  17. sdvplot-0.1.0/src/sdvplot/_deprecate.py +39 -0
  18. sdvplot-0.1.0/src/sdvplot/_dispatch.py +342 -0
  19. sdvplot-0.1.0/src/sdvplot/_errors.py +93 -0
  20. sdvplot-0.1.0/src/sdvplot/_headshots.py +131 -0
  21. sdvplot-0.1.0/src/sdvplot/_images.py +292 -0
  22. sdvplot-0.1.0/src/sdvplot/_index.py +178 -0
  23. sdvplot-0.1.0/src/sdvplot/_lazy.py +15 -0
  24. sdvplot-0.1.0/src/sdvplot/_manifest.py +73 -0
  25. sdvplot-0.1.0/src/sdvplot/_marks.py +296 -0
  26. sdvplot-0.1.0/src/sdvplot/_normalize.py +82 -0
  27. sdvplot-0.1.0/src/sdvplot/_placement.py +168 -0
  28. sdvplot-0.1.0/src/sdvplot/_resolve.py +363 -0
  29. sdvplot-0.1.0/src/sdvplot/_surface.py +220 -0
  30. sdvplot-0.1.0/src/sdvplot/_tables.py +87 -0
  31. sdvplot-0.1.0/src/sdvplot/_tiers.py +135 -0
  32. sdvplot-0.1.0/src/sdvplot/_types.py +38 -0
  33. sdvplot-0.1.0/src/sdvplot/_versions.py +30 -0
  34. sdvplot-0.1.0/src/sdvplot/_web.py +61 -0
  35. sdvplot-0.1.0/src/sdvplot/altair.py +652 -0
  36. sdvplot-0.1.0/src/sdvplot/bokeh.py +323 -0
  37. sdvplot-0.1.0/src/sdvplot/data/INDEX_VERSION +1 -0
  38. sdvplot-0.1.0/src/sdvplot/data/aliases.parquet +0 -0
  39. sdvplot-0.1.0/src/sdvplot/data/teams.parquet +0 -0
  40. sdvplot-0.1.0/src/sdvplot/folium.py +316 -0
  41. sdvplot-0.1.0/src/sdvplot/great_tables/__init__.py +267 -0
  42. sdvplot-0.1.0/src/sdvplot/great_tables/_cells.py +1814 -0
  43. sdvplot-0.1.0/src/sdvplot/great_tables/_export.py +844 -0
  44. sdvplot-0.1.0/src/sdvplot/great_tables/_layout.py +1802 -0
  45. sdvplot-0.1.0/src/sdvplot/great_tables/_marks.py +798 -0
  46. sdvplot-0.1.0/src/sdvplot/great_tables/_themes.py +2134 -0
  47. sdvplot-0.1.0/src/sdvplot/holoviews.py +303 -0
  48. sdvplot-0.1.0/src/sdvplot/matplotlib.py +934 -0
  49. sdvplot-0.1.0/src/sdvplot/plotly.py +595 -0
  50. sdvplot-0.1.0/src/sdvplot/plotnine.py +1104 -0
  51. sdvplot-0.1.0/src/sdvplot/plottable.py +141 -0
  52. sdvplot-0.1.0/src/sdvplot/py.typed +0 -0
  53. sdvplot-0.1.0/src/sdvplot/pygal.py +416 -0
  54. sdvplot-0.1.0/src/sdvplot/reactable.py +488 -0
  55. sdvplot-0.1.0/src/sdvplot/testing.py +579 -0
  56. sdvplot-0.1.0/src/sdvplot/typing.py +21 -0
  57. sdvplot-0.1.0/tests/__init__.py +0 -0
  58. sdvplot-0.1.0/tests/baseline/test_baseline_add_images.png +0 -0
  59. sdvplot-0.1.0/tests/baseline/test_baseline_court_coords_half_court.png +0 -0
  60. sdvplot-0.1.0/tests/baseline/test_baseline_plotnine_facets.png +0 -0
  61. sdvplot-0.1.0/tests/baseline/test_baseline_plotnine_mean_and_median_lines.png +0 -0
  62. sdvplot-0.1.0/tests/baseline/test_baseline_plotnine_team_tiers.png +0 -0
  63. sdvplot-0.1.0/tests/baseline/test_baseline_plotnine_team_tiers_light.png +0 -0
  64. sdvplot-0.1.0/tests/baseline/test_baseline_plotnine_title_image.png +0 -0
  65. sdvplot-0.1.0/tests/baseline/test_baseline_scatter_with_logos.png +0 -0
  66. sdvplot-0.1.0/tests/baseline/test_baseline_team_tiers.png +0 -0
  67. sdvplot-0.1.0/tests/baseline/test_baseline_team_tiers_light.png +0 -0
  68. sdvplot-0.1.0/tests/baseline/test_baseline_title_image.png +0 -0
  69. sdvplot-0.1.0/tests/baseline/test_baseline_x_axis_logos.png +0 -0
  70. sdvplot-0.1.0/tests/baseline/test_baseline_y_axis_logos.png +0 -0
  71. sdvplot-0.1.0/tests/conftest.py +410 -0
  72. sdvplot-0.1.0/tests/fixtures/README.md +19 -0
  73. sdvplot-0.1.0/tests/fixtures/ggpath_ref_lines.csv +15 -0
  74. sdvplot-0.1.0/tests/fixtures/marks.csv +16 -0
  75. sdvplot-0.1.0/tests/fixtures/nba_shotchartdetail_2023.csv +41 -0
  76. sdvplot-0.1.0/tests/fixtures/sdvplotr_court_coords.csv +41 -0
  77. sdvplot-0.1.0/tests/fixtures/sdvplotr_logo_history.csv +402 -0
  78. sdvplot-0.1.0/tests/gt_export_fakes.py +46 -0
  79. sdvplot-0.1.0/tests/gt_frames.py +14 -0
  80. sdvplot-0.1.0/tests/gt_html.py +25 -0
  81. sdvplot-0.1.0/tests/test_adapters_live.py +38 -0
  82. sdvplot-0.1.0/tests/test_altair.py +232 -0
  83. sdvplot-0.1.0/tests/test_api.py +404 -0
  84. sdvplot-0.1.0/tests/test_automation_example.py +926 -0
  85. sdvplot-0.1.0/tests/test_bokeh.py +86 -0
  86. sdvplot-0.1.0/tests/test_build_index.py +641 -0
  87. sdvplot-0.1.0/tests/test_cache.py +768 -0
  88. sdvplot-0.1.0/tests/test_colors.py +56 -0
  89. sdvplot-0.1.0/tests/test_commit_msg_hook.py +60 -0
  90. sdvplot-0.1.0/tests/test_compat_cartopy.py +83 -0
  91. sdvplot-0.1.0/tests/test_compat_charts.py +100 -0
  92. sdvplot-0.1.0/tests/test_compat_geo.py +49 -0
  93. sdvplot-0.1.0/tests/test_compat_matrix.py +18 -0
  94. sdvplot-0.1.0/tests/test_compat_style.py +68 -0
  95. sdvplot-0.1.0/tests/test_compat_text.py +68 -0
  96. sdvplot-0.1.0/tests/test_contrast.py +67 -0
  97. sdvplot-0.1.0/tests/test_court.py +214 -0
  98. sdvplot-0.1.0/tests/test_deprecate.py +60 -0
  99. sdvplot-0.1.0/tests/test_dispatch.py +557 -0
  100. sdvplot-0.1.0/tests/test_fetch_sources.py +438 -0
  101. sdvplot-0.1.0/tests/test_folium.py +75 -0
  102. sdvplot-0.1.0/tests/test_gen_docs.py +411 -0
  103. sdvplot-0.1.0/tests/test_great_tables.py +438 -0
  104. sdvplot-0.1.0/tests/test_gt_cells_borders.py +238 -0
  105. sdvplot-0.1.0/tests/test_gt_cells_colors.py +240 -0
  106. sdvplot-0.1.0/tests/test_gt_cells_format.py +180 -0
  107. sdvplot-0.1.0/tests/test_gt_cells_rows.py +200 -0
  108. sdvplot-0.1.0/tests/test_gt_export_batch.py +124 -0
  109. sdvplot-0.1.0/tests/test_gt_export_compose.py +152 -0
  110. sdvplot-0.1.0/tests/test_gt_export_pixels.py +99 -0
  111. sdvplot-0.1.0/tests/test_gt_export_render.py +214 -0
  112. sdvplot-0.1.0/tests/test_gt_export_save.py +87 -0
  113. sdvplot-0.1.0/tests/test_gt_layout_exports.py +30 -0
  114. sdvplot-0.1.0/tests/test_gt_layout_notes.py +97 -0
  115. sdvplot-0.1.0/tests/test_gt_layout_reshape.py +121 -0
  116. sdvplot-0.1.0/tests/test_gt_layout_rows.py +162 -0
  117. sdvplot-0.1.0/tests/test_gt_layout_scales.py +215 -0
  118. sdvplot-0.1.0/tests/test_gt_layout_titles.py +115 -0
  119. sdvplot-0.1.0/tests/test_gt_repr_important.py +158 -0
  120. sdvplot-0.1.0/tests/test_gt_themes.py +583 -0
  121. sdvplot-0.1.0/tests/test_harness.py +17 -0
  122. sdvplot-0.1.0/tests/test_headshots.py +127 -0
  123. sdvplot-0.1.0/tests/test_holoviews.py +73 -0
  124. sdvplot-0.1.0/tests/test_home_figures.py +104 -0
  125. sdvplot-0.1.0/tests/test_image_loaders.py +114 -0
  126. sdvplot-0.1.0/tests/test_images.py +312 -0
  127. sdvplot-0.1.0/tests/test_images_baseline.py +187 -0
  128. sdvplot-0.1.0/tests/test_import_time.py +16 -0
  129. sdvplot-0.1.0/tests/test_index.py +44 -0
  130. sdvplot-0.1.0/tests/test_input_validation.py +260 -0
  131. sdvplot-0.1.0/tests/test_manifest.py +66 -0
  132. sdvplot-0.1.0/tests/test_marks.py +167 -0
  133. sdvplot-0.1.0/tests/test_matplotlib.py +345 -0
  134. sdvplot-0.1.0/tests/test_normalize.py +16 -0
  135. sdvplot-0.1.0/tests/test_notebooks.py +698 -0
  136. sdvplot-0.1.0/tests/test_package.py +14 -0
  137. sdvplot-0.1.0/tests/test_placement.py +119 -0
  138. sdvplot-0.1.0/tests/test_plotly.py +248 -0
  139. sdvplot-0.1.0/tests/test_plotnine.py +366 -0
  140. sdvplot-0.1.0/tests/test_plottable.py +37 -0
  141. sdvplot-0.1.0/tests/test_pygal.py +271 -0
  142. sdvplot-0.1.0/tests/test_reactable.py +154 -0
  143. sdvplot-0.1.0/tests/test_readme.py +29 -0
  144. sdvplot-0.1.0/tests/test_real_index.py +384 -0
  145. sdvplot-0.1.0/tests/test_repo_files.py +184 -0
  146. sdvplot-0.1.0/tests/test_resolve.py +123 -0
  147. sdvplot-0.1.0/tests/test_resolve_series.py +70 -0
  148. sdvplot-0.1.0/tests/test_sdvplotr_parity.py +154 -0
  149. sdvplot-0.1.0/tests/test_submodule_examples.py +327 -0
  150. sdvplot-0.1.0/tests/test_suggest.py +15 -0
  151. sdvplot-0.1.0/tests/test_surface.py +176 -0
  152. sdvplot-0.1.0/tests/test_table_contract.py +106 -0
  153. sdvplot-0.1.0/tests/test_tables.py +59 -0
  154. sdvplot-0.1.0/tests/test_team_tiers.py +346 -0
  155. sdvplot-0.1.0/tests/test_title_image.py +276 -0
  156. sdvplot-0.1.0/tests/test_types.py +89 -0
  157. sdvplot-0.1.0/tests/test_url_safety.py +220 -0
  158. sdvplot-0.1.0/tests/test_warning_order.py +44 -0
  159. sdvplot-0.1.0/tests/test_web.py +62 -0
  160. sdvplot-0.1.0/tests/test_web_live.py +30 -0
  161. sdvplot-0.1.0/tests/test_web_render.py +194 -0
@@ -0,0 +1,44 @@
1
+ # Line endings: store LF in the repo AND check out LF on every platform.
2
+ #
3
+ # With only `* text=auto` (and core.autocrlf=false / native core.eol), a Windows
4
+ # checkout rewrites the LF that the tooling emits (codegen, ruff, Python file
5
+ # writes, doctoc, the CHANGELOG sync) back to CRLF, so every `git add` warns
6
+ # "LF will be replaced by CRLF the next time Git touches it". Pinning eol=lf makes
7
+ # the working tree match the (already all-LF) index on all platforms and ends the
8
+ # churn. No Windows-only scripts (.bat/.cmd) live here, so nothing needs CRLF.
9
+ * text=auto eol=lf
10
+
11
+ # Binary assets — never apply text/eol normalization (it would corrupt them).
12
+ *.png binary
13
+ *.ico binary
14
+ *.jpg binary
15
+ *.jpeg binary
16
+ *.gif binary
17
+ *.webp binary
18
+ *.ubj binary
19
+ *.parquet binary
20
+ *.pyc binary
21
+ *.woff binary
22
+ *.woff2 binary
23
+ *.ttf binary
24
+ *.eot binary
25
+ *.pdf binary
26
+ *.zip binary
27
+ *.gz binary
28
+
29
+ # Jupyter notebooks are JSON — keep them explicit LF text so the binary rules
30
+ # above never accidentally shadow them.
31
+ *.ipynb text eol=lf
32
+
33
+ # Generated lockfile: LF text, collapsed in review diffs (machine-written).
34
+ uv.lock text eol=lf linguist-generated=true
35
+
36
+ # sdvplot generated artifacts: LF text / binary, collapsed in review diffs.
37
+ src/sdvplot/data/teams.parquet binary linguist-generated=true
38
+ src/sdvplot/data/aliases.parquet binary linguist-generated=true
39
+ src/sdvplot/data/INDEX_VERSION text eol=lf linguist-generated=true
40
+ data-raw/*.csv text eol=lf linguist-generated=true
41
+ docs/yarn.lock text eol=lf linguist-generated=true
42
+ docs/docs/reference/** linguist-generated=true
43
+ docs/docs/tutorials/** linguist-generated=true
44
+ *.svg text eol=lf
@@ -0,0 +1,18 @@
1
+ __pycache__/
2
+ .venv/
3
+ dist/
4
+ *.egg-info/
5
+ .pytest_cache/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+ .env
9
+ docs/node_modules/
10
+ docs/build/
11
+ docs/.docusaurus/
12
+ .superpowers/
13
+ mpl-results/
14
+
15
+ # examples/automation/sdvplot_social.py output
16
+ /out/
17
+ .coverage
18
+ .coverage.*
@@ -0,0 +1,561 @@
1
+ <!-- START doctoc generated TOC please keep comment here to allow auto update -->
2
+ <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
3
+
4
+ - [Changelog](#changelog)
5
+ - [[Unreleased]](#unreleased)
6
+ - [[0.1.0] - 2026-10-05](#010---2026-10-05)
7
+ - [Migrating from the git pre-release](#migrating-from-the-git-pre-release)
8
+ - [Added](#added)
9
+ - [Core (team identity, colors, logos, cache, adapter contract)](#core-team-identity-colors-logos-cache-adapter-contract)
10
+ - [Repository standards](#repository-standards)
11
+ - [Matplotlib family](#matplotlib-family)
12
+ - [Tables, wave A (marks and team identity)](#tables-wave-a-marks-and-team-identity)
13
+ - [Table themes](#table-themes)
14
+ - [Tables wave C1 (cell styling and formatting)](#tables-wave-c1-cell-styling-and-formatting)
15
+ - [Tables, wave D (image export and composition)](#tables-wave-d-image-export-and-composition)
16
+ - [Web family](#web-family)
17
+ - [Long tail (pygal, Cartopy, gallery compatibility)](#long-tail-pygal-cartopy-gallery-compatibility)
18
+ - [Tables wave C2 (legends, layout and annotation)](#tables-wave-c2-legends-layout-and-annotation)
19
+ - [Parity extras (court coordinates, images by path, reference lines)](#parity-extras-court-coordinates-images-by-path-reference-lines)
20
+ - [Parity extras (title images, team tiers)](#parity-extras-title-images-team-tiers)
21
+ - [Release hardening](#release-hardening)
22
+ - [Changed](#changed)
23
+ - [Documentation](#documentation)
24
+ - [Documentation — example notebooks by section, interactive outputs and the gallery](#documentation--example-notebooks-by-section-interactive-outputs-and-the-gallery)
25
+ - [Release hardening](#release-hardening-1)
26
+ - [Fixed](#fixed)
27
+ - [Adapter contract follow-ups](#adapter-contract-follow-ups)
28
+ - [Tables follow-ups](#tables-follow-ups)
29
+ - [Content findings (team index, team tiers, surface)](#content-findings-team-index-team-tiers-surface)
30
+ - [Release hardening](#release-hardening-2)
31
+ - [Security](#security)
32
+
33
+ <!-- END doctoc generated TOC please keep comment here to allow auto update -->
34
+
35
+ # Changelog
36
+
37
+ All notable changes to sdvplot are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
38
+
39
+ ## [Unreleased]
40
+
41
+ ## [0.1.0] - 2026-10-05
42
+
43
+ First release.
44
+
45
+ ### Migrating from the git pre-release
46
+
47
+ 0.1.0 freezes the public API. Code written against a git install from before it needs these changes; each old call now
48
+ fails loudly, with the message shown:
49
+
50
+ - `palette("nfl", "secondary")` is `palette("nfl", which="secondary")`: `palette` is `palette(league, teams=None, *,
51
+ which="primary", season=None)`, as sdvplotR's `sdv_color_palette(sport, teams, type)`. The old call raises
52
+ `InputError: 'secondary' is a color slot, not a team; pass it by keyword: palette(league, teams=...,
53
+ which="secondary")`.
54
+ - `team_colors(teams, "nfl")` is `team_colors("nfl", teams)`: the league comes first, as in sdvplotR's
55
+ `sdv_team_colors(sport, team, type)`. With a list or Series of teams the old order raises `InputError: league must be
56
+ a league key such as 'nfl', got list; team_colors and palette take the league first: team_colors(league, teams)`;
57
+ with one team, `InputError: unknown league 'KC'; known leagues: [...]`.
58
+ - Secondary arguments are keyword-only: `season`, `id_system` and `strict` on `resolve`; `season`, `variant`,
59
+ `mark_type` and `size` on `logo_url` and `logo_image`; `season` on `marks`; `n` on `suggest`; `id_system` on
60
+ `headshot_url`; `x` and `y` on `court_coords`. `resolve(v, "nfl", 2020)` raises `TypeError: resolve() takes 2
61
+ positional arguments but 3 were given`; write `resolve(v, "nfl", season=2020)`.
62
+ - Each public submodule exports only its `__all__`, and `dir()` shows only that. Helpers and the adapter test hooks are
63
+ underscored (`_drawn_marks`, `_drawn_axis_marks`, `_visible_axis_labels`, `_SUPPORTS_AXIS_LOGOS`,
64
+ `_drawn_title_images`, great_tables' `_drawn_cells` and `_rendered_html`), so `sdvplot.matplotlib.draw_placements`
65
+ raises `AttributeError: module 'sdvplot.matplotlib' has no attribute 'draw_placements'`.
66
+ - A table's `height` is pixels: `gt_sdv_logos(gt, "team", league="nfl", height=0.1)` (and the front door on a `GT`)
67
+ raises `InputError: height is the image height in pixels for a table (such as 30), got 0.1; a fraction of the plot
68
+ height is the unit for plots, not tables` instead of drawing a 0.1 px image.
69
+ - New errors, each still the builtin you may already catch: `SdvplotError` is the base of them all; `InputError` (a
70
+ `ValueError`) is raised by the shared argument checks (league, `which`, `id_system`, `mark_type`, height, alpha);
71
+ `UnsupportedTargetError` (a `TypeError`) by an adapter given the wrong kind of object or asked for axis logos it
72
+ cannot draw; `UnsafeDownloadError` (an `OSError`) and `UnsafeCachePathError` (a `ValueError`) are exported.
73
+
74
+ ### Added
75
+
76
+ #### Core (team identity, colors, logos, cache, adapter contract)
77
+
78
+ - Team resolver and id systems: `resolve()` maps abbreviations, names and provider ids (ESPN, NHL, nflverse, MLB, NBA, HockeyTech, NCAA, PFF, Cricinfo, CFBD, Baseball-Reference, FanGraphs, sdvplotR) to a stable string `team_id`, with a documented `PRIORITY` order and `nhl_id` available only through an explicit `id_system`.
79
+ - `suggest()` for near-miss candidates when a value does not resolve.
80
+ - `palette()` and `team_colors()`: team colors as a `{team: "#hex"}` mapping or one hex per value, with `color_source="fallback"` marking placeholders.
81
+ - `logo_url()`, `logo_image()` and `marks()`, with era selection by season (relocated franchises get their era's mark), variant and source preference.
82
+ - `headshot_url()`: ESPN athlete ids for NFL, NBA, WNBA, MLB, NHL and college football and basketball, plus NFL gsis ids through the nflverse player table.
83
+ - A download cache (`SDVPLOT_CACHE_DIR`, `SDVPLOT_CACHE_TTL`) with `clear_cache()`.
84
+ - The adapter registry and contract harness (`add_logos`, `add_wordmarks`, `add_headshots`, `axis_logos`, `sdvplot.testing`).
85
+ - A bundled index of 5,879 teams across 28 leagues, rebuilt reproducibly from `data-raw/` by `tools/build_index.py`.
86
+ - sdvplotR parity: 99.8% of sdvplotR's `clean_team_abbrs()` keys resolve to the same team (4,232 of 4,241 checked).
87
+
88
+ #### Repository standards
89
+
90
+ - A Docusaurus docs site at <https://sdvplot.sportsdataverse.org> with a generated API reference and rendered tutorials.
91
+ - Example notebooks, executed by `tools/render_notebooks.py` into the tutorials.
92
+ - CI, a release workflow, and pre-commit hooks, in sdv-py's layout.
93
+ - `CONTRIBUTING.md`, `CLAUDE.md`, `.github/copilot-instructions.md`, issue and pull-request templates, and sdv-py's dotfiles.
94
+
95
+ #### Matplotlib family
96
+
97
+ - `add_logos`, `add_wordmarks`, `add_headshots` and `axis_logos` work on matplotlib Axes, one-Axes Figures and seaborn
98
+ grids (`sdvplot.matplotlib`); `height` is a fraction of the Axes height at any dpi or figure size.
99
+ - plotnine: `geom_sdv_logos`, `geom_sdv_wordmarks`, `geom_sdv_headshots`, `axis_logos` and the team color scales
100
+ `scale_color_sdv` / `scale_fill_sdv` (`sdvplot.plotnine`).
101
+ - `surface()`: sportypy playing surfaces in team colors, the port of sdvplotR's `sdv_surface()`.
102
+ - plottable `logo_column` and `headshot_column` (`sdvplot.plottable`, new `[plottable]` extra).
103
+ - The adapter contract (`sdvplot.testing`) now covers wordmarks, headshots, axis logos and alpha (rules 0-8).
104
+
105
+ #### Tables, wave A (marks and team identity)
106
+
107
+ - `sdvplot.great_tables`, ported from sdvplotR with `league=` for `sport=`:
108
+ - `gt_sdv_logos`, `gt_sdv_wordmarks` and `gt_sdv_headshots` put marks in body, stub or row-group cells, and
109
+ `gt_sdv_cols_label` puts them in column labels.
110
+ - `gt_merge_stack_team_color` stacks two columns in one cell, the bottom line in the team color.
111
+ - The themes `gt_theme_sdv` (light or dark, three densities) and `gt_theme_sdv_team`.
112
+ - Heights are pixels. Unknown values keep their text and warn once, when the function is called.
113
+ - `sdvplot.add_logos(gt, "team", league="nfl")`, `add_wordmarks` and `add_headshots` route a great_tables `GT` to
114
+ those functions.
115
+ - `sdvplot.reactable` (new `[reactable]` extra): `reactable_sdv_logos`, `reactable_sdv_wordmarks`,
116
+ `reactable_sdv_headshots`, `reactable_sdv_cols_label`, `reactable_sdv_team_color_bar` and
117
+ `reactable_sdv_team_color_bg`. Each returns `reactable.Column` objects.
118
+ - `sdvplot.testing.check_table_adapter_contract` (rules T0-T6) for table adapters.
119
+ - `docs/PARITY_TABLES.md`: how each sdvplotR table function maps to sdvplot.
120
+ - The `[tables]` extra now needs great_tables 1.0 or later.
121
+
122
+ #### Table themes
123
+
124
+ - The 18 sdvplotR table themes for great_tables, with sdvplotR's names, arguments and defaults
125
+ (`sdvplot.great_tables`): `gt_theme_almanac`, `gt_theme_athletic`, `gt_theme_booktabs`, `gt_theme_broadsheet`,
126
+ `gt_theme_brutalist`, `gt_theme_drench`, `gt_theme_gtutils`, `gt_theme_kenpom`, `gt_theme_midnight`, `gt_theme_ncaa`,
127
+ `gt_theme_pl`, `gt_theme_savant`, `gt_theme_scoreboard`, `gt_theme_sofa`, `gt_theme_swiss`, `gt_theme_terminal`,
128
+ `gt_theme_tier` and `gt_theme_tufte`, each with `density="comfortable" | "compact" | "social"`.
129
+ - `pal_midnight`: sdvplotR's five-color rank palette for dark grounds (every step clears 4.5:1 on midnight and terminal).
130
+ - `gt_theme_preview()`: the same rows in every theme, as `{theme name: GT}`.
131
+ - Theme fonts load every weight from Google Fonts, over gt's fallback stack.
132
+
133
+ #### Tables wave C1 (cell styling and formatting)
134
+
135
+ - 17 sdvplotR cell helpers in `sdvplot.great_tables`, with R's names and arguments: `gt_538_caption`,
136
+ `gt_bold_rows`, `gt_border_bars_bottom`, `gt_border_bars_top`, `gt_border_grid`, `gt_color_pills`,
137
+ `gt_color_ranks`, `gt_color_results`, `gt_column_subheaders`, `gt_cutline`, `gt_delta`, `gt_fmt_rank`,
138
+ `gt_fmt_tally`, `gt_group_stripes`, `gt_highlight_cells`, `gt_highlight_na` and `gt_indicator_boxes`. They take
139
+ tables built from pandas or polars data.
140
+ - `gt_color_pills` and `gt_color_ranks` record their color scale for `gt_legend_continuous`.
141
+ - `docs/PARITY_TABLES.md` lists where they differ from R.
142
+
143
+ #### Tables, wave D (image export and composition)
144
+
145
+ - `sdvplot.great_tables`: `gt_save_crop`, `gt_social_crop` and `gt_save_batch` save tables as trimmed, padded images
146
+ (great_tables' `GT.gtsave`, headless Chrome); `gt_grid` and `gt_stack_tables` compose several tables into one HTML
147
+ block or image. Ports of the sdvplotR functions of the same names; differences are in `docs/PARITY_TABLES.md`.
148
+ - `[tables]` names `htmltools` and `nokap`, which great_tables 1.0 already installs. Saving needs Chrome or Chromium
149
+ (set `CHROME_PATH` for a non-standard install); no selenium.
150
+
151
+ #### Web family
152
+
153
+ - `add_logos`, `add_wordmarks`, `add_headshots` and `axis_logos` work on Plotly figures (`sdvplot.plotly`; sdvplot
154
+ pins the axis ranges so `height` is a fraction of the plot area) and Altair charts (`sdvplot.altair`, plus the
155
+ native `logo_layer`).
156
+ - `add_logos`, `add_wordmarks` and `add_headshots` on Bokeh figures (`sdvplot.bokeh`), HoloViews elements through a
157
+ Bokeh plot hook (`sdvplot.holoviews`) and Folium maps (`sdvplot.folium`, `x` longitude and `y` latitude).
158
+ - `embed=True` inlines the images as data URIs, for HTML that renders offline and for static export.
159
+ - The `[holoviews]` extra now installs Bokeh 3 as well.
160
+
161
+ #### Long tail (pygal, Cartopy, gallery compatibility)
162
+
163
+ - pygal: `add_logos`, `add_wordmarks` and `add_headshots` on XY-family charts (`XY`, `DateTimeLine`, `DateLine`,
164
+ `TimeLine`, `TimeDeltaLine`), drawn in every render; `embed=True` makes SVG and PNG exports work offline;
165
+ `team_style()` colors series by team (`sdvplot.pygal`, new `[pygal]` extra).
166
+ - pygal: a deep copy of a chart (`copy.deepcopy`) renders the marks it was copied with, marks added to the copy or to
167
+ the original afterwards stay on that chart, and a chart with marks pickles. Copy with `copy.deepcopy`; a shallow
168
+ copy (`copy.copy`) shares pygal's own series and filters and is not supported.
169
+ - Cartopy: the matplotlib adapter's `add_logos`, `add_wordmarks` and `add_headshots` take `transform=` (e.g.
170
+ `ccrs.PlateCarree()` for longitude/latitude, or any matplotlib transform); a `GeoAxes` without it raises
171
+ `ValueError`. A mark whose point falls outside the Axes is not drawn, in any coordinate system.
172
+ - `docs/COMPATIBILITY.md`: every package on python-graph-gallery's best dataviz packages list, how it gets marks or
173
+ colors, and the test that proves it (`tests/test_compat_*.py`, new `compat` dependency group); a watch item for
174
+ Reflex XY image marks, re-checked at each release.
175
+
176
+ #### Tables wave C2 (legends, layout and annotation)
177
+
178
+ - `sdvplot.great_tables` gains sdvplotR's legend, layout and annotation helpers, with sdvplotR's names and arguments
179
+ on pandas or polars data: `gt_legend_continuous`, `gt_legend_discrete`, `gt_marginalia`, `gt_outliers`,
180
+ `gt_percentile_bar`, `gt_row_accent`, `gt_scale_note`, `gt_set_font`, `gt_significance`, `gt_snake`,
181
+ `gt_snake_align`, `gt_social_tag`, `gt_spotlight`, `gt_tiers`, `gt_title_header`, `gt_watermark`, `gt_wrap_labels`.
182
+ - `gt_legend_continuous()` with no arguments draws the scale that `gt_percentile_bar`, `gt_color_ranks` or
183
+ `gt_color_pills` colored with; `gt_legend_discrete()` draws the key `gt_tiers` used.
184
+ - `docs/PARITY_TABLES.md` lists every difference from sdvplotR for these functions.
185
+
186
+ #### Parity extras (court coordinates, images by path, reference lines)
187
+
188
+ - `sdvplot.court_coords()`: stats.nba.com / stats.wnba.com legacy shot locations (`LOC_X`/`LOC_Y`, `x_legacy`/`y_legacy`)
189
+ to the court frame sportypy and `surface("nba")` draw, on pandas or polars, the port of sdvplotR's
190
+ `sdv_court_coords()`; bit-identical to it on real `shotchartdetail` rows.
191
+ - `sdvplot.matplotlib.add_images()` and `sdvplot.plotnine.geom_from_path()`: any image by local path or URL at (x, y),
192
+ sized like the logo verbs, the port of ggpath's `geom_from_path()`; unreadable images are skipped with one warning.
193
+ - `sdvplot.plotnine.geom_mean_lines()` and `geom_median_lines()`: per-panel reference lines, the ports of ggpath's,
194
+ matching its values on real data.
195
+ - `docs/PARITY.md` maps the remaining sdvplotR exports to sdvplot functions or recipes; `tools/export_parity_extras.R`
196
+ exports the sdvplotR and ggpath oracles the parity tests read.
197
+
198
+ #### Parity extras (title images, team tiers)
199
+
200
+ - `title_image()` in `sdvplot.matplotlib` (Axes title or Figure suptitle) and `sdvplot.plotnine` (added with `+`):
201
+ sdvplotR's `ggtitle_image()`, an image beside the title. The image is a team's logo when `league=` is given (an
202
+ unknown team warns once and keeps the title) or any image by URL or local path (one that cannot be read warns once
203
+ and keeps the title); `height` is in points, `side` is `"left"` or `"right"`, and the image and title are aligned
204
+ together as the title is, through later `set_title` calls. A second call on the same title replaces the image.
205
+ - `team_tiers()` in `sdvplot.matplotlib` (a Figure) and `sdvplot.plotnine` (a ggplot): sdvplotR's `sdv_team_tiers()`
206
+ tier list on its dark theme, from a pandas or polars frame with `tier_no` and `team` (optional `tier_rank`), with
207
+ `presort`, `tier_desc`, `no_line_below_tier` and `devel=True` (team text, no downloads). One shared preparation
208
+ (`sdvplot._tiers`) ranks, wraps the tier labels and sets the limits for both. The default logo height, 0.1 of the
209
+ panel, is about the largest at which 32 logos in 5 tiers neither overlap nor leave the panel at the default figure size.
210
+
211
+ #### Release hardening
212
+
213
+ - A deprecation helper, `sdvplot._deprecate` (`deprecate()` and `@deprecated_alias`), and its warning,
214
+ `sdvplot.SdvplotDeprecationWarning` (a `FutureWarning` and a `SdvplotWarning`), with a deprecation policy in
215
+ CONTRIBUTING.md: one minor release of warnings before a removal. Nothing is deprecated yet.
216
+ - `id_system` and `strict` wherever a team is resolved, passed to the resolver as `resolve()` takes them:
217
+ `team_colors`, `palette`, `logo_url`, `logo_image`, the great_tables helpers `gt_sdv_logos`, `gt_sdv_wordmarks`,
218
+ `gt_sdv_cols_label` and `gt_merge_stack_team_color` (`gt_theme_sdv_team` takes `id_system`; it is always strict),
219
+ and the reactable helpers `reactable_sdv_logos`, `reactable_sdv_wordmarks`, `reactable_sdv_cols_label`,
220
+ `reactable_sdv_team_color_bar` and `reactable_sdv_team_color_bg`. NHL stats ids need it:
221
+ `team_colors("nhl", [1, 6, 10])` reads them as ESPN ids and returns the Bruins, Oilers and Canadiens without a
222
+ warning, while `team_colors("nhl", [1, 6, 10], id_system="nhl_id")` gives the Devils, Bruins and Leafs.
223
+ `gt_sdv_cols_label`'s `id_system` now defaults to None: "auto" for logos and wordmarks, "espn" for headshots.
224
+ `team_colors`'s `which` is typed `Which`, so `which="secondry"` is a type error, as it is for `palette`.
225
+ - `sdvplot.typing`: the `Literal` types of the closed argument vocabularies (`IdSystem`, `HeadshotIdSystem`, `Which`,
226
+ `MarkType`), for annotating code that keeps an argument in a variable (`which: Which = "primary"`).
227
+
228
+ ### Changed
229
+
230
+ #### Documentation
231
+
232
+ - An automation example, `examples/automation/sdvplot_social.py`: social game-day graphics from live ESPN data through
233
+ sportsdataverse-py, for the NFL, college football, NBA, WNBA, MLB and NHL. `leaderboard` makes a season's leaders as
234
+ a great_tables table with headshots and logos; `gameday` makes final-score cards and a player-of-the-game card in
235
+ matplotlib. Images are 1080 x 1080 or 1200 x 675, in team colors with readable ink. With no games or leaders yet it
236
+ falls back to the most recent date or season and says so in the caption. Each run writes a manifest of images, alt
237
+ text, captions and hashtags; `post` publishes it to Bluesky over plain `requests` (a dry-run unless `--post`),
238
+ posting only fresh posts and each one once (a posted-ledger), retrying what is safe to retry.
239
+ With it: a GitHub Actions template to copy (`examples/automation/workflows/sdvplot-social.yml`), a weekly dry-run in
240
+ sdvplot's CI (`.github/workflows/automation-example.yml`, which never posts), offline tests
241
+ (`tests/test_automation_example.py`) and the docs page *Social graphics* (`docs/docs/automation/index.md`).
242
+
243
+ #### Documentation — example notebooks by section, interactive outputs and the gallery
244
+
245
+ - `tools/render_notebooks.py` renders every notebook under `examples/notebooks/`, its folder picking the section: the
246
+ top level is "Getting started", then `leagues/` (Tutorials by league), `cookbooks/`, `recipes/` and
247
+ `leaderboards/`, each a sidebar category, beside the hand-written Automation guide. A notebook's
248
+ `metadata["sdvplot"]` (`label`, `position`, `description`, optional per-cell `timeout`) replaces the renderer's
249
+ hard-coded list, and `--only` takes a path such as `leagues/nfl`. A deleted notebook's page and outputs go with it.
250
+ - Interactive outputs (Plotly, Vega-Lite, Altair, great_tables, folium, Bokeh, HoloViews, reactable widgets) render
251
+ as standalone pages under `docs/static/outputs/`, shown in iframes that a site client module sizes to their content.
252
+ - A gallery page shows every figure tagged `gallery` in a notebook as a thumbnail linked to its example; each render
253
+ writes a per-notebook sidecar, so a partial render keeps the gallery whole (`--gallery-only` rebuilds it).
254
+ - The weekly `live-tests-cron` render executes every notebook and publishes every generated path.
255
+
256
+ #### Release hardening
257
+
258
+ - `sdvplot.plotnine.scale_color_sdv` / `scale_fill_sdv` take `which`, `season` and `na_value` by keyword only (as
259
+ `palette()` and `team_colors()` do), and they and `sdvplot.pygal.team_style` take `id_system` and `strict`:
260
+ `scale_color_sdv("nhl", id_system="nhl_id")` reads NHL stats ids, which `"auto"` reads as ESPN ids and colored as
261
+ other teams without a word. A season before the league's first is reported with the league's own range in the first
262
+ error (`season 1850 ... for nfl (1920 to ...)`), not the index's and then the league's on a retry.
263
+
264
+ - Team colors for the 3,636 teams that had only placeholder colors: all of soccer, MiLB, cricket, the HockeyTech
265
+ leagues, college hockey, the PHF, AAF, USFL and XFL, and the college teams ESPN's lists give none. 2,270 now carry
266
+ ESPN's colors (`color_source="espn"`): its per-team endpoint by ESPN id, and for a college team its school's colors in
267
+ another ESPN sport, through the same school id at the same location or, for college baseball and softball (which
268
+ number their teams apart from the school), a unique exact name and location. The other 1,364 carry the two dominant
269
+ colors of their archived logo, flagged `color_source="logo"` because no source publishes them (`teamcolors` was
270
+ surveyed and not used: GPL data from 2020 that adds 5 teams). Two scoreboard-only men's college hockey teams, with no
271
+ logo and no ESPN color, keep a fallback. ESPN's stand-in colors (black alone, or black with its stock red) no longer
272
+ count as a team's: 417 college teams that showed them now show their school's ESPN colors (119) or their logo's
273
+ (298). A secondary equal to its primary is dropped (5 teams). `tools/fetch_sources.py --colors-only` refreshes the
274
+ two new snapshots, `data-raw/espn_colors.csv` and `data-raw/logo_colors.csv`.
275
+
276
+ - The `sdvplot.great_tables` helpers take their options by keyword only: past the table and the columns (or the
277
+ other leading "what" arguments: `gt_delta(gt, from_, to)`, `gt_highlight_cells(gt, columns, condition)`,
278
+ `gt_significance(gt, columns, p_columns)`, `gt_tiers(gt, levels, colors)`, `gt_save_batch(data, group, fn, file)`,
279
+ `gt_save_crop(data, file)`, `gt_title_header(gt, title)` and so on) every argument is keyword-only, so a later
280
+ release can add an option without rebinding a positional value. No public function of sdvplot or its submodules
281
+ takes more than four arguments by position (a test keeps it so). Migrate by naming the option:
282
+ `gt_color_pills(gt, "pts", palette=...)`, `gt_save_batch(df, "conf", build, "{group}.png", dir="out")`.
283
+ - Documentation: an "Add an adapter" guide for contributors (`docs/docs/adapters/add-an-adapter.md`), a checklist from
284
+ the adapter module to the changelog entry, with a worked example that passes `check_adapter_contract`.
285
+ - Documentation: the MBB, WBB and college-hockey tutorials draw their tier lists on `team_tiers(theme="light")`, where
286
+ dark logos (Iowa, West Virginia, Penn State) no longer vanish into the dark background.
287
+ - Documentation: the tables cookbook's stripes gotcha says theme order no longer matters (`gt_theme_kenpom` bands
288
+ with a CSS rule) and that stripes cover plain fills only in VS Code and Positron notebooks, and the college softball
289
+ World Series table turns row striping off after `gt_theme_ncaa` (`gt_color_results` fills every row).
290
+ - Documentation: every example notebook is re-rendered against the 0.1.0 API. The pages that called a league's
291
+ colors fallbacks (the colors tutorial and cookbook; cricket, MLB, NBA, PWHL, soccer, spring football, college
292
+ hockey; the rank bump chart) now say where the index's colors come from (`color_source` `espn` or `logo`); the WNBA
293
+ tier list ranks teams within each tier, so its logos no longer overlap; the cache page lists `urlimages/` and the
294
+ current `versions()` output. The docs pages' Python examples run offline in the test suite, which checks every
295
+ output their comments show. The social-graphics workflow template pins a current sdvplot commit.
296
+ - The social-graphics example (`examples/automation/sdvplot_social.py`) covers men's and women's college basketball
297
+ (`--league mbb` / `wbb`, hashtags CBB and WCBB). Both keep NCAA Division I only, as ESPN's group 50 (checked by
298
+ name): leaderboards read that group's own leaders, since ESPN's league-wide college leaders are mostly Division II,
299
+ III and NAIA players on teams the index does not hold, and score cards keep games between two of its teams.
300
+ - Documentation: "The adapter contract" page follows `sdvplot.testing` again: rules 0 to 8 (exactly one warning per
301
+ skip reason, `height` and `alpha` checked on every verb when called, drawn heights measured within 1%), the axis
302
+ hooks `_drawn_axis_marks` (`(team_id, tick, height)`) and `_visible_axis_labels`, the table harness's rules T0 to T6,
303
+ and a minimal adapter that passes the current harness.
304
+ - API reference: one page per public submodule (`sdvplot.matplotlib`, `sdvplot.plotnine`, `sdvplot.plotly`,
305
+ `sdvplot.altair`, `sdvplot.bokeh`, `sdvplot.holoviews`, `sdvplot.folium`, `sdvplot.pygal`, `sdvplot.great_tables`,
306
+ `sdvplot.reactable`, `sdvplot.plottable`, `sdvplot.testing`, `sdvplot.typing`), with a section per public name in the
307
+ top-level pages' format (signature, arguments, returns, raises, example, see also). Only the 17 top-level functions
308
+ had pages, so `gt_theme_athletic` or any adapter-only function was on none.
309
+ - The docstring gate (`tools/gen_docs.py --check`) finds the public submodules itself, as `tests/test_api.py` does,
310
+ instead of reading a list a new submodule could be left off; a new one also fails until it has a reference page.
311
+ - The submodule examples run on a seeded cache (a logo and wordmark for every NFL team, the examples' headshots and
312
+ images) instead of an empty one, so the 32 that stopped at their first download now run to the end: an error after
313
+ the first mark lookup no longer passes. Only the four that render through a headless browser stay tolerated.
314
+ - Release: a release run refuses a README without a PyPI install line (it becomes the version's PyPI page), a
315
+ CHANGELOG without a dated heading for the tag, or entries left under `[Unreleased]`
316
+ (`tests/test_repo_files.py::test_the_tagged_release_is_ready`). The dist is built in its own job from a fresh
317
+ checkout after the tests pass, with no uv cache from other workflows, a pinned uv and no persisted credentials; the
318
+ docs deploy (which holds `contents: write`) pins its actions by commit SHA.
319
+ - CI: the built wheel is installed with no extras and its core is exercised (3.10 and 3.14), then every public
320
+ submodule is imported with `[all]`; the offline suite runs on 3.10 through 3.13; pytest runs with `--strict-markers`
321
+ and `--strict-config`. Python 3.14 is a declared classifier. A run on `main` is never cancelled by a later push (only
322
+ a PR's is), and the live network tests run on manual dispatch and weekly in `live-tests-cron`, not on every push.
323
+ - Repeated lookups are faster: with everything cached, `logo_url` takes about 0.1 ms per call instead of 2.5 ms, and
324
+ `logo_image` about 1 ms instead of 4 ms (most of it the copy of the image the caller gets). A cached file within
325
+ its TTL is remembered for the session instead of having its metadata re-read on every call, the bundled index's
326
+ directory is looked up once, and each team's marks are taken from the manifest once per manifest load.
327
+ - The matplotlib-family adapters (matplotlib, seaborn, plotnine, plottable, `title_image`) keep a mark in memory no
328
+ bigger than they draw it, 512 px tall. Half of the logo archive is 4096 px: each such mark held 64 MiB, a quarter of
329
+ the decoded-image cache, so a few of them pushed out everything else and every plot decoded them again (about 0.5 s
330
+ each). Now a repeated `add_logos` with a 4096 px mark takes about 0.02 s instead of 0.5 s; the first one still pays
331
+ the decode.
332
+ - Documentation: `sdvplot.matplotlib.add_images` and `title_image`, and `sdvplot.plotnine.geom_from_path` and
333
+ `title_image`, say an image URL must be https. They said "http or https", but an http URL is refused with
334
+ `UnsafeDownloadError`.
335
+ - Documentation: every public function's docstring names the error classes the code can raise since the API freeze:
336
+ `InputError` for the shared checks (heights, alpha, league, id system, season, variant; it said `ValueError`),
337
+ `UnsupportedTargetError` for a target an adapter cannot draw on (it said `TypeError`), and each download error a
338
+ function can meet (`OfflineError`, `DownloadError`, `IntegrityError`, `UnsafeDownloadError`, `UnsafeCachePathError`,
339
+ and `OptionalDependencyError` for an SVG mark without the `svg` extra), with the ones a plotnine layer, a reactable
340
+ column or a plottable column raises when it is drawn marked as such. The 17 top-level functions' examples now run
341
+ offline in `tests/test_submodule_examples.py`, as the submodules' do. The concept and adapter pages say what the code
342
+ does: the cache's `urlimages/` directory, shared downloads and `UnsafeDownloadError` (not an `OfflineError`);
343
+ `id_system` and `strict` on `palette()`, `team_colors()` and `logo_url()`; the MLB Stats API codes whose team a
344
+ season does change (`KCA`, `WAS`, `SEA`, `MIL`), where the page said no code's did; the libraries each extra installs.
345
+ - Issue templates: the bug report asks for the plotting or table library and the league, and takes the whole
346
+ `sdvplot.versions()` output; the feature request lists every top-level function, the submodule helpers, new
347
+ adapters and new leagues; the wrong-team template, now also for wrong colors, picks the league from a list and asks
348
+ for the team id returned and expected, the season and the team's `color_source`. A test keeps those lists equal to
349
+ the leagues, adapters, functions and color sources sdvplot has. The issue chooser links the docs and private
350
+ security reporting.
351
+
352
+ ### Fixed
353
+
354
+ #### Adapter contract follow-ups
355
+
356
+ - plotnine: a faceted plot warns once per render for each reason points are skipped (an unknown team, a missing
357
+ mark), naming the values of every panel, instead of once per panel; `axis_logos` on a faceted plot likewise warns
358
+ once.
359
+ - plotnine: `add_logos`/`add_wordmarks` with one season per team draw on a faceted plot (each panel's copy of a
360
+ point keeps its season); the mark geoms take a per-row season as the `season` aesthetic.
361
+ - plotnine: the mark geoms leave missing and out-of-limits x/y to plotnine (`na_rm`, scale limits), so a row
362
+ plotnine drops itself no longer also warns `skipped ... missing x or y`.
363
+ - plotnine: a point that plotnine copies into every panel (a layer without the facet column) counts once in the
364
+ warning, not once per panel.
365
+ - plotnine: drawing no longer swaps the interpreter's warning filters (`warnings.catch_warnings`, not thread-safe)
366
+ to keep the per-panel placement quiet; it uses a private quiet path instead.
367
+ - The adapters' test hooks report the height an image was drawn at, not the height they were asked for: matplotlib
368
+ and plotnine measure each image's extent after a draw, pygal renders the chart and reads the SVG, and the axis-logo
369
+ hooks of matplotlib, plotnine, Plotly and Altair report each image's height too.
370
+ - `sdvplot.testing.check_adapter_contract` is stricter: a call that skips nothing must not warn and each reason it
371
+ skips points for gives exactly one `SdvplotWarning` (rules 1, 2, 6 and 7); `height` (including its out-of-range
372
+ values) is checked on `add_headshots` and `axis_logos` as well as `add_logos` and `add_wordmarks`, and `alpha` on
373
+ every verb that takes it; heights are compared within 1% of the requested value, as measured by the hooks.
374
+ An out-of-range `height` must raise when the verb is called, not only when the marks are rendered.
375
+ `drawn_axis_marks` now returns `(team_id, tick position, height)`. `check_table_adapter_contract` likewise requires
376
+ no warning for known values and exactly one for all-unknown input.
377
+
378
+ #### Tables follow-ups
379
+
380
+ - Muted text (`gt_theme_sdv_team`'s subtitle, the `gt_legend_discrete` subtitle) blends at sdvplotR's exact weights,
381
+ as the table themes already did; a few colors were one step off in a channel.
382
+ - A table whose id is the empty string gets a random id before a theme or cell, border or watermark helper scopes CSS
383
+ to it; only `gt_theme_sdv` did this before, and elsewhere the CSS (`# td`) reached no cell.
384
+ - Every `sdvplot.great_tables` function refuses raw data with one message ("gt must be a great_tables GT, not
385
+ DataFrame. It looks like raw data: wrap it in great_tables.GT() first."), as sdvplotR's `.check_gt` words it; an
386
+ unknown `density` and an unknown `*_style` key are worded alike across the table modules too, and the style error
387
+ names the argument.
388
+ - `gt_sdv_logos`, `gt_sdv_wordmarks` and `gt_sdv_headshots` document exactly which `locations` they take
389
+ (`loc.body()`, `loc.stub()`, `loc.row_groups()`) and raise `ValueError` for any other: column labels used to come out
390
+ as escaped `<img>` text, and a title or source note was silently left alone. `gt_sdv_cols_label` puts marks in the
391
+ column labels.
392
+ - A headshot whose ESPN player id was read through a float (pandas stores `[3139477, None]` as floats, so the cell reads
393
+ `3139477.0`) carries `3139477` in its alt text and team attribute, in every adapter; the URL was already right.
394
+ - `gt_theme_preview(n=...)` takes only a positive whole number of rows (numpy integers included); 0, negative numbers,
395
+ booleans, floats and strings raise `ValueError` instead of showing no rows, all but the last, one row or a polars
396
+ error.
397
+ - A table theme swaps out the fonts an earlier sdvplot theme put in front of the table's fonts instead of stacking on
398
+ them: a table themed twice no longer lists every font twice, and a second theme no longer keeps the first one's font
399
+ as its fallback (with `gt_theme_sdv`, directly behind Lato). Fonts you set with `opt_table_font()` stay, behind the
400
+ theme's, as in sdvplotR. `gt_theme_sdv` and `gt_theme_sdv_team` fall back to gt's `default_fonts()`, as sdvplotR's
401
+ do.
402
+ - A translucent `#rgba`/`#rrggbbaa` color is refused (`ValueError`) where sdvplot draws the color it computes (palette
403
+ stops and ramps, legend and tier swatches, theme accents), instead of being drawn solid with its alpha silently
404
+ dropped; an opaque alpha (`f`/`ff`) is accepted and `#rgba` is now read. A color sdvplot passes to CSS as given and
405
+ only measures for its ink (`gt_color_pills(na_color=)`, `gt_indicator_boxes(color_yes=, color_no=, color_na=)`,
406
+ `gt_outliers(fill=)`) is still drawn translucent, and its ink is now read on the color it shows over the table
407
+ background rather than on the color with its alpha dropped. `reactable_sdv_team_color_bg` still replaces
408
+ `na_color`'s alpha with its own `alpha`, as sdvplotR does; CSS-only color arguments take any CSS color.
409
+ - A translucent table background (`tab_options(table_background_color="#111111CC")`) is read as the color it shows
410
+ over the page when `gt_legend_discrete` and `gt_marginalia` pick their ink, so a near-black one gets light text.
411
+
412
+ #### Content findings (team index, team tiers, surface)
413
+
414
+ - MLB historical codes are season-dated and reach their franchise. A new snapshot, `data-raw/mlbstats_history.csv`
415
+ (the MLB Stats API's teams for every season since 1901; its team ids are franchise ids), dates each abbreviation
416
+ and teamCode by the seasons the API used it. `KCA` is the Kansas City Athletics in 1955-67 and the Royals otherwise
417
+ (the Royals' teamCode `kca` from 1968). `WAS` is the Twins' Senators to 1960, the Rangers' 1961-71 and otherwise the
418
+ Nationals (teamCode `was` from 2005). `PHA`, `BSN`, `BRO`, `NYG`, `SLB`, `WS1`/`WS2`, `MON`, `CAL`, `ANA`, `FLA`
419
+ and `OAK` reach today's team. A teamCode or fileCode is dropped only over seasons that overlap another franchise's
420
+ run of the same abbreviation. Baseball-Reference/sportsipy MLB codes now go through that history instead of today's
421
+ team names, which sent the 1901 Milwaukee Brewers (`MLA`) to today's Brewers and the 1872 Washington Nationals to
422
+ today's Nationals; `PHA`, `KCA`, `MLN`, `SEP`, `WSH` (1901-60), `WSA` and the rest now resolve. An ESPN
423
+ abbreviation or sdvplotR key another franchise held first starts the season after it (`MIL` from 1966, `SEA` from
424
+ 1970, `WSH` from 1961; sdvplotR's `KCA` from 1968), so `MIL` in 1960 is the Milwaukee Braves.
425
+ - `resolve()` without a season reads a reused code as its current holder: the team whose range covers the latest
426
+ season in the index, then any. A season no alias covers falls back the same way before ranges are ignored.
427
+ Measured over every alias of every league, undated and in each season 1870-2026, the only answers that change are
428
+ the MLB eras above; a code only one team ever held, and every other league, answer as before.
429
+ - ESPN's college baseball and softball abbreviations resolve. ESPN's teams list gives NC State `NCST` and Missouri
430
+ `MIZZ`, while its per-team endpoint, scoreboards and standings use `NCSU`, `MIZ`, `UCR`, `KENN` and about 120 others;
431
+ a new snapshot, `data-raw/espn_abbrs.csv`, keeps the per-team abbreviations. One the list gives another team stays
432
+ with that team (LSU Alexandria's per-team `LSU`), and one ESPN gives two teams (softball's `CEN`) is left out.
433
+ - UFL 2024-25 codes (`BIR`, `ARL`, `MEM`, `MIC`, `SA`, `HOU` for the Roughnecks) and every XFL code (2020, 2023)
434
+ resolve, dated by the seasons ESPN's scoreboards show them (`data-raw/espn_abbrs.csv`), with the names of those
435
+ seasons ("Houston Roughnecks", "Arlington Renegades"). Known gap: ESPN id 126075 was the Houston Roughnecks in
436
+ 2024-25, but the archive has only the 2026 Houston Gamblers marks for it, so `logo_url("HOU", "ufl", season=2024)`
437
+ is the Gamblers logo. The archive's Roughnecks mark is the XFL team's (xfl 112648, 2020-23), another league's
438
+ entity, and nothing establishes that the UFL team used it, so it is not wired (`docs/PARITY.md`).
439
+ - NHL Utah: `logo_url("UTA", "nhl", season=2025)` (2024-25; NHL seasons are end years) is the Utah Hockey Club's mark
440
+ again, and 2026 on the Utah Mammoth's. The NHL's logo API dates the Mammoth's (team 68) logos from 2024-25, so they
441
+ outranked the Hockey Club's (team 59) one-season marks. A curated range (`data-raw/curated/mark_ranges.csv`, each
442
+ row with its reason) now starts team 68's marks in 2026, and a mark alias's range narrows a manifest row's own range
443
+ instead of only filling an open one (no other archived mark changes).
444
+ - Women's college hockey: ESPN's scoreboards use team ids its teams list lacks. A new snapshot,
445
+ `data-raw/espn_unlisted_teams.csv`, keeps them: one whose name and abbreviation are a listed team's is that team's
446
+ second ESPN id (Minnesota State's `24059` resolves to `2364`, not only by name), and any other is a team of its own
447
+ (Delaware, `48`, `DEL`). Men's college hockey adds the teams its scoreboards use that the archive lacks, listed by
448
+ ESPN or not (SUNY Morrisville, `126813`; Maryville, `132633`); the scan reads one month at a time, since a year of
449
+ men's games passes the scoreboard's 1,000-event cap. The archive has no mark for these teams, so their logos are
450
+ `None` with a warning and their colors are flagged `color_source="fallback"`; none is made up.
451
+ - `team_tiers()` (matplotlib and plotnine) takes `theme="dark"` (the default, sdvplotR's) or `theme="light"`: dark
452
+ logos such as Ohio State's, Texas A&M's and Penn State's vanished on the fixed dark background. The light theme's
453
+ labels, lines, subtitle and caption meet WCAG contrast on white; sdvplotR has only the dark theme (`docs/PARITY.md`).
454
+ - `surface()` no longer floods stderr with "findfont: Font family 'Clarendon-Regular' not found". sportypy numbers
455
+ football fields in Clarendon-Regular, a font it does not ship, so matplotlib logged a warning for every number it
456
+ measured and drew its default font anyway. When Clarendon is not installed, `surface()` asks sportypy for that
457
+ default (DejaVu Sans) by name, through `field_updates`, so the numbers look the same and nothing is logged; process
458
+ logging is untouched, and a `number_font` the caller passes still wins.
459
+ - Borders and fills that sdvplot's great_tables helpers draw now show in a notebook too. great_tables' notebook repr
460
+ marks its own cell rules `!important` (`td, th {border-style: none}`, the stub's and row groups' backgrounds), and
461
+ a stylesheet `!important` beats a plain inline style, so `gt_row_accent`'s bars, for one, showed in saved images and
462
+ vanished in Jupyter. Every border and fill the helpers set (`gt_row_accent`, `gt_spotlight`, `gt_border_grid`,
463
+ `gt_cutline`, `gt_group_stripes`, `gt_marginalia`, `gt_snake`, `gt_tiers`, `gt_outliers`, `gt_bold_rows`,
464
+ `gt_color_results`, `gt_highlight_cells`, the team-mark row groups and the themes) is now inline `!important`, through
465
+ one helper.
466
+
467
+ #### Release hardening
468
+
469
+ - `surface()` draws a rink or court in well under a second instead of 16-19 s: matplotlib's `add_patch` walked every
470
+ segment of sportypy's 10,000-point circle polygons as a Bezier curve to find the data limits (~1.3 M segments per
471
+ rink); for a polygon those limits are its vertices, so they now come from the vertices in one call. The drawn PNG
472
+ and the Axes' data limits are byte-for-byte the same.
473
+ - `gt_theme_kenpom` no longer wipes cell fills. Its row bands were `!important` cell fills, so theming a table after
474
+ `data_color`, `tab_style(style.fill(...))` or `gt_color_results` replaced their fills, and a plain fill applied after
475
+ the theme lost to them. The bands are now a table-scoped stylesheet rule on the data rows as drawn (row group
476
+ headings and summary rows are neither counted nor painted), which every cell fill shows over, in either order and in
477
+ the notebook repr (`docs/PARITY_TABLES.md` records the divergence).
478
+ - Row striping no longer covers the text color sdvplot's fill helpers draw in VS Code and Positron notebooks. There,
479
+ great_tables' repr marks its whole stylesheet `!important` (Jupyter, Quarto, Databricks and saved files do not), and
480
+ on every other row the stripe's text color beat the plain inline ink that `gt_color_results`, `gt_bold_rows`,
481
+ `gt_tiers`, `gt_spotlight`, `gt_outliers`, `gt_highlight_cells` and `gt_highlight_na` pair with their `!important`
482
+ fills: white text on a dark fill turned the stripe's dark gray. That ink is now `!important` too. `gt_color_ranks`
483
+ fills through great_tables' `data_color`, whose plain fills sdvplot cannot mark, so on a striped table in VS Code or
484
+ Positron it warns once (`SdvplotWarning`) and says to turn striping off with `opt_row_striping(row_striping=False)`.
485
+ - A failed logo download raises sdvplot's own errors, never a `requests` exception or a bare `OSError`: an HTTP error
486
+ status (4xx or 5xx) with no cached copy is the new `sdvplot.DownloadError` (an `OfflineError` and an `OSError`), and
487
+ a download whose sha256 does not match the manifest, or an archived file that is not an image, is the new
488
+ `sdvplot.IntegrityError` (a `DownloadError`). A 404 used to escape as `requests.HTTPError`, which
489
+ `except sdvplot.SdvplotError` did not catch. `except OSError` still catches both.
490
+ - Every shared input check raises `InputError`, so `except sdvplot.SdvplotError` catches bad input:
491
+ `logo_image(size=...)` takes an int from 1 to 4096 (`size=0` was a `ZeroDivisionError`, `size=-5` a Pillow
492
+ `ValueError`, and `size=2.5` or `size=True` drew a 2x2 or 1x1 image; a size far past 4096 could abort the process in
493
+ the SVG renderer), `suggest(n=...)` an int of at least 1 (`n=-1` was difflib's `ValueError` naming `-3`), a season
494
+ that is not a year (`"2020-21"`, a `pd.Timestamp`) or a season list of the wrong length is an `InputError`, and so
495
+ is an unknown `mark_type` in `reactable_sdv_cols_label` and `gt_sdv_cols_label`.
496
+ - `matplotlib.title_image` and `plotnine.title_image` take `height` in points of at least 1: `height=0.1` (a plot
497
+ fraction, as `add_logos` takes) drew a 0.1 pt image without a word, and now raises an `InputError` naming points.
498
+ - Importing an adapter submodule without its library (`import sdvplot.plotly` without plotly) raises
499
+ `OptionalDependencyError` naming the extra (`pip install "sdvplot[plotly]"`) rather than a bare
500
+ `ModuleNotFoundError`; the front door had the hint, a direct import did not. `OptionalDependencyError` is now a
501
+ `ModuleNotFoundError` (and still an `ImportError`), so `except ModuleNotFoundError` around the import keeps working.
502
+ - The warning (or, with `strict=True`, the error) for an ambiguous team value lists the teams it could mean and what
503
+ picks one: `'Charlotte' (ambiguous: 2429 Charlotte 49ers or 3253 Charlotte Saints); pass season= for a code reused
504
+ across eras, or id_system= for the id system of the values`. It used to say only "ambiguous".
505
+ - Docstrings: `matplotlib.add_logos`, `add_wordmarks`, `add_headshots` and `axis_logos` list the
506
+ `UnsupportedTargetError` and `OfflineError` they raise (`add_images` the former), and plotnine's `add_logos` and
507
+ `add_wordmarks` say `season` takes one season or one per point, as they always did.
508
+ - A season outside the seasons sdvplot knows for the league is an `InputError` (a `ValueError`) naming the bounds,
509
+ wherever a season is taken. The first season is the league's earliest dated alias in the bundled index (1920 for the
510
+ NFL, 1947 for the NBA, 1997 for the WNBA, 2020 for the XFL; 1871, MLB's, for a league whose history is not dated), the
511
+ last is next year. `logo_url("OAK", "nfl", season=1900)` and `resolve(..., season=20)` used to resolve silently. A
512
+ split season such as `"2020-21"` gets a hint (pass the ending year), and a season of the wrong type (a `pd.Timestamp`)
513
+ is an `InputError` that names `season` rather than the team values.
514
+ - A `variant` that no mark in the archive has (a typo, or not a string) is an `InputError` listing the league's
515
+ variants; it used to fall back to the default mark without a word. A variant the team lacks still falls back, as
516
+ before.
517
+ - Warnings point at the caller's line: they walk out of sdvplot's frames instead of using fixed `stacklevel`s, which
518
+ named `_colors.py`, `_placement.py` and other sdvplot files whenever the call went through more than one function.
519
+ - A misspelled column name is the same `ValueError` on pandas and polars (`column(s) ['teamz'] not in the table; its
520
+ columns are [...]`) in every great_tables helper that takes columns: the `gt_sdv_*` marks (their
521
+ `locations=loc.body(...)` too), `gt_percentile_bar`, `gt_wrap_labels`, `gt_color_pills` and the rest, through the one
522
+ column resolver they share. pandas used to match nothing silently and polars raised its own `ColumnNotFoundError`.
523
+ - Threads that ask for the same uncached mark at once (`logo_image()` from a thread pool) download and decode it once:
524
+ the cache runs one fetch per file and the others wait for it, and the decoded-image cache decodes each key once. On
525
+ Windows every thread used to download its own copy, and replacing the file while another thread had it open raised
526
+ `PermissionError: [WinError 5] Access is denied`; the logo manifest's first load warned `could not refresh ...`
527
+ the same way. A replace that another process refuses, over the same content-addressed file it already wrote, is
528
+ no longer an error.
529
+ - A long-running session no longer keeps every logo manifest (about 17 MiB parsed) or nflverse player table it has read:
530
+ when the cached file is refreshed, the previous one is freed. `clear_cache()` now also frees the parsed manifest, the
531
+ player table and the per-league tables built from the manifest, as it already freed the decoded images.
532
+ - `clear_cache()` unlinks a cache subdirectory that is a symlink in the default cache directory (what it points to is
533
+ untouched) and leaves one alone with a warning in a directory you chose. It used to raise `OSError` from
534
+ `shutil.rmtree` after deleting `manifest/`, leaving the later subdirectories and the in-memory caches as they were;
535
+ the in-memory caches are now emptied even when a removal fails.
536
+ - The in-memory cache of decoded images counts bytes per sample: a 16-bit image (mode `I;16`) is two bytes a pixel and
537
+ a 32-bit one (`I`, `F`) four. They were counted at one byte a sample, so they could hold two to four times the
538
+ 256 MB budget.
539
+
540
+ ### Security
541
+
542
+ - Image URLs from the logo manifest (`archive_url`) and from nflverse's player table (`headshot`) must be plain https
543
+ URLs: a host, then only RFC 3986 characters, with no quote, `<`, `>`, whitespace, backslash or control character. A
544
+ manifest row that fails is dropped and a headshot that fails is treated as missing (the player gets their ESPN
545
+ headshot when nflverse has their ESPN id), each with one `SdvplotWarning`. Such a URL used to reach the web adapters
546
+ unchanged, and Altair's HTML export wrote it into a `<script>` block unescaped, so a poisoned manifest or player table
547
+ could run script in an exported page. The web adapters also percent-encode any such character left in an image URL.
548
+ - SVG rendering is bounded. An SVG mark is rendered inside a `size` x `size` box (its longest side `size` pixels, at
549
+ most `sdvplot._images.MAX_SIZE`, 4096) after a small probe render measures its aspect ratio. An SVG more than 64 times
550
+ longer than it is wide, or a `size` over 4096, is an `InputError` before anything is rendered. resvg used to render
551
+ at `width=size` first, so a tall SVG or a large `size` asked for gigabytes and could abort the Python process.
552
+ - `urllib3>=2.6` is a dependency. `requests>=2.33` still allowed urllib3 1.26 and 2.0 to 2.5, which decompress a
553
+ whole received chunk at once: a 275-byte gzip body cost 4 GB of memory before the download byte cap saw it
554
+ (CVE-2025-66471). The download loop's fallback for urllib3 below 2 is removed.
555
+ - A download's 120 s deadline covers the TLS handshake and the response headers, not only the body: a watchdog shuts
556
+ the connection's socket down at the deadline, across every redirect hop, and no single read waits past it. Each read
557
+ had a 60 s timeout of its own, so a server sending a header byte every 59 s held the call open almost indefinitely.
558
+ Through a proxy, the watchdog covers the body only.
559
+
560
+ [Unreleased]: https://github.com/sportsdataverse/sdvplot/compare/v0.1.0...HEAD
561
+ [0.1.0]: https://github.com/sportsdataverse/sdvplot/releases/tag/v0.1.0