Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 102d0a6

Browse files
authored
Merge pull request #27840 from saranti/box_legend_support
Add legend support for boxplots
2 parents 1174aad + c96334d commit 102d0a6

File tree

5 files changed

+140
-4
lines changed

5 files changed

+140
-4
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
Legend support for Boxplot
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
Boxplots now support a *label* parameter to create legend entries.
4+
5+
Legend labels can be passed as a list of strings to label multiple boxes in a single
6+
`.Axes.boxplot` call:
7+
8+
9+
.. plot::
10+
:include-source: true
11+
:alt: Example of creating 3 boxplots and assigning legend labels as a sequence.
12+
13+
import matplotlib.pyplot as plt
14+
import numpy as np
15+
16+
np.random.seed(19680801)
17+
fruit_weights = [
18+
np.random.normal(130, 10, size=100),
19+
np.random.normal(125, 20, size=100),
20+
np.random.normal(120, 30, size=100),
21+
]
22+
labels = ['peaches', 'oranges', 'tomatoes']
23+
colors = ['peachpuff', 'orange', 'tomato']
24+
25+
fig, ax = plt.subplots()
26+
ax.set_ylabel('fruit weight (g)')
27+
28+
bplot = ax.boxplot(fruit_weights,
29+
patch_artist=True, # fill with color
30+
label=labels)
31+
32+
# fill with colors
33+
for patch, color in zip(bplot['boxes'], colors):
34+
patch.set_facecolor(color)
35+
36+
ax.set_xticks([])
37+
ax.legend()
38+
39+
40+
Or as a single string to each individual `.Axes.boxplot`:
41+
42+
.. plot::
43+
:include-source: true
44+
:alt: Example of creating 2 boxplots and assigning each legend label as a string.
45+
46+
import matplotlib.pyplot as plt
47+
import numpy as np
48+
49+
fig, ax = plt.subplots()
50+
51+
data_A = np.random.random((100, 3))
52+
data_B = np.random.random((100, 3)) + 0.2
53+
pos = np.arange(3)
54+
55+
ax.boxplot(data_A, positions=pos - 0.2, patch_artist=True, label='Box A',
56+
boxprops={'facecolor': 'steelblue'})
57+
ax.boxplot(data_B, positions=pos + 0.2, patch_artist=True, label='Box B',
58+
boxprops={'facecolor': 'lightblue'})
59+
60+
ax.legend()

lib/matplotlib/axes/_axes.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3816,7 +3816,7 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None,
38163816
tick_labels=None, flierprops=None, medianprops=None,
38173817
meanprops=None, capprops=None, whiskerprops=None,
38183818
manage_ticks=True, autorange=False, zorder=None,
3819-
capwidths=None):
3819+
capwidths=None, label=None):
38203820
"""
38213821
Draw a box and whisker plot.
38223822
@@ -4003,6 +4003,20 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None,
40034003
The style of the median.
40044004
meanprops : dict, default: None
40054005
The style of the mean.
4006+
label : str or list of str, optional
4007+
Legend labels. Use a single string when all boxes have the same style and
4008+
you only want a single legend entry for them. Use a list of strings to
4009+
label all boxes individually. To be distinguishable, the boxes should be
4010+
styled individually, which is currently only possible by modifying the
4011+
returned artists, see e.g. :doc:`/gallery/statistics/boxplot_demo`.
4012+
4013+
In the case of a single string, the legend entry will technically be
4014+
associated with the first box only. By default, the legend will show the
4015+
median line (``result["medians"]``); if *patch_artist* is True, the legend
4016+
will show the box `.Patch` artists (``result["boxes"]``) instead.
4017+
4018+
.. versionadded:: 3.9
4019+
40064020
data : indexable object, optional
40074021
DATA_PARAMETER_PLACEHOLDER
40084022
@@ -4123,7 +4137,7 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None,
41234137
meanline=meanline, showfliers=showfliers,
41244138
capprops=capprops, whiskerprops=whiskerprops,
41254139
manage_ticks=manage_ticks, zorder=zorder,
4126-
capwidths=capwidths)
4140+
capwidths=capwidths, label=label)
41274141
return artists
41284142

41294143
def bxp(self, bxpstats, positions=None, widths=None, vert=True,
@@ -4132,7 +4146,7 @@ def bxp(self, bxpstats, positions=None, widths=None, vert=True,
41324146
boxprops=None, whiskerprops=None, flierprops=None,
41334147
medianprops=None, capprops=None, meanprops=None,
41344148
meanline=False, manage_ticks=True, zorder=None,
4135-
capwidths=None):
4149+
capwidths=None, label=None):
41364150
"""
41374151
Draw a box and whisker plot from pre-computed statistics.
41384152
@@ -4215,6 +4229,20 @@ def bxp(self, bxpstats, positions=None, widths=None, vert=True,
42154229
If True, the tick locations and labels will be adjusted to match the
42164230
boxplot positions.
42174231
4232+
label : str or list of str, optional
4233+
Legend labels. Use a single string when all boxes have the same style and
4234+
you only want a single legend entry for them. Use a list of strings to
4235+
label all boxes individually. To be distinguishable, the boxes should be
4236+
styled individually, which is currently only possible by modifying the
4237+
returned artists, see e.g. :doc:`/gallery/statistics/boxplot_demo`.
4238+
4239+
In the case of a single string, the legend entry will technically be
4240+
associated with the first box only. By default, the legend will show the
4241+
median line (``result["medians"]``); if *patch_artist* is True, the legend
4242+
will show the box `.Patch` artists (``result["boxes"]``) instead.
4243+
4244+
.. versionadded:: 3.9
4245+
42184246
zorder : float, default: ``Line2D.zorder = 2``
42194247
The zorder of the resulting boxplot.
42204248
@@ -4379,6 +4407,7 @@ def do_patch(xs, ys, **kwargs):
43794407
if showbox:
43804408
do_box = do_patch if patch_artist else do_plot
43814409
boxes.append(do_box(box_x, box_y, **box_kw))
4410+
median_kw.setdefault('label', '_nolegend_')
43824411
# draw the whiskers
43834412
whisker_kw.setdefault('label', '_nolegend_')
43844413
whiskers.append(do_plot(whis_x, whislo_y, **whisker_kw))
@@ -4389,7 +4418,6 @@ def do_patch(xs, ys, **kwargs):
43894418
caps.append(do_plot(cap_x, cap_lo, **cap_kw))
43904419
caps.append(do_plot(cap_x, cap_hi, **cap_kw))
43914420
# draw the medians
4392-
median_kw.setdefault('label', '_nolegend_')
43934421
medians.append(do_plot(med_x, med_y, **median_kw))
43944422
# maybe draw the means
43954423
if showmeans:
@@ -4407,6 +4435,18 @@ def do_patch(xs, ys, **kwargs):
44074435
flier_y = stats['fliers']
44084436
fliers.append(do_plot(flier_x, flier_y, **flier_kw))
44094437

4438+
# Set legend labels
4439+
if label:
4440+
box_or_med = boxes if showbox and patch_artist else medians
4441+
if cbook.is_scalar_or_string(label):
4442+
# assign the label only to the first box
4443+
box_or_med[0].set_label(label)
4444+
else: # label is a sequence
4445+
if len(box_or_med) != len(label):
4446+
raise ValueError(datashape_message.format("label"))
4447+
for artist, lbl in zip(box_or_med, label):
4448+
artist.set_label(lbl)
4449+
44104450
if manage_ticks:
44114451
axis_name = "x" if vert else "y"
44124452
interval = getattr(self.dataLim, f"interval{axis_name}")

lib/matplotlib/axes/_axes.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ class Axes(_AxesBase):
372372
autorange: bool = ...,
373373
zorder: float | None = ...,
374374
capwidths: float | ArrayLike | None = ...,
375+
label: Sequence[str] | None = ...,
375376
*,
376377
data=...,
377378
) -> dict[str, Any]: ...
@@ -397,6 +398,7 @@ class Axes(_AxesBase):
397398
manage_ticks: bool = ...,
398399
zorder: float | None = ...,
399400
capwidths: float | ArrayLike | None = ...,
401+
label: Sequence[str] | None = ...,
400402
) -> dict[str, Any]: ...
401403
def scatter(
402404
self,

lib/matplotlib/pyplot.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2941,6 +2941,7 @@ def boxplot(
29412941
autorange: bool = False,
29422942
zorder: float | None = None,
29432943
capwidths: float | ArrayLike | None = None,
2944+
label: Sequence[str] | None = None,
29442945
*,
29452946
data=None,
29462947
) -> dict[str, Any]:
@@ -2972,6 +2973,7 @@ def boxplot(
29722973
autorange=autorange,
29732974
zorder=zorder,
29742975
capwidths=capwidths,
2976+
label=label,
29752977
**({"data": data} if data is not None else {}),
29762978
)
29772979

lib/matplotlib/tests/test_legend.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,3 +1435,35 @@ def test_legend_text():
14351435
leg_bboxes.append(
14361436
leg.get_window_extent().transformed(ax.transAxes.inverted()))
14371437
assert_allclose(leg_bboxes[1].bounds, leg_bboxes[0].bounds)
1438+
1439+
1440+
def test_boxplot_legend_labels():
1441+
# Test that legend entries are generated when passing `label`.
1442+
np.random.seed(19680801)
1443+
data = np.random.random((10, 4))
1444+
fig, axs = plt.subplots(nrows=1, ncols=4)
1445+
legend_labels = ['box A', 'box B', 'box C', 'box D']
1446+
1447+
# Testing legend labels and patch passed to legend.
1448+
bp1 = axs[0].boxplot(data, patch_artist=True, label=legend_labels)
1449+
assert [v.get_label() for v in bp1['boxes']] == legend_labels
1450+
handles, labels = axs[0].get_legend_handles_labels()
1451+
assert labels == legend_labels
1452+
assert all(isinstance(h, mpl.patches.PathPatch) for h in handles)
1453+
1454+
# Testing legend without `box`.
1455+
bp2 = axs[1].boxplot(data, label=legend_labels, showbox=False)
1456+
# Without a box, The legend entries should be passed from the medians.
1457+
assert [v.get_label() for v in bp2['medians']] == legend_labels
1458+
handles, labels = axs[1].get_legend_handles_labels()
1459+
assert labels == legend_labels
1460+
assert all(isinstance(h, mpl.lines.Line2D) for h in handles)
1461+
1462+
# Testing legend with number of labels different from number of boxes.
1463+
with pytest.raises(ValueError, match='values must have same the length'):
1464+
bp3 = axs[2].boxplot(data, label=legend_labels[:-1])
1465+
1466+
# Test that for a string label, only the first box gets a label.
1467+
bp4 = axs[3].boxplot(data, label='box A')
1468+
assert bp4['medians'][0].get_label() == 'box A'
1469+
assert all(x.get_label().startswith("_") for x in bp4['medians'][1:])

0 commit comments

Comments
 (0)