Skip to content

Commit 68edfcb

Browse files
committed
add legend support for boxplots
1 parent 76eaa96 commit 68edfcb

8 files changed

Lines changed: 185 additions & 52 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
``boxplot`` legend labels
2+
~~~~~~~~~~~~~~~~~~~~~~~~~
3+
The tick labels on `~.Axes.boxplot` were previously set with the `labels` parameter.
4+
This has been changed to `tick_labels` to be consistent with `~.Axes.bar` and to
5+
accommodate the newly introduced `label` parameter for the legend labels.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
Legend support for Boxplot
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
Boxplots now generate legend entries and can be labelled with the
4+
new `label` parameter. If a patch is passed to the box with `show_patch=True`,
5+
the legend gets its handle from the patch instead of the `.Line2D` object from
6+
the whiskers.
7+
The old `labels` parameter that was used for setting tick labels is deprecated
8+
and replaced with `tick_labels`.
9+
10+
.. plot::
11+
:include-source: true
12+
:alt: Example of creating 3 boxplots and assigning legend labels and tick labels with keywords.
13+
14+
import matplotlib.pyplot as plt
15+
import numpy as np
16+
17+
np.random.seed(19680801)
18+
fruit_weights = [
19+
np.random.normal(130, 10, size=100),
20+
np.random.normal(125, 20, size=100),
21+
np.random.normal(120, 30, size=100),
22+
]
23+
labels = ['peaches', 'oranges', 'tomatoes']
24+
colors = ['peachpuff', 'orange', 'tomato']
25+
tick_lb = ['A', 'B', 'C']
26+
27+
fig, ax = plt.subplots()
28+
ax.set_ylabel('fruit weight (g)')
29+
30+
bplot = ax.boxplot(fruit_weights,
31+
patch_artist=True, # fill with color
32+
tick_labels=tick_lb,
33+
label=labels)
34+
35+
# fill with colors
36+
for patch, color in zip(bplot['boxes'], colors):
37+
patch.set_facecolor(color)
38+
39+
ax.legend()
40+
plt.show()

‎lib/matplotlib/axes/_axes.py‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3765,15 +3765,16 @@ def apply_mask(arrays, mask):
37653765
return errorbar_container # (l0, caplines, barcols)
37663766

37673767
@_preprocess_data()
3768+
@_api.rename_parameter("3.9", "labels", "tick_labels")
37683769
def boxplot(self, x, notch=None, sym=None, vert=None, whis=None,
37693770
positions=None, widths=None, patch_artist=None,
37703771
bootstrap=None, usermedians=None, conf_intervals=None,
37713772
meanline=None, showmeans=None, showcaps=None,
37723773
showbox=None, showfliers=None, boxprops=None,
3773-
labels=None, flierprops=None, medianprops=None,
3774+
tick_labels=None, flierprops=None, medianprops=None,
37743775
meanprops=None, capprops=None, whiskerprops=None,
37753776
manage_ticks=True, autorange=False, zorder=None,
3776-
capwidths=None):
3777+
capwidths=None, label=None):
37773778
"""
37783779
Draw a box and whisker plot.
37793780
@@ -3884,10 +3885,12 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None,
38843885
If `False` produces boxes with the Line2D artist. Otherwise,
38853886
boxes are drawn with Patch artists.
38863887
3887-
labels : sequence, optional
3888+
tick_labels : sequence, optional
38883889
Labels for each dataset (one per dataset). These are used for
38893890
x-tick labels; *not* for legend entries.
38903891
3892+
.. versionadded:: 3.9
3893+
38913894
manage_ticks : bool, default: True
38923895
If True, the tick locations and labels will be adjusted to match
38933896
the boxplot positions.
@@ -3956,6 +3959,10 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None,
39563959
The style of the mean.
39573960
data : indexable object, optional
39583961
DATA_PARAMETER_PLACEHOLDER
3962+
label : sequence, optional
3963+
Legend labels for each boxplot.
3964+
3965+
.. versionadded:: 3.9
39593966
39603967
See Also
39613968
--------
@@ -3970,7 +3977,7 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None,
39703977
bootstrap = mpl.rcParams['boxplot.bootstrap']
39713978

39723979
bxpstats = cbook.boxplot_stats(x, whis=whis, bootstrap=bootstrap,
3973-
labels=labels, autorange=autorange)
3980+
tick_labels=tick_labels, autorange=autorange)
39743981
if notch is None:
39753982
notch = mpl.rcParams['boxplot.notch']
39763983
if vert is None:
@@ -4006,6 +4013,9 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None,
40064013
if 'color' in boxprops:
40074014
boxprops['edgecolor'] = boxprops.pop('color')
40084015

4016+
if label:
4017+
boxprops['label'] = label
4018+
40094019
# if non-default sym value, put it into the flier dictionary
40104020
# the logic for providing the default symbol ('b+') now lives
40114021
# in bxp in the initial value of flierkw
@@ -4330,8 +4340,8 @@ def do_patch(xs, ys, **kwargs):
43304340
if showbox:
43314341
do_box = do_patch if patch_artist else do_plot
43324342
boxes.append(do_box(box_x, box_y, **box_kw))
4343+
whisker_kw.setdefault('label', '_nolegend_')
43334344
# draw the whiskers
4334-
whisker_kw.setdefault('label', '_nolegend_')
43354345
whiskers.append(do_plot(whis_x, whislo_y, **whisker_kw))
43364346
whiskers.append(do_plot(whis_x, whishi_y, **whisker_kw))
43374347
# maybe draw the caps
@@ -4358,6 +4368,22 @@ def do_patch(xs, ys, **kwargs):
43584368
flier_y = stats['fliers']
43594369
fliers.append(do_plot(flier_x, flier_y, **flier_kw))
43604370

4371+
# Set legend labels
4372+
if 'label' in boxprops:
4373+
# If showbox=False, label will be passed to whiskers instead.
4374+
box_or_whis = boxes if showbox else whiskers
4375+
for index, element in enumerate(box_or_whis):
4376+
try:
4377+
if isinstance(boxprops['label'], list):
4378+
element.set_label(boxprops['label'][index])
4379+
else:
4380+
element.set_label(boxprops['label'])
4381+
# If only one label is passed, stop it from being repeated
4382+
# on each legend handle.
4383+
boxprops['label'] = ''
4384+
except Exception:
4385+
IndexError('List index out of range')
4386+
43614387
if manage_ticks:
43624388
axis_name = "x" if vert else "y"
43634389
interval = getattr(self.dataLim, f"interval{axis_name}")

‎lib/matplotlib/axes/_axes.pyi‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ class Axes(_AxesBase):
360360
showbox: bool | None = ...,
361361
showfliers: bool | None = ...,
362362
boxprops: dict[str, Any] | None = ...,
363-
labels: Sequence[str] | None = ...,
363+
tick_labels: Sequence[str] | None = ...,
364364
flierprops: dict[str, Any] | None = ...,
365365
medianprops: dict[str, Any] | None = ...,
366366
meanprops: dict[str, Any] | None = ...,
@@ -370,6 +370,7 @@ class Axes(_AxesBase):
370370
autorange: bool = ...,
371371
zorder: float | None = ...,
372372
capwidths: float | ArrayLike | None = ...,
373+
label: Sequence[str] | None = ...,
373374
*,
374375
data=...,
375376
) -> dict[str, Any]: ...

‎lib/matplotlib/cbook.py‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,7 +1126,8 @@ def _broadcast_with_masks(*args, compress=False):
11261126
return inputs
11271127

11281128

1129-
def boxplot_stats(X, whis=1.5, bootstrap=None, labels=None,
1129+
@_api.rename_parameter("3.9", "labels", "tick_labels")
1130+
def boxplot_stats(X, whis=1.5, bootstrap=None, tick_labels=None,
11301131
autorange=False):
11311132
r"""
11321133
Return a list of dictionaries of statistics used to draw a series of box
@@ -1161,10 +1162,12 @@ def boxplot_stats(X, whis=1.5, bootstrap=None, labels=None,
11611162
Number of times the confidence intervals around the median
11621163
should be bootstrapped (percentile method).
11631164
1164-
labels : array-like, optional
1165+
tick_labels : array-like, optional
11651166
Labels for each dataset. Length must be compatible with
11661167
dimensions of *X*.
11671168
1169+
.. versionadded:: 3.9
1170+
11681171
autorange : bool, optional (False)
11691172
When `True` and the data are distributed such that the 25th and 75th
11701173
percentiles are equal, ``whis`` is set to (0, 100) such that the
@@ -1240,13 +1243,13 @@ def _compute_conf_interval(data, med, iqr, bootstrap):
12401243
X = _reshape_2D(X, "X")
12411244

12421245
ncols = len(X)
1243-
if labels is None:
1244-
labels = itertools.repeat(None)
1245-
elif len(labels) != ncols:
1246+
if tick_labels is None:
1247+
tick_labels = itertools.repeat(None)
1248+
elif len(tick_labels) != ncols:
12461249
raise ValueError("Dimensions of labels and X must be compatible")
12471250

12481251
input_whis = whis
1249-
for ii, (x, label) in enumerate(zip(X, labels)):
1252+
for ii, (x, label) in enumerate(zip(X, tick_labels)):
12501253

12511254
# empty dict
12521255
stats = {}

‎lib/matplotlib/cbook.pyi‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def boxplot_stats(
135135
X: ArrayLike,
136136
whis: float | tuple[float, float] = ...,
137137
bootstrap: int | None = ...,
138-
labels: ArrayLike | None = ...,
138+
tick_labels: ArrayLike | None = ...,
139139
autorange: bool = ...,
140140
) -> list[dict[str, Any]]: ...
141141

‎lib/matplotlib/pyplot.py‎

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2667,17 +2667,21 @@ def annotate(
26672667
text: str,
26682668
xy: tuple[float, float],
26692669
xytext: tuple[float, float] | None = None,
2670-
xycoords: str
2671-
| Artist
2672-
| Transform
2673-
| Callable[[RendererBase], Bbox | Transform]
2674-
| tuple[float, float] = "data",
2675-
textcoords: str
2676-
| Artist
2677-
| Transform
2678-
| Callable[[RendererBase], Bbox | Transform]
2679-
| tuple[float, float]
2680-
| None = None,
2670+
xycoords: (
2671+
str
2672+
| Artist
2673+
| Transform
2674+
| Callable[[RendererBase], Bbox | Transform]
2675+
| tuple[float, float]
2676+
) = "data",
2677+
textcoords: (
2678+
str
2679+
| Artist
2680+
| Transform
2681+
| Callable[[RendererBase], Bbox | Transform]
2682+
| tuple[float, float]
2683+
| None
2684+
) = None,
26812685
arrowprops: dict[str, Any] | None = None,
26822686
annotation_clip: bool | None = None,
26832687
**kwargs,
@@ -2855,7 +2859,7 @@ def boxplot(
28552859
showbox: bool | None = None,
28562860
showfliers: bool | None = None,
28572861
boxprops: dict[str, Any] | None = None,
2858-
labels: Sequence[str] | None = None,
2862+
tick_labels: Sequence[str] | None = None,
28592863
flierprops: dict[str, Any] | None = None,
28602864
medianprops: dict[str, Any] | None = None,
28612865
meanprops: dict[str, Any] | None = None,
@@ -2865,6 +2869,7 @@ def boxplot(
28652869
autorange: bool = False,
28662870
zorder: float | None = None,
28672871
capwidths: float | ArrayLike | None = None,
2872+
label: Sequence[str] | None = None,
28682873
*,
28692874
data=None,
28702875
) -> dict[str, Any]:
@@ -2886,7 +2891,7 @@ def boxplot(
28862891
showbox=showbox,
28872892
showfliers=showfliers,
28882893
boxprops=boxprops,
2889-
labels=labels,
2894+
tick_labels=tick_labels,
28902895
flierprops=flierprops,
28912896
medianprops=medianprops,
28922897
meanprops=meanprops,
@@ -2896,6 +2901,7 @@ def boxplot(
28962901
autorange=autorange,
28972902
zorder=zorder,
28982903
capwidths=capwidths,
2904+
label=label,
28992905
**({"data": data} if data is not None else {}),
29002906
)
29012907

@@ -2928,8 +2934,9 @@ def cohere(
29282934
NFFT: int = 256,
29292935
Fs: float = 2,
29302936
Fc: int = 0,
2931-
detrend: Literal["none", "mean", "linear"]
2932-
| Callable[[ArrayLike], ArrayLike] = mlab.detrend_none,
2937+
detrend: (
2938+
Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike]
2939+
) = mlab.detrend_none,
29332940
window: Callable[[ArrayLike], ArrayLike] | ArrayLike = mlab.window_hanning,
29342941
noverlap: int = 0,
29352942
pad_to: int | None = None,
@@ -2986,9 +2993,9 @@ def csd(
29862993
NFFT: int | None = None,
29872994
Fs: float | None = None,
29882995
Fc: int | None = None,
2989-
detrend: Literal["none", "mean", "linear"]
2990-
| Callable[[ArrayLike], ArrayLike]
2991-
| None = None,
2996+
detrend: (
2997+
Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] | None
2998+
) = None,
29922999
window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = None,
29933000
noverlap: int | None = None,
29943001
pad_to: int | None = None,
@@ -3651,9 +3658,9 @@ def psd(
36513658
NFFT: int | None = None,
36523659
Fs: float | None = None,
36533660
Fc: int | None = None,
3654-
detrend: Literal["none", "mean", "linear"]
3655-
| Callable[[ArrayLike], ArrayLike]
3656-
| None = None,
3661+
detrend: (
3662+
Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] | None
3663+
) = None,
36573664
window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = None,
36583665
noverlap: int | None = None,
36593666
pad_to: int | None = None,
@@ -3759,9 +3766,9 @@ def specgram(
37593766
NFFT: int | None = None,
37603767
Fs: float | None = None,
37613768
Fc: int | None = None,
3762-
detrend: Literal["none", "mean", "linear"]
3763-
| Callable[[ArrayLike], ArrayLike]
3764-
| None = None,
3769+
detrend: (
3770+
Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] | None
3771+
) = None,
37653772
window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = None,
37663773
noverlap: int | None = None,
37673774
cmap: str | Colormap | None = None,
@@ -4071,10 +4078,9 @@ def violinplot(
40714078
showmedians: bool = False,
40724079
quantiles: Sequence[float | Sequence[float]] | None = None,
40734080
points: int = 100,
4074-
bw_method: Literal["scott", "silverman"]
4075-
| float
4076-
| Callable[[GaussianKDE], float]
4077-
| None = None,
4081+
bw_method: (
4082+
Literal["scott", "silverman"] | float | Callable[[GaussianKDE], float] | None
4083+
) = None,
40784084
*,
40794085
data=None,
40804086
) -> dict[str, Collection]:

0 commit comments

Comments
 (0)