sdvplot 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. sdvplot/__init__.py +68 -0
  2. sdvplot/_cache.py +484 -0
  3. sdvplot/_colors.py +206 -0
  4. sdvplot/_contrast.py +60 -0
  5. sdvplot/_court.py +110 -0
  6. sdvplot/_deprecate.py +39 -0
  7. sdvplot/_dispatch.py +342 -0
  8. sdvplot/_errors.py +93 -0
  9. sdvplot/_headshots.py +131 -0
  10. sdvplot/_images.py +292 -0
  11. sdvplot/_index.py +178 -0
  12. sdvplot/_lazy.py +15 -0
  13. sdvplot/_manifest.py +73 -0
  14. sdvplot/_marks.py +296 -0
  15. sdvplot/_normalize.py +82 -0
  16. sdvplot/_placement.py +168 -0
  17. sdvplot/_resolve.py +363 -0
  18. sdvplot/_surface.py +220 -0
  19. sdvplot/_tables.py +87 -0
  20. sdvplot/_tiers.py +135 -0
  21. sdvplot/_types.py +38 -0
  22. sdvplot/_versions.py +30 -0
  23. sdvplot/_web.py +61 -0
  24. sdvplot/altair.py +652 -0
  25. sdvplot/bokeh.py +323 -0
  26. sdvplot/data/INDEX_VERSION +1 -0
  27. sdvplot/data/aliases.parquet +0 -0
  28. sdvplot/data/teams.parquet +0 -0
  29. sdvplot/folium.py +316 -0
  30. sdvplot/great_tables/__init__.py +267 -0
  31. sdvplot/great_tables/_cells.py +1814 -0
  32. sdvplot/great_tables/_export.py +844 -0
  33. sdvplot/great_tables/_layout.py +1802 -0
  34. sdvplot/great_tables/_marks.py +798 -0
  35. sdvplot/great_tables/_themes.py +2134 -0
  36. sdvplot/holoviews.py +303 -0
  37. sdvplot/matplotlib.py +934 -0
  38. sdvplot/plotly.py +595 -0
  39. sdvplot/plotnine.py +1104 -0
  40. sdvplot/plottable.py +141 -0
  41. sdvplot/py.typed +0 -0
  42. sdvplot/pygal.py +416 -0
  43. sdvplot/reactable.py +488 -0
  44. sdvplot/testing.py +579 -0
  45. sdvplot/typing.py +21 -0
  46. sdvplot-0.1.0.dist-info/METADATA +188 -0
  47. sdvplot-0.1.0.dist-info/RECORD +49 -0
  48. sdvplot-0.1.0.dist-info/WHEEL +4 -0
  49. sdvplot-0.1.0.dist-info/licenses/LICENSE +21 -0
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