sdvplot 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sdvplot/__init__.py +68 -0
- sdvplot/_cache.py +484 -0
- sdvplot/_colors.py +206 -0
- sdvplot/_contrast.py +60 -0
- sdvplot/_court.py +110 -0
- sdvplot/_deprecate.py +39 -0
- sdvplot/_dispatch.py +342 -0
- sdvplot/_errors.py +93 -0
- sdvplot/_headshots.py +131 -0
- sdvplot/_images.py +292 -0
- sdvplot/_index.py +178 -0
- sdvplot/_lazy.py +15 -0
- sdvplot/_manifest.py +73 -0
- sdvplot/_marks.py +296 -0
- sdvplot/_normalize.py +82 -0
- sdvplot/_placement.py +168 -0
- sdvplot/_resolve.py +363 -0
- sdvplot/_surface.py +220 -0
- sdvplot/_tables.py +87 -0
- sdvplot/_tiers.py +135 -0
- sdvplot/_types.py +38 -0
- sdvplot/_versions.py +30 -0
- sdvplot/_web.py +61 -0
- sdvplot/altair.py +652 -0
- sdvplot/bokeh.py +323 -0
- sdvplot/data/INDEX_VERSION +1 -0
- sdvplot/data/aliases.parquet +0 -0
- sdvplot/data/teams.parquet +0 -0
- sdvplot/folium.py +316 -0
- sdvplot/great_tables/__init__.py +267 -0
- sdvplot/great_tables/_cells.py +1814 -0
- sdvplot/great_tables/_export.py +844 -0
- sdvplot/great_tables/_layout.py +1802 -0
- sdvplot/great_tables/_marks.py +798 -0
- sdvplot/great_tables/_themes.py +2134 -0
- sdvplot/holoviews.py +303 -0
- sdvplot/matplotlib.py +934 -0
- sdvplot/plotly.py +595 -0
- sdvplot/plotnine.py +1104 -0
- sdvplot/plottable.py +141 -0
- sdvplot/py.typed +0 -0
- sdvplot/pygal.py +416 -0
- sdvplot/reactable.py +488 -0
- sdvplot/testing.py +579 -0
- sdvplot/typing.py +21 -0
- sdvplot-0.1.0.dist-info/METADATA +188 -0
- sdvplot-0.1.0.dist-info/RECORD +49 -0
- sdvplot-0.1.0.dist-info/WHEEL +4 -0
- sdvplot-0.1.0.dist-info/licenses/LICENSE +21 -0
sdvplot/plottable.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""plottable columns of team logos, wordmarks and player headshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from sdvplot._errors import requires_extra
|
|
8
|
+
|
|
9
|
+
with requires_extra("plottable"):
|
|
10
|
+
from plottable import ColumnDefinition
|
|
11
|
+
|
|
12
|
+
from sdvplot._placement import place
|
|
13
|
+
from sdvplot.matplotlib import _image
|
|
14
|
+
|
|
15
|
+
__all__ = ["headshot_column", "logo_column"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def __dir__() -> list[str]: # dir() and tab completion show the public API only
|
|
19
|
+
return list(__all__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _cell(kind: str, league: str, season: Any, variant: str, id_system: str) -> Any:
|
|
23
|
+
def draw(ax: Any, value: Any, **_: Any) -> None:
|
|
24
|
+
ax.set_axis_off()
|
|
25
|
+
placements = place([0.0], [0.0], [value], league=league, season=season, kind=kind, variant=variant,
|
|
26
|
+
id_system=id_system) # fmt: skip
|
|
27
|
+
if placements:
|
|
28
|
+
ax.imshow(_image(placements[0]))
|
|
29
|
+
ax.set_aspect("equal")
|
|
30
|
+
ax._sdvplot_cell = placements[0].team_id
|
|
31
|
+
|
|
32
|
+
return draw
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def logo_column(
|
|
36
|
+
name: str,
|
|
37
|
+
*,
|
|
38
|
+
league: str,
|
|
39
|
+
season: Any = None,
|
|
40
|
+
variant: str = "default",
|
|
41
|
+
mark_type: str = "logo",
|
|
42
|
+
id_system: str = "auto",
|
|
43
|
+
**column_definition_kwargs: Any,
|
|
44
|
+
) -> ColumnDefinition:
|
|
45
|
+
"""A plottable column that shows each row's team as its logo (or wordmark).
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
name: The data column holding the teams.
|
|
49
|
+
league: The SDV league key, e.g. "nfl".
|
|
50
|
+
season: One season for every row.
|
|
51
|
+
variant: "default", "dark", or a named variant from ``marks()``.
|
|
52
|
+
mark_type: "logo" or "wordmark".
|
|
53
|
+
id_system: The id system of the column's values.
|
|
54
|
+
**column_definition_kwargs: Passed to ``plottable.ColumnDefinition`` (``title``, ``width``, ``group``, ...).
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
plottable.ColumnDefinition: The column definition; an unknown team leaves its cell blank, with an
|
|
58
|
+
SdvplotWarning.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
InputError: (a ValueError) When the table is drawn, if ``mark_type`` is not "logo" or "wordmark", ``league``,
|
|
62
|
+
``id_system`` or ``variant`` is unknown, or ``season`` is not a year or is outside the seasons sdvplot knows
|
|
63
|
+
for the league (the column definition itself is built without checking them).
|
|
64
|
+
OfflineError: When the table is drawn, if the logo manifest or a mark's image is neither cached nor downloadable
|
|
65
|
+
(a DownloadError, also an OSError, when the CDN answers with an error status; an IntegrityError when it
|
|
66
|
+
sends a file that does not match the manifest's sha256, or one PIL cannot decode).
|
|
67
|
+
UnsafeDownloadError: (an OSError) When the table is drawn, if a download is refused: larger than the byte cap,
|
|
68
|
+
past the deadline, or redirected away from https.
|
|
69
|
+
UnsafeCachePathError: (a ValueError) When the table is drawn, if the manifest's sha256 or extension for a mark
|
|
70
|
+
would put the file outside the cache directory.
|
|
71
|
+
OptionalDependencyError: When the table is drawn, if a mark is an SVG and the ``svg`` extra is not installed.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
::
|
|
75
|
+
|
|
76
|
+
from plottable import Table
|
|
77
|
+
from sdvplot.plottable import logo_column
|
|
78
|
+
import pandas as pd
|
|
79
|
+
|
|
80
|
+
df = pd.DataFrame(
|
|
81
|
+
{
|
|
82
|
+
"team": ["KC", "BUF", "BAL"],
|
|
83
|
+
"espn_id": ["3139477", "3918298", "3916387"],
|
|
84
|
+
"wins": [12, 10, 9],
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
Table(df, column_definitions=[logo_column("team", league="nfl", title="")])
|
|
89
|
+
|
|
90
|
+
See Also:
|
|
91
|
+
plottable: https://plottable.readthedocs.io/
|
|
92
|
+
"""
|
|
93
|
+
draw = _cell(mark_type, league, season, variant, id_system)
|
|
94
|
+
return ColumnDefinition(name=name, plot_fn=draw, **column_definition_kwargs)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def headshot_column(
|
|
98
|
+
name: str, *, league: str, id_system: str = "espn", **column_definition_kwargs: Any
|
|
99
|
+
) -> ColumnDefinition:
|
|
100
|
+
"""A plottable column that shows each row's player as a headshot.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
name: The data column holding the player ids.
|
|
104
|
+
league: The SDV league key, e.g. "nfl".
|
|
105
|
+
id_system: "espn" or "gsis" (NFL), as in ``headshot_url``.
|
|
106
|
+
**column_definition_kwargs: Passed to ``plottable.ColumnDefinition``.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
plottable.ColumnDefinition: The column definition; an unknown id leaves its cell blank, with an SdvplotWarning.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
InputError: (a ValueError) When the table is drawn, if ``league`` has no ESPN headshots or ``id_system`` is not
|
|
113
|
+
valid for it.
|
|
114
|
+
OfflineError: When the table is drawn, if a headshot, or with ``id_system="gsis"`` the nflverse player table, is
|
|
115
|
+
neither cached nor downloadable (a DownloadError, also an OSError, for an HTTP error status).
|
|
116
|
+
UnsafeDownloadError: (an OSError) When the table is drawn, if a download is refused: larger than the byte cap,
|
|
117
|
+
past the deadline, or redirected away from https.
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
::
|
|
121
|
+
|
|
122
|
+
import pandas as pd
|
|
123
|
+
from plottable import Table
|
|
124
|
+
from sdvplot.plottable import headshot_column
|
|
125
|
+
|
|
126
|
+
df = pd.DataFrame(
|
|
127
|
+
{
|
|
128
|
+
"team": ["KC", "BUF", "BAL"],
|
|
129
|
+
"espn_id": ["3139477", "3918298", "3916387"],
|
|
130
|
+
"wins": [12, 10, 9],
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
Table(df, column_definitions=[headshot_column("espn_id", league="nfl", title="")])
|
|
135
|
+
|
|
136
|
+
See Also:
|
|
137
|
+
sdvplotR geom_nfl_headshots(): https://sdvplotR.sportsdataverse.org/ ;
|
|
138
|
+
sdvplot.plottable.logo_column: the same with team logos
|
|
139
|
+
"""
|
|
140
|
+
draw = _cell("headshot", league, None, "default", id_system)
|
|
141
|
+
return ColumnDefinition(name=name, plot_fn=draw, **column_definition_kwargs)
|
sdvplot/py.typed
ADDED
|
File without changes
|
sdvplot/pygal.py
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""The pygal adapter: logos, wordmarks and headshots on pygal XY charts, plus team colors as a pygal Style.
|
|
2
|
+
|
|
3
|
+
pygal builds a new SVG tree on every render, so add_* records the marks on the chart and registers one xml filter
|
|
4
|
+
(``chart.add_xml_filter``, pygal's public hook) that appends one ``<image>`` per recorded mark to the plot overlay
|
|
5
|
+
group, above pygal's own dots, during the render. Positions come from ``chart.view``, the projection pygal uses for its
|
|
6
|
+
own dots, after pygal's own value adapters (dates become timestamps), so a logo sits exactly where pygal would draw that
|
|
7
|
+
point. Resolution and its warnings happen when add_* is called, not at render time.
|
|
8
|
+
|
|
9
|
+
Copy a chart with ``copy.deepcopy``: the copy renders the marks the chart had, and marks added to either one afterwards
|
|
10
|
+
stay on that one. ``copy.copy`` is not supported: a shallow copy shares pygal's own series and filters.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from sdvplot._errors import UnsupportedTargetError, requires_extra
|
|
18
|
+
|
|
19
|
+
with requires_extra("pygal"):
|
|
20
|
+
import pygal
|
|
21
|
+
from pygal.style import Style
|
|
22
|
+
|
|
23
|
+
from sdvplot._colors import team_colors
|
|
24
|
+
from sdvplot._placement import check_alpha, check_height, place
|
|
25
|
+
from sdvplot._resolve import _unpack
|
|
26
|
+
from sdvplot._types import IdSystem, Which
|
|
27
|
+
from sdvplot._web import aspect, image_src
|
|
28
|
+
|
|
29
|
+
_SUPPORTS_AXIS_LOGOS = False
|
|
30
|
+
_SUPPORTED = "pygal.XY, DateTimeLine, DateLine, TimeLine or TimeDeltaLine"
|
|
31
|
+
_HREF = "{http://www.w3.org/1999/xlink}href" # pygal writes its own links as xlink:href (SVG 1.1 renderers need it)
|
|
32
|
+
_MARK = "data-sdvplot-mark" # on each mark <image>: its index in the chart's _sdvplot_marks
|
|
33
|
+
|
|
34
|
+
__all__ = ["add_headshots", "add_logos", "add_wordmarks", "axis_logos", "team_style"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def __dir__() -> list[str]: # dir() and tab completion show the public API only
|
|
38
|
+
return list(__all__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _check_chart(chart: Any) -> None:
|
|
42
|
+
if not isinstance(chart, pygal.XY):
|
|
43
|
+
raise UnsupportedTargetError(
|
|
44
|
+
f"sdvplot.pygal draws on XY-family charts ({_SUPPORTED}); a {type(chart).__name__} chart places values by "
|
|
45
|
+
"category or angle, not at an (x, y) point"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _MarksFilter:
|
|
50
|
+
"""The xml filter of one chart: appends the chart's recorded marks to its plot overlay (pygal's dot layer).
|
|
51
|
+
|
|
52
|
+
An object rather than a closure, so ``copy.deepcopy`` points the copy's filter at the copy (through the memo) and
|
|
53
|
+
the chart still pickles.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, chart: Any) -> None:
|
|
57
|
+
self.chart = chart
|
|
58
|
+
|
|
59
|
+
def __call__(self, root: Any) -> Any:
|
|
60
|
+
chart = self.chart
|
|
61
|
+
view = getattr(chart, "view", None) # set during a render; absent when every series is empty
|
|
62
|
+
overlay = next((el for el in root.iter() if el.get("class") == "plot overlay"), None)
|
|
63
|
+
if view is None or overlay is None:
|
|
64
|
+
return root
|
|
65
|
+
ident = lambda v: v # noqa: E731
|
|
66
|
+
adapt = getattr(chart, "_adapt", None) or ident # pygal's value adapters, as applied to its own points
|
|
67
|
+
x_adapt = getattr(chart, "_x_adapt", None) or ident
|
|
68
|
+
for index, (_, x, y, height, _, src, ratio, alpha) in enumerate(vars(chart).get("_sdvplot_marks", ())):
|
|
69
|
+
cx, cy = view((adapt(x_adapt(x)), adapt(y)))
|
|
70
|
+
if cx is None or cy is None or not (0 <= cx <= view.width and 0 <= cy <= view.height):
|
|
71
|
+
continue # outside the plot, like a matplotlib annotation outside the limits
|
|
72
|
+
h = height * view.height
|
|
73
|
+
w = h * ratio
|
|
74
|
+
attrs = {
|
|
75
|
+
_HREF: src, "x": f"{cx - w / 2:.3f}", "y": f"{cy - h / 2:.3f}", "width": f"{w:.3f}",
|
|
76
|
+
"height": f"{h:.3f}", "preserveAspectRatio": "xMidYMid meet", "opacity": f"{alpha:g}",
|
|
77
|
+
"pointer-events": "none", # hovering still reaches pygal's dot (and its tooltip) underneath
|
|
78
|
+
_MARK: str(index), # which recorded mark this is: lets _drawn_marks measure the rendered image
|
|
79
|
+
} # fmt: skip
|
|
80
|
+
overlay.append(overlay.makeelement("image", attrs))
|
|
81
|
+
return root
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _add(
|
|
85
|
+
chart: Any,
|
|
86
|
+
x: Any,
|
|
87
|
+
y: Any,
|
|
88
|
+
teams: Any,
|
|
89
|
+
*,
|
|
90
|
+
kind: str,
|
|
91
|
+
league: str,
|
|
92
|
+
season: Any,
|
|
93
|
+
height: float,
|
|
94
|
+
alpha: float,
|
|
95
|
+
variant: str,
|
|
96
|
+
embed: bool,
|
|
97
|
+
id_system: str,
|
|
98
|
+
) -> Any:
|
|
99
|
+
_check_chart(chart)
|
|
100
|
+
h, a = check_height(height), check_alpha(alpha)
|
|
101
|
+
placements = place(x, y, teams, league=league, season=season, kind=kind, variant=variant, id_system=id_system)
|
|
102
|
+
sources: dict[str, str] = {}
|
|
103
|
+
marks = []
|
|
104
|
+
for p in placements:
|
|
105
|
+
if p.url not in sources:
|
|
106
|
+
sources[p.url] = image_src(p, embed=embed) # with embed=True this reads the cache now, not at render
|
|
107
|
+
marks.append((p.team_id, p.x, p.y, h, p.url, sources[p.url], aspect(p), a))
|
|
108
|
+
state = vars(chart)
|
|
109
|
+
state["_sdvplot_marks"] = [*state.get("_sdvplot_marks", ()), *marks] # a new list, never one another chart holds
|
|
110
|
+
if not any(isinstance(f, _MarksFilter) and f.chart is chart for f in chart.xml_filters):
|
|
111
|
+
chart.add_xml_filter(_MarksFilter(chart)) # one per chart: it draws every add_* call's marks
|
|
112
|
+
return chart
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def add_logos(
|
|
116
|
+
chart: Any,
|
|
117
|
+
x: Any,
|
|
118
|
+
y: Any,
|
|
119
|
+
teams: Any,
|
|
120
|
+
*,
|
|
121
|
+
league: str,
|
|
122
|
+
season: Any = None,
|
|
123
|
+
height: float = 0.1,
|
|
124
|
+
alpha: float = 1,
|
|
125
|
+
variant: str = "default",
|
|
126
|
+
embed: bool = False,
|
|
127
|
+
id_system: str = "auto",
|
|
128
|
+
) -> Any:
|
|
129
|
+
"""Draw each team's logo centred on its (x, y) point of a pygal XY chart, in every render of the chart.
|
|
130
|
+
|
|
131
|
+
Copy the chart with ``copy.deepcopy`` (the copy keeps the logos); a shallow copy shares pygal's own series and
|
|
132
|
+
filters.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
chart: A pygal ``XY`` chart (``XY(stroke=False)`` for a scatter) or a ``DateTimeLine``/``DateLine``/
|
|
136
|
+
``TimeLine``/``TimeDeltaLine``.
|
|
137
|
+
x: The points' x positions in the chart's units (numbers, or dates for the time charts; read by position).
|
|
138
|
+
y: The points' y positions, the same length as ``x``.
|
|
139
|
+
teams: The team for each point, in any id system ``resolve()`` understands.
|
|
140
|
+
league: The SDV league key, e.g. "nfl".
|
|
141
|
+
season: One season, or one per point, to pick each team's mark for that era.
|
|
142
|
+
height: The logo height as a fraction of the plot height, in (0, 1].
|
|
143
|
+
alpha: Opacity, 0 to 1.
|
|
144
|
+
variant: "default", "dark", or a named variant from ``marks()``.
|
|
145
|
+
embed: True puts the image bytes in the SVG as data URIs, so the SVG and ``render_to_png()`` need no network.
|
|
146
|
+
id_system: The id system of ``teams``; "auto" tries each in order.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
object: ``chart`` itself, with a filter that draws the logos whenever it renders.
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
InputError: (a ValueError) If ``height`` or ``alpha`` is out of range, ``league``, ``id_system`` or ``variant``
|
|
153
|
+
is unknown, or ``season`` is not a year or is outside the seasons sdvplot knows for the league.
|
|
154
|
+
ValueError: If ``x``/``y``/``teams`` differ in length.
|
|
155
|
+
UnsupportedTargetError: (a TypeError) If ``chart`` is not an XY-family chart (Bar, Line and Pie place values by
|
|
156
|
+
category or angle).
|
|
157
|
+
OfflineError: If the logo manifest, or with ``embed=True`` a mark's image, is neither cached nor downloadable (a
|
|
158
|
+
DownloadError, also an OSError, when the CDN answers with an error status; an IntegrityError when it sends a
|
|
159
|
+
file that does not match the manifest's sha256).
|
|
160
|
+
UnsafeDownloadError: (an OSError) If a download is refused: larger than the byte cap, past the deadline, or
|
|
161
|
+
redirected away from https.
|
|
162
|
+
UnsafeCachePathError: (a ValueError) If ``embed=True`` and the manifest's sha256 or extension for a mark would
|
|
163
|
+
put the file outside the cache directory.
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
::
|
|
167
|
+
|
|
168
|
+
import pygal
|
|
169
|
+
import sdvplot
|
|
170
|
+
|
|
171
|
+
chart = pygal.XY(stroke=False)
|
|
172
|
+
chart.add("games", [(10, -3), (20, -7)])
|
|
173
|
+
sdvplot.add_logos(chart, [10, 20], [-3, -7], ["KC", "BUF"], league="nfl", height=0.15)
|
|
174
|
+
chart.render_to_file("chart.svg")
|
|
175
|
+
|
|
176
|
+
See Also:
|
|
177
|
+
sdvplotR geom_nfl_logos(): https://sdvplotR.sportsdataverse.org/ ;
|
|
178
|
+
pygal: https://www.pygal.org/
|
|
179
|
+
"""
|
|
180
|
+
return _add(
|
|
181
|
+
chart, x, y, teams, kind="logo", league=league, season=season, height=height, alpha=alpha,
|
|
182
|
+
variant=variant, embed=embed, id_system=id_system,
|
|
183
|
+
) # fmt: skip
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def add_wordmarks(
|
|
187
|
+
chart: Any,
|
|
188
|
+
x: Any,
|
|
189
|
+
y: Any,
|
|
190
|
+
teams: Any,
|
|
191
|
+
*,
|
|
192
|
+
league: str,
|
|
193
|
+
season: Any = None,
|
|
194
|
+
height: float = 0.1,
|
|
195
|
+
alpha: float = 1,
|
|
196
|
+
variant: str = "default",
|
|
197
|
+
embed: bool = False,
|
|
198
|
+
id_system: str = "auto",
|
|
199
|
+
) -> Any:
|
|
200
|
+
"""Draw each team's wordmark centred on its (x, y) point of a pygal XY chart, in every render of the chart.
|
|
201
|
+
|
|
202
|
+
Copy the chart with ``copy.deepcopy`` (the copy keeps the wordmarks); a shallow copy shares pygal's own series and
|
|
203
|
+
filters.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
chart: A pygal ``XY`` chart or one of its time variants (``DateTimeLine``, ``DateLine``, ...).
|
|
207
|
+
x: The points' x positions in the chart's units (read by position).
|
|
208
|
+
y: The points' y positions, the same length as ``x``.
|
|
209
|
+
teams: The team for each point, in any id system ``resolve()`` understands.
|
|
210
|
+
league: The SDV league key, e.g. "nfl".
|
|
211
|
+
season: One season, or one per point.
|
|
212
|
+
height: The wordmark height as a fraction of the plot height, in (0, 1].
|
|
213
|
+
alpha: Opacity, 0 to 1.
|
|
214
|
+
variant: "default", "dark", or a named variant from ``marks()``.
|
|
215
|
+
embed: True puts the image bytes in the SVG as data URIs (no network when rendering).
|
|
216
|
+
id_system: The id system of ``teams``; "auto" tries each in order.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
object: ``chart`` itself, with a filter that draws the wordmarks whenever it renders.
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
InputError: (a ValueError) If ``height`` or ``alpha`` is out of range, ``league``, ``id_system`` or ``variant``
|
|
223
|
+
is unknown, or ``season`` is not a year or is outside the seasons sdvplot knows for the league.
|
|
224
|
+
ValueError: If the inputs differ in length.
|
|
225
|
+
UnsupportedTargetError: (a TypeError) If ``chart`` is not an XY-family chart.
|
|
226
|
+
OfflineError: If the logo manifest, or with ``embed=True`` a mark's image, is neither cached nor downloadable (a
|
|
227
|
+
DownloadError, also an OSError, when the CDN answers with an error status; an IntegrityError when it sends a
|
|
228
|
+
file that does not match the manifest's sha256).
|
|
229
|
+
UnsafeDownloadError: (an OSError) If a download is refused: larger than the byte cap, past the deadline, or
|
|
230
|
+
redirected away from https.
|
|
231
|
+
UnsafeCachePathError: (a ValueError) If ``embed=True`` and the manifest's sha256 or extension for a mark would
|
|
232
|
+
put the file outside the cache directory.
|
|
233
|
+
|
|
234
|
+
Example:
|
|
235
|
+
::
|
|
236
|
+
|
|
237
|
+
import pygal
|
|
238
|
+
import sdvplot
|
|
239
|
+
|
|
240
|
+
chart = pygal.XY(stroke=False)
|
|
241
|
+
chart.add("games", [(10, -3)])
|
|
242
|
+
sdvplot.add_wordmarks(chart, [10], [-3], ["KC"], league="nfl", height=0.08)
|
|
243
|
+
|
|
244
|
+
See Also:
|
|
245
|
+
sdvplotR geom_nfl_wordmarks(): https://sdvplotR.sportsdataverse.org/
|
|
246
|
+
"""
|
|
247
|
+
return _add(
|
|
248
|
+
chart, x, y, teams, kind="wordmark", league=league, season=season, height=height, alpha=alpha,
|
|
249
|
+
variant=variant, embed=embed, id_system=id_system,
|
|
250
|
+
) # fmt: skip
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def add_headshots(
|
|
254
|
+
chart: Any,
|
|
255
|
+
x: Any,
|
|
256
|
+
y: Any,
|
|
257
|
+
players: Any,
|
|
258
|
+
*,
|
|
259
|
+
league: str,
|
|
260
|
+
height: float = 0.1,
|
|
261
|
+
alpha: float = 1,
|
|
262
|
+
embed: bool = False,
|
|
263
|
+
id_system: str = "espn",
|
|
264
|
+
) -> Any:
|
|
265
|
+
"""Draw each player's headshot centred on its (x, y) point of a pygal XY chart, in every render of the chart.
|
|
266
|
+
|
|
267
|
+
Copy the chart with ``copy.deepcopy`` (the copy keeps the headshots); a shallow copy shares pygal's own series and
|
|
268
|
+
filters.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
chart: A pygal ``XY`` chart or one of its time variants (``DateTimeLine``, ``DateLine``, ...).
|
|
272
|
+
x: The points' x positions in the chart's units (read by position).
|
|
273
|
+
y: The points' y positions, the same length as ``x``.
|
|
274
|
+
players: The player id for each point.
|
|
275
|
+
league: The SDV league key, e.g. "nfl".
|
|
276
|
+
height: The headshot height as a fraction of the plot height, in (0, 1].
|
|
277
|
+
alpha: Opacity, 0 to 1.
|
|
278
|
+
embed: True puts the image bytes in the SVG as data URIs (no network when rendering).
|
|
279
|
+
id_system: "espn" (ESPN athlete ids) or "gsis" (NFL), as in ``headshot_url``.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
object: ``chart`` itself, with a filter that draws the headshots whenever it renders.
|
|
283
|
+
|
|
284
|
+
Raises:
|
|
285
|
+
InputError: (a ValueError) If ``height`` or ``alpha`` is out of range, ``league`` has no ESPN headshots, or
|
|
286
|
+
``id_system`` is not valid for ``league``.
|
|
287
|
+
ValueError: If the inputs differ in length.
|
|
288
|
+
UnsupportedTargetError: (a TypeError) If ``chart`` is not an XY-family chart.
|
|
289
|
+
OfflineError: If the nflverse player table (``id_system="gsis"``), or with ``embed=True`` a headshot, is neither
|
|
290
|
+
cached nor downloadable (a DownloadError, also an OSError, for an HTTP error status).
|
|
291
|
+
UnsafeDownloadError: (an OSError) If a download is refused: larger than the byte cap, past the deadline, or
|
|
292
|
+
redirected away from https.
|
|
293
|
+
|
|
294
|
+
Example:
|
|
295
|
+
::
|
|
296
|
+
|
|
297
|
+
import pygal
|
|
298
|
+
import sdvplot
|
|
299
|
+
|
|
300
|
+
chart = pygal.XY(stroke=False)
|
|
301
|
+
chart.add("passing", [(4.2, 0.31)])
|
|
302
|
+
sdvplot.add_headshots(chart, [4.2], [0.31], ["3139477"], league="nfl", height=0.15)
|
|
303
|
+
|
|
304
|
+
See Also:
|
|
305
|
+
sdvplotR geom_nfl_headshots(): https://sdvplotR.sportsdataverse.org/
|
|
306
|
+
"""
|
|
307
|
+
return _add(
|
|
308
|
+
chart, x, y, players, kind="headshot", league=league, season=None, height=height, alpha=alpha,
|
|
309
|
+
variant="default", embed=embed, id_system=id_system,
|
|
310
|
+
) # fmt: skip
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def axis_logos(chart: Any, axis: str, **kwargs: Any) -> Any:
|
|
314
|
+
"""Not supported: pygal draws axis labels as text nodes, so sdvplot.pygal cannot put logos in their place.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
chart: A pygal chart.
|
|
318
|
+
axis: "x" or "y".
|
|
319
|
+
**kwargs: The arguments the other adapters take (``league``, ``height``, ...); ignored.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
object: Never returns.
|
|
323
|
+
|
|
324
|
+
Raises:
|
|
325
|
+
UnsupportedTargetError: (a TypeError) Always.
|
|
326
|
+
|
|
327
|
+
Example:
|
|
328
|
+
::
|
|
329
|
+
|
|
330
|
+
import pygal
|
|
331
|
+
import sdvplot.pygal
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
sdvplot.pygal.axis_logos(pygal.Bar(), "x", league="nfl")
|
|
335
|
+
except TypeError:
|
|
336
|
+
pass # use matplotlib, plotnine or Plotly for axis logos
|
|
337
|
+
|
|
338
|
+
See Also:
|
|
339
|
+
sdvplot.matplotlib.axis_logos(): https://sdvplot.sportsdataverse.org/
|
|
340
|
+
"""
|
|
341
|
+
raise UnsupportedTargetError("sdvplot.pygal does not draw axis logos: pygal axis labels are text nodes")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def team_style(
|
|
345
|
+
teams: Any,
|
|
346
|
+
*,
|
|
347
|
+
league: str,
|
|
348
|
+
which: Which = "primary",
|
|
349
|
+
season: Any = None,
|
|
350
|
+
id_system: IdSystem = "auto",
|
|
351
|
+
strict: bool = False,
|
|
352
|
+
**style_kwargs: Any,
|
|
353
|
+
) -> Style:
|
|
354
|
+
"""A pygal Style whose series colors are the teams' colors, in the order the series are added.
|
|
355
|
+
|
|
356
|
+
A team that does not resolve keeps pygal's default color for its position (with one SdvplotWarning), so the
|
|
357
|
+
other series keep theirs.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
teams: One team per series, in series order (or one team, for one series), in any id system ``resolve()``
|
|
361
|
+
understands.
|
|
362
|
+
league: The SDV league key, e.g. "nfl".
|
|
363
|
+
which: "primary" or "secondary".
|
|
364
|
+
season: One season, or one per team.
|
|
365
|
+
id_system: The id system of ``teams``: ``"auto"`` (the default) tries them in order; pass ``"nhl_id"`` for NHL
|
|
366
|
+
stats ids, which ``"auto"`` never tries.
|
|
367
|
+
strict: Raise ``UnresolvedTeamError`` for a team that does not resolve, instead of keeping pygal's default
|
|
368
|
+
color for it with one ``SdvplotWarning``.
|
|
369
|
+
**style_kwargs: Any other ``pygal.style.Style`` option (``background``, ``font_family``, ...).
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
pygal.style.Style: A style for ``pygal.XY(style=...)`` or any other pygal chart.
|
|
373
|
+
|
|
374
|
+
Raises:
|
|
375
|
+
TypeError: If ``teams`` is not a scalar, list, tuple, numpy array, or pandas/polars Series.
|
|
376
|
+
InputError: (a ValueError) If ``league`` or ``id_system`` is unknown, ``which`` is not "primary"/"secondary", or
|
|
377
|
+
``season`` is not a year or is outside the seasons sdvplot knows for the league (or a list whose length does
|
|
378
|
+
not match the teams).
|
|
379
|
+
UnresolvedTeamError: (a ValueError) With ``strict=True``, for a team that does not resolve.
|
|
380
|
+
|
|
381
|
+
Example:
|
|
382
|
+
::
|
|
383
|
+
|
|
384
|
+
import pygal
|
|
385
|
+
import sdvplot.pygal
|
|
386
|
+
|
|
387
|
+
chart = pygal.Bar(style=sdvplot.pygal.team_style(["KC", "BUF"], league="nfl"))
|
|
388
|
+
chart.add("KC", [12])
|
|
389
|
+
chart.add("BUF", [10])
|
|
390
|
+
|
|
391
|
+
See Also:
|
|
392
|
+
sdvplot.team_colors(): https://sdvplot.sportsdataverse.org/ ;
|
|
393
|
+
pygal styles: https://www.pygal.org/en/stable/documentation/styles.html
|
|
394
|
+
"""
|
|
395
|
+
defaults = Style.colors
|
|
396
|
+
colors = team_colors( # a bare "KC" is one team
|
|
397
|
+
league, _unpack(teams)[0], which=which, season=season, id_system=id_system, strict=strict
|
|
398
|
+
)
|
|
399
|
+
return Style(colors=tuple(c or defaults[i % len(defaults)] for i, c in enumerate(colors)), **style_kwargs)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _drawn_marks(chart: Any) -> list[tuple[Any, ...]]:
|
|
403
|
+
"""Test hook: render the chart, then (team_id, x, y, height, url) for each mark image in the SVG, in draw order;
|
|
404
|
+
height = the image's height attribute / the plot area's height."""
|
|
405
|
+
recorded = vars(chart).get("_sdvplot_marks", ())
|
|
406
|
+
root = chart.render_tree()
|
|
407
|
+
images = [el for el in root.iter() if el.get(_MARK) is not None]
|
|
408
|
+
if not images:
|
|
409
|
+
return []
|
|
410
|
+
plot = next(el for el in root.iter() if el.get("class") == "plot")
|
|
411
|
+
plot_h = float(next(el for el in plot if el.get("class") == "background").get("height"))
|
|
412
|
+
out = []
|
|
413
|
+
for el in images:
|
|
414
|
+
team_id, x, y, _, url = recorded[int(el.get(_MARK))][:5]
|
|
415
|
+
out.append((team_id, x, y, float(el.get("height")) / plot_h, url))
|
|
416
|
+
return out
|