Skip to content

Commit 0130f77

Browse files
committed
Use a dataclass instead of a dict as a return type for boxplot()
This is a prove of concept. Backward compatibility is achieved though a collections.abc.Mapping mixin. Note though, that I did not go as far as MutableMapping. If someone has written to the returned dict, they would still be broken. open issues: - documentation incomplete - should switch usages in the matplotlib code to attributes - Where to put the dataclass? cbook or axes? - should we use silent_list to make the repr a bit nicer?
1 parent b6f7a68 commit 0130f77

File tree

7 files changed

+83
-16
lines changed

7 files changed

+83
-16
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
``boxplot()`` returns a dataclass instead of a dict
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
The dataclass still supports dict-like access for backwards compatibility.

galleries/examples/statistics/boxplot_color.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
labels=labels) # will be used to label x-ticks
3030

3131
# fill with colors
32-
for patch, color in zip(bplot['boxes'], colors):
32+
for patch, color in zip(bplot.boxes, colors):
3333
patch.set_facecolor(color)
3434

3535
plt.show()

galleries/examples/statistics/boxplot_demo.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,9 @@
108108
fig.subplots_adjust(left=0.075, right=0.95, top=0.9, bottom=0.25)
109109

110110
bp = ax1.boxplot(data, notch=False, sym='+', vert=True, whis=1.5)
111-
plt.setp(bp['boxes'], color='black')
112-
plt.setp(bp['whiskers'], color='black')
113-
plt.setp(bp['fliers'], color='red', marker='+')
111+
plt.setp(bp.boxes, color='black')
112+
plt.setp(bp.whiskers, color='black')
113+
plt.setp(bp.fliers, color='red', marker='+')
114114

115115
# Add a horizontal grid to the plot, but make it very light in color
116116
# so we can use it for reading data values but not be distracting
@@ -229,8 +229,8 @@ def fake_bootstrapper(n):
229229

230230
ax.set_xlabel('treatment')
231231
ax.set_ylabel('response')
232-
plt.setp(bp['whiskers'], color='k', linestyle='-')
233-
plt.setp(bp['fliers'], markersize=3.0)
232+
plt.setp(bp.whiskers, color='k', linestyle='-')
233+
plt.setp(bp.fliers, markersize=3.0)
234234
plt.show()
235235

236236

lib/matplotlib/axes/_axes.py

+20-10
Original file line numberDiff line numberDiff line change
@@ -3908,10 +3908,10 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None,
39083908
39093909
Returns
39103910
-------
3911-
dict
3912-
A dictionary mapping each component of the boxplot to a list
3913-
of the `.Line2D` instances created. That dictionary has the
3914-
following keys (assuming vertical boxplots):
3911+
BoxplotArtists
3912+
A dataclass mapping each component of the boxplot to a list
3913+
of the `.Line2D` instances created. The dataclass has the
3914+
following attributes (assuming vertical boxplots):
39153915
39163916
- ``boxes``: the main body of the boxplot showing the
39173917
quartiles and the median's confidence intervals if
@@ -3930,6 +3930,10 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None,
39303930
39313931
- ``means``: points or lines representing the means.
39323932
3933+
.. versionchanged:: 3.9
3934+
Formerly, a dict was returned. The return value is now a dataclass, that
3935+
still supports dict-like access for backward-compatibility.
3936+
39333937
Other Parameters
39343938
----------------
39353939
showcaps : bool, default: :rc:`boxplot.showcaps`
@@ -4171,10 +4175,10 @@ def bxp(self, bxpstats, positions=None, widths=None, vert=True,
41714175
41724176
Returns
41734177
-------
4174-
dict
4175-
A dictionary mapping each component of the boxplot to a list
4176-
of the `.Line2D` instances created. That dictionary has the
4177-
following keys (assuming vertical boxplots):
4178+
BoxplotArtists
4179+
A dataclass mapping each component of the boxplot to a list
4180+
of the `.Line2D` instances created. The dataclass has the
4181+
following attributes (assuming vertical boxplots):
41784182
41794183
- ``boxes``: main bodies of the boxplot showing the quartiles, and
41804184
the median's confidence intervals if enabled.
@@ -4184,6 +4188,10 @@ def bxp(self, bxpstats, positions=None, widths=None, vert=True,
41844188
- ``fliers``: points representing data beyond the whiskers (fliers).
41854189
- ``means``: points or lines representing the means.
41864190
4191+
.. versionchanged:: 3.9
4192+
Formerly, a dict was returned. The return value is now a dataclass, that
4193+
still supports dict-like access for backward-compatibility.
4194+
41874195
See Also
41884196
--------
41894197
boxplot : Draw a boxplot from data instead of pre-computed statistics.
@@ -4390,8 +4398,10 @@ def do_patch(xs, ys, **kwargs):
43904398

43914399
self._request_autoscale_view()
43924400

4393-
return dict(whiskers=whiskers, caps=caps, boxes=boxes,
4394-
medians=medians, fliers=fliers, means=means)
4401+
return cbook.BoxplotArtists(
4402+
whiskers=whiskers, caps=caps, boxes=boxes,
4403+
medians=medians, fliers=fliers, means=means
4404+
)
43954405

43964406
@staticmethod
43974407
def _parse_scatter_color_args(c, edgecolors, kwargs, xsize,

lib/matplotlib/cbook.py

+28
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import collections
77
import collections.abc
88
import contextlib
9+
import dataclasses
910
import functools
1011
import gzip
1112
import itertools
@@ -19,6 +20,7 @@
1920
import time
2021
import traceback
2122
import types
23+
from typing import Union
2224
import weakref
2325

2426
import numpy as np
@@ -1126,6 +1128,32 @@ def _broadcast_with_masks(*args, compress=False):
11261128
return inputs
11271129

11281130

1131+
@dataclasses.dataclass(frozen=True)
1132+
class BoxplotArtists(collections.abc.Mapping):
1133+
"""
1134+
Collection of the artists created by `~.Axes.boxplot`.
1135+
1136+
For backward compatibility with the previously used dict representation,
1137+
this can alternatively be accessed like a (read-only) dict. However,
1138+
dict-like access is discouraged.
1139+
"""
1140+
boxes: Union[list["matplotlib.lines.Line2D"], list["matplotlib.patches.Patch"]]
1141+
medians: list["matplotlib.lines.Line2D"]
1142+
whiskers: list["matplotlib.lines.Line2D"]
1143+
caps: list["matplotlib.lines.Line2D"]
1144+
fliers: list["matplotlib.lines.Line2D"]
1145+
means: list["matplotlib.lines.Line2D"]
1146+
1147+
def __getitem__(self, key):
1148+
return getattr(self, key)
1149+
1150+
def __iter__(self):
1151+
return iter(self.__annotations__)
1152+
1153+
def __len__(self):
1154+
return len(self.__annotations__)
1155+
1156+
11291157
def boxplot_stats(X, whis=1.5, bootstrap=None, labels=None,
11301158
autorange=False):
11311159
r"""

lib/matplotlib/cbook.pyi

+17
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import collections.abc
22
from collections.abc import Callable, Collection, Generator, Iterable, Iterator
33
import contextlib
4+
import dataclasses
45
import os
56
from pathlib import Path
67

78
from matplotlib.artist import Artist
9+
from matplotlib.lines import Line2D
10+
from matplotlib.patches import Patch
811

912
import numpy as np
1013
from numpy.typing import ArrayLike
@@ -15,6 +18,7 @@ from typing import (
1518
IO,
1619
Literal,
1720
TypeVar,
21+
Union,
1822
overload,
1923
)
2024

@@ -131,6 +135,19 @@ class GrouperView(Generic[_T]):
131135
def simple_linear_interpolation(a: ArrayLike, steps: int) -> np.ndarray: ...
132136
def delete_masked_points(*args): ...
133137
def _broadcast_with_masks(*args: ArrayLike, compress: bool = ...) -> list[ArrayLike]: ...
138+
139+
@dataclasses.dataclass
140+
class BoxplotArtists(collections.abc.Mapping):
141+
boxes: Union[list[Line2D], list[Patch]]
142+
medians: list[Line2D]
143+
whiskers: list[Line2D]
144+
caps: list[Line2D]
145+
fliers: list[Line2D]
146+
means: list[Line2D]
147+
def __getitem__(self, key: str) -> Union[list[Line2D], list[Patch]]: ...
148+
def __iter__(self) -> Iterator[str]: ...
149+
def __len__(self) -> int: ...
150+
134151
def boxplot_stats(
135152
X: ArrayLike,
136153
whis: float | tuple[float, float] = ...,

lib/matplotlib/tests/test_axes.py

+9
Original file line numberDiff line numberDiff line change
@@ -3645,6 +3645,15 @@ def test_boxplot_mod_artist_after_plotting():
36453645
obj.set_color('green')
36463646

36473647

3648+
def test_boxplot_return_dict_and_dataclass():
3649+
# check that the returned BoxplotArtists works as a dataclass and as a dict
3650+
# i.e. bp['key'] and bp.key are equivalent
3651+
fig, ax = plt.subplots()
3652+
bp = ax.boxplot(np.linspace(0, 1, 11), sym="o")
3653+
for key in bp:
3654+
assert bp[key] is getattr(bp, key)
3655+
3656+
36483657
@image_comparison(['violinplot_vert_baseline.png',
36493658
'violinplot_vert_baseline.png'])
36503659
def test_vert_violinplot_baseline():

0 commit comments

Comments
 (0)