Skip to content

Commit ef3d7e9

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 810a43b commit ef3d7e9

File tree

7 files changed

+82
-16
lines changed

7 files changed

+82
-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: True
@@ -4157,10 +4161,10 @@ def bxp(self, bxpstats, positions=None, widths=None, vert=True,
41574161
41584162
Returns
41594163
-------
4160-
dict
4161-
A dictionary mapping each component of the boxplot to a list
4162-
of the `.Line2D` instances created. That dictionary has the
4163-
following keys (assuming vertical boxplots):
4164+
BoxplotArtists
4165+
A dataclass mapping each component of the boxplot to a list
4166+
of the `.Line2D` instances created. The dataclass has the
4167+
following attributes (assuming vertical boxplots):
41644168
41654169
- ``boxes``: main bodies of the boxplot showing the quartiles, and
41664170
the median's confidence intervals if enabled.
@@ -4170,6 +4174,10 @@ def bxp(self, bxpstats, positions=None, widths=None, vert=True,
41704174
- ``fliers``: points representing data beyond the whiskers (fliers).
41714175
- ``means``: points or lines representing the means.
41724176
4177+
.. versionchanged:: 3.9
4178+
Formerly, a dict was returned. The return value is now a dataclass, that
4179+
still supports dict-like access for backward-compatibility.
4180+
41734181
Examples
41744182
--------
41754183
.. plot:: gallery/statistics/bxp.py
@@ -4376,8 +4384,10 @@ def do_patch(xs, ys, **kwargs):
43764384

43774385
self._request_autoscale_view()
43784386

4379-
return dict(whiskers=whiskers, caps=caps, boxes=boxes,
4380-
medians=medians, fliers=fliers, means=means)
4387+
return cbook.BoxplotArtists(
4388+
whiskers=whiskers, caps=caps, boxes=boxes,
4389+
medians=medians, fliers=fliers, means=means
4390+
)
43814391

43824392
@staticmethod
43834393
def _parse_scatter_color_args(c, edgecolors, kwargs, xsize,

lib/matplotlib/cbook.py

+27
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,31 @@ 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.
1138+
"""
1139+
boxes: Union[list["matplotlib.lines.Line2D"], list["matplotlib.patches.Patch"]]
1140+
medians: list["matplotlib.lines.Line2D"]
1141+
whiskers: list["matplotlib.lines.Line2D"]
1142+
caps: list["matplotlib.lines.Line2D"]
1143+
fliers: list["matplotlib.lines.Line2D"]
1144+
means: list["matplotlib.lines.Line2D"]
1145+
1146+
def __getitem__(self, key):
1147+
return getattr(self, key)
1148+
1149+
def __iter__(self):
1150+
return iter(self.__annotations__)
1151+
1152+
def __len__(self):
1153+
return len(self.__annotations__)
1154+
1155+
11291156
def boxplot_stats(X, whis=1.5, bootstrap=None, labels=None,
11301157
autorange=False):
11311158
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)