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.
- sdvplot-0.1.0/.gitattributes +44 -0
- sdvplot-0.1.0/.gitignore +18 -0
- sdvplot-0.1.0/CHANGELOG.md +561 -0
- sdvplot-0.1.0/CITATION.cff +18 -0
- sdvplot-0.1.0/CODE_OF_CONDUCT.md +83 -0
- sdvplot-0.1.0/CONTRIBUTING.md +237 -0
- sdvplot-0.1.0/LICENSE +21 -0
- sdvplot-0.1.0/PKG-INFO +188 -0
- sdvplot-0.1.0/README.md +107 -0
- sdvplot-0.1.0/SECURITY.md +34 -0
- sdvplot-0.1.0/pyproject.toml +171 -0
- sdvplot-0.1.0/src/sdvplot/__init__.py +68 -0
- sdvplot-0.1.0/src/sdvplot/_cache.py +484 -0
- sdvplot-0.1.0/src/sdvplot/_colors.py +206 -0
- sdvplot-0.1.0/src/sdvplot/_contrast.py +60 -0
- sdvplot-0.1.0/src/sdvplot/_court.py +110 -0
- sdvplot-0.1.0/src/sdvplot/_deprecate.py +39 -0
- sdvplot-0.1.0/src/sdvplot/_dispatch.py +342 -0
- sdvplot-0.1.0/src/sdvplot/_errors.py +93 -0
- sdvplot-0.1.0/src/sdvplot/_headshots.py +131 -0
- sdvplot-0.1.0/src/sdvplot/_images.py +292 -0
- sdvplot-0.1.0/src/sdvplot/_index.py +178 -0
- sdvplot-0.1.0/src/sdvplot/_lazy.py +15 -0
- sdvplot-0.1.0/src/sdvplot/_manifest.py +73 -0
- sdvplot-0.1.0/src/sdvplot/_marks.py +296 -0
- sdvplot-0.1.0/src/sdvplot/_normalize.py +82 -0
- sdvplot-0.1.0/src/sdvplot/_placement.py +168 -0
- sdvplot-0.1.0/src/sdvplot/_resolve.py +363 -0
- sdvplot-0.1.0/src/sdvplot/_surface.py +220 -0
- sdvplot-0.1.0/src/sdvplot/_tables.py +87 -0
- sdvplot-0.1.0/src/sdvplot/_tiers.py +135 -0
- sdvplot-0.1.0/src/sdvplot/_types.py +38 -0
- sdvplot-0.1.0/src/sdvplot/_versions.py +30 -0
- sdvplot-0.1.0/src/sdvplot/_web.py +61 -0
- sdvplot-0.1.0/src/sdvplot/altair.py +652 -0
- sdvplot-0.1.0/src/sdvplot/bokeh.py +323 -0
- sdvplot-0.1.0/src/sdvplot/data/INDEX_VERSION +1 -0
- sdvplot-0.1.0/src/sdvplot/data/aliases.parquet +0 -0
- sdvplot-0.1.0/src/sdvplot/data/teams.parquet +0 -0
- sdvplot-0.1.0/src/sdvplot/folium.py +316 -0
- sdvplot-0.1.0/src/sdvplot/great_tables/__init__.py +267 -0
- sdvplot-0.1.0/src/sdvplot/great_tables/_cells.py +1814 -0
- sdvplot-0.1.0/src/sdvplot/great_tables/_export.py +844 -0
- sdvplot-0.1.0/src/sdvplot/great_tables/_layout.py +1802 -0
- sdvplot-0.1.0/src/sdvplot/great_tables/_marks.py +798 -0
- sdvplot-0.1.0/src/sdvplot/great_tables/_themes.py +2134 -0
- sdvplot-0.1.0/src/sdvplot/holoviews.py +303 -0
- sdvplot-0.1.0/src/sdvplot/matplotlib.py +934 -0
- sdvplot-0.1.0/src/sdvplot/plotly.py +595 -0
- sdvplot-0.1.0/src/sdvplot/plotnine.py +1104 -0
- sdvplot-0.1.0/src/sdvplot/plottable.py +141 -0
- sdvplot-0.1.0/src/sdvplot/py.typed +0 -0
- sdvplot-0.1.0/src/sdvplot/pygal.py +416 -0
- sdvplot-0.1.0/src/sdvplot/reactable.py +488 -0
- sdvplot-0.1.0/src/sdvplot/testing.py +579 -0
- sdvplot-0.1.0/src/sdvplot/typing.py +21 -0
- sdvplot-0.1.0/tests/__init__.py +0 -0
- sdvplot-0.1.0/tests/baseline/test_baseline_add_images.png +0 -0
- sdvplot-0.1.0/tests/baseline/test_baseline_court_coords_half_court.png +0 -0
- sdvplot-0.1.0/tests/baseline/test_baseline_plotnine_facets.png +0 -0
- sdvplot-0.1.0/tests/baseline/test_baseline_plotnine_mean_and_median_lines.png +0 -0
- sdvplot-0.1.0/tests/baseline/test_baseline_plotnine_team_tiers.png +0 -0
- sdvplot-0.1.0/tests/baseline/test_baseline_plotnine_team_tiers_light.png +0 -0
- sdvplot-0.1.0/tests/baseline/test_baseline_plotnine_title_image.png +0 -0
- sdvplot-0.1.0/tests/baseline/test_baseline_scatter_with_logos.png +0 -0
- sdvplot-0.1.0/tests/baseline/test_baseline_team_tiers.png +0 -0
- sdvplot-0.1.0/tests/baseline/test_baseline_team_tiers_light.png +0 -0
- sdvplot-0.1.0/tests/baseline/test_baseline_title_image.png +0 -0
- sdvplot-0.1.0/tests/baseline/test_baseline_x_axis_logos.png +0 -0
- sdvplot-0.1.0/tests/baseline/test_baseline_y_axis_logos.png +0 -0
- sdvplot-0.1.0/tests/conftest.py +410 -0
- sdvplot-0.1.0/tests/fixtures/README.md +19 -0
- sdvplot-0.1.0/tests/fixtures/ggpath_ref_lines.csv +15 -0
- sdvplot-0.1.0/tests/fixtures/marks.csv +16 -0
- sdvplot-0.1.0/tests/fixtures/nba_shotchartdetail_2023.csv +41 -0
- sdvplot-0.1.0/tests/fixtures/sdvplotr_court_coords.csv +41 -0
- sdvplot-0.1.0/tests/fixtures/sdvplotr_logo_history.csv +402 -0
- sdvplot-0.1.0/tests/gt_export_fakes.py +46 -0
- sdvplot-0.1.0/tests/gt_frames.py +14 -0
- sdvplot-0.1.0/tests/gt_html.py +25 -0
- sdvplot-0.1.0/tests/test_adapters_live.py +38 -0
- sdvplot-0.1.0/tests/test_altair.py +232 -0
- sdvplot-0.1.0/tests/test_api.py +404 -0
- sdvplot-0.1.0/tests/test_automation_example.py +926 -0
- sdvplot-0.1.0/tests/test_bokeh.py +86 -0
- sdvplot-0.1.0/tests/test_build_index.py +641 -0
- sdvplot-0.1.0/tests/test_cache.py +768 -0
- sdvplot-0.1.0/tests/test_colors.py +56 -0
- sdvplot-0.1.0/tests/test_commit_msg_hook.py +60 -0
- sdvplot-0.1.0/tests/test_compat_cartopy.py +83 -0
- sdvplot-0.1.0/tests/test_compat_charts.py +100 -0
- sdvplot-0.1.0/tests/test_compat_geo.py +49 -0
- sdvplot-0.1.0/tests/test_compat_matrix.py +18 -0
- sdvplot-0.1.0/tests/test_compat_style.py +68 -0
- sdvplot-0.1.0/tests/test_compat_text.py +68 -0
- sdvplot-0.1.0/tests/test_contrast.py +67 -0
- sdvplot-0.1.0/tests/test_court.py +214 -0
- sdvplot-0.1.0/tests/test_deprecate.py +60 -0
- sdvplot-0.1.0/tests/test_dispatch.py +557 -0
- sdvplot-0.1.0/tests/test_fetch_sources.py +438 -0
- sdvplot-0.1.0/tests/test_folium.py +75 -0
- sdvplot-0.1.0/tests/test_gen_docs.py +411 -0
- sdvplot-0.1.0/tests/test_great_tables.py +438 -0
- sdvplot-0.1.0/tests/test_gt_cells_borders.py +238 -0
- sdvplot-0.1.0/tests/test_gt_cells_colors.py +240 -0
- sdvplot-0.1.0/tests/test_gt_cells_format.py +180 -0
- sdvplot-0.1.0/tests/test_gt_cells_rows.py +200 -0
- sdvplot-0.1.0/tests/test_gt_export_batch.py +124 -0
- sdvplot-0.1.0/tests/test_gt_export_compose.py +152 -0
- sdvplot-0.1.0/tests/test_gt_export_pixels.py +99 -0
- sdvplot-0.1.0/tests/test_gt_export_render.py +214 -0
- sdvplot-0.1.0/tests/test_gt_export_save.py +87 -0
- sdvplot-0.1.0/tests/test_gt_layout_exports.py +30 -0
- sdvplot-0.1.0/tests/test_gt_layout_notes.py +97 -0
- sdvplot-0.1.0/tests/test_gt_layout_reshape.py +121 -0
- sdvplot-0.1.0/tests/test_gt_layout_rows.py +162 -0
- sdvplot-0.1.0/tests/test_gt_layout_scales.py +215 -0
- sdvplot-0.1.0/tests/test_gt_layout_titles.py +115 -0
- sdvplot-0.1.0/tests/test_gt_repr_important.py +158 -0
- sdvplot-0.1.0/tests/test_gt_themes.py +583 -0
- sdvplot-0.1.0/tests/test_harness.py +17 -0
- sdvplot-0.1.0/tests/test_headshots.py +127 -0
- sdvplot-0.1.0/tests/test_holoviews.py +73 -0
- sdvplot-0.1.0/tests/test_home_figures.py +104 -0
- sdvplot-0.1.0/tests/test_image_loaders.py +114 -0
- sdvplot-0.1.0/tests/test_images.py +312 -0
- sdvplot-0.1.0/tests/test_images_baseline.py +187 -0
- sdvplot-0.1.0/tests/test_import_time.py +16 -0
- sdvplot-0.1.0/tests/test_index.py +44 -0
- sdvplot-0.1.0/tests/test_input_validation.py +260 -0
- sdvplot-0.1.0/tests/test_manifest.py +66 -0
- sdvplot-0.1.0/tests/test_marks.py +167 -0
- sdvplot-0.1.0/tests/test_matplotlib.py +345 -0
- sdvplot-0.1.0/tests/test_normalize.py +16 -0
- sdvplot-0.1.0/tests/test_notebooks.py +698 -0
- sdvplot-0.1.0/tests/test_package.py +14 -0
- sdvplot-0.1.0/tests/test_placement.py +119 -0
- sdvplot-0.1.0/tests/test_plotly.py +248 -0
- sdvplot-0.1.0/tests/test_plotnine.py +366 -0
- sdvplot-0.1.0/tests/test_plottable.py +37 -0
- sdvplot-0.1.0/tests/test_pygal.py +271 -0
- sdvplot-0.1.0/tests/test_reactable.py +154 -0
- sdvplot-0.1.0/tests/test_readme.py +29 -0
- sdvplot-0.1.0/tests/test_real_index.py +384 -0
- sdvplot-0.1.0/tests/test_repo_files.py +184 -0
- sdvplot-0.1.0/tests/test_resolve.py +123 -0
- sdvplot-0.1.0/tests/test_resolve_series.py +70 -0
- sdvplot-0.1.0/tests/test_sdvplotr_parity.py +154 -0
- sdvplot-0.1.0/tests/test_submodule_examples.py +327 -0
- sdvplot-0.1.0/tests/test_suggest.py +15 -0
- sdvplot-0.1.0/tests/test_surface.py +176 -0
- sdvplot-0.1.0/tests/test_table_contract.py +106 -0
- sdvplot-0.1.0/tests/test_tables.py +59 -0
- sdvplot-0.1.0/tests/test_team_tiers.py +346 -0
- sdvplot-0.1.0/tests/test_title_image.py +276 -0
- sdvplot-0.1.0/tests/test_types.py +89 -0
- sdvplot-0.1.0/tests/test_url_safety.py +220 -0
- sdvplot-0.1.0/tests/test_warning_order.py +44 -0
- sdvplot-0.1.0/tests/test_web.py +62 -0
- sdvplot-0.1.0/tests/test_web_live.py +30 -0
- 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
|
sdvplot-0.1.0/.gitignore
ADDED
|
@@ -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
|