Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 73f2933

Browse filesBrowse files
committed
add legend to boxplot
1 parent 4ebc8ce commit 73f2933
Copy full SHA for 73f2933

File tree

Expand file treeCollapse file tree

5 files changed

+137
-4
lines changed
Filter options
Expand file treeCollapse file tree

5 files changed

+137
-4
lines changed
+60Lines 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+
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 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

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_axes.py
+41-4Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3797,7 +3797,7 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None,
37973797
labels=None, flierprops=None, medianprops=None,
37983798
meanprops=None, capprops=None, whiskerprops=None,
37993799
manage_ticks=True, autorange=False, zorder=None,
3800-
capwidths=None):
3800+
capwidths=None, label=None):
38013801
"""
38023802
Draw a box and whisker plot.
38033803
@@ -3978,6 +3978,18 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None,
39783978
The style of the median.
39793979
meanprops : dict, default: None
39803980
The style of the mean.
3981+
label : str or list of str, optional
3982+
Legend labels. Use a single string when all boxes have the same style and
3983+
you only want a single legend entry for them. Use a list of strings to
3984+
label all boxes individually. To be distinguishable, the boxes should be
3985+
styled individually, which is currently only possible by modifying the
3986+
returned artists, see e.g. :doc:`/gallery/statistics/boxplot_demo`.
3987+
3988+
In the case of a single string, the legend entry will technically be
3989+
associated with the first box only. By default, the legend will show the
3990+
median line (``result["medians"]``); if *patch_artist* is True, the legend
3991+
will show the box `Patch` artists (``result["boxes"]``) instead.
3992+
39813993
data : indexable object, optional
39823994
DATA_PARAMETER_PLACEHOLDER
39833995
@@ -4098,7 +4110,7 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None,
40984110
meanline=meanline, showfliers=showfliers,
40994111
capprops=capprops, whiskerprops=whiskerprops,
41004112
manage_ticks=manage_ticks, zorder=zorder,
4101-
capwidths=capwidths)
4113+
capwidths=capwidths, label=label)
41024114
return artists
41034115

41044116
def bxp(self, bxpstats, positions=None, widths=None, vert=True,
@@ -4107,7 +4119,7 @@ def bxp(self, bxpstats, positions=None, widths=None, vert=True,
41074119
boxprops=None, whiskerprops=None, flierprops=None,
41084120
medianprops=None, capprops=None, meanprops=None,
41094121
meanline=False, manage_ticks=True, zorder=None,
4110-
capwidths=None):
4122+
capwidths=None, label=None):
41114123
"""
41124124
Draw a box and whisker plot from pre-computed statistics.
41134125
@@ -4190,6 +4202,18 @@ def bxp(self, bxpstats, positions=None, widths=None, vert=True,
41904202
If True, the tick locations and labels will be adjusted to match the
41914203
boxplot positions.
41924204
4205+
label : str or list of str, optional
4206+
Legend labels. Use a single string when all boxes have the same style and
4207+
you only want a single legend entry for them. Use a list of strings to
4208+
label all boxes individually. To be distinguishable, the boxes should be
4209+
styled individually, which is currently only possible by modifying the
4210+
returned artists, see e.g. :doc:`/gallery/statistics/boxplot_demo`.
4211+
4212+
In the case of a single string, the legend entry will technically be
4213+
associated with the first box only. By default, the legend will show the
4214+
median line (``result["medians"]``); if *patch_artist* is True, the legend
4215+
will show the box `Patch` artists (``result["boxes"]``) instead.
4216+
41934217
zorder : float, default: ``Line2D.zorder = 2``
41944218
The zorder of the resulting boxplot.
41954219
@@ -4354,6 +4378,7 @@ def do_patch(xs, ys, **kwargs):
43544378
if showbox:
43554379
do_box = do_patch if patch_artist else do_plot
43564380
boxes.append(do_box(box_x, box_y, **box_kw))
4381+
median_kw.setdefault('label', '_nolegend_')
43574382
# draw the whiskers
43584383
whisker_kw.setdefault('label', '_nolegend_')
43594384
whiskers.append(do_plot(whis_x, whislo_y, **whisker_kw))
@@ -4364,7 +4389,6 @@ def do_patch(xs, ys, **kwargs):
43644389
caps.append(do_plot(cap_x, cap_lo, **cap_kw))
43654390
caps.append(do_plot(cap_x, cap_hi, **cap_kw))
43664391
# draw the medians
4367-
median_kw.setdefault('label', '_nolegend_')
43684392
medians.append(do_plot(med_x, med_y, **median_kw))
43694393
# maybe draw the means
43704394
if showmeans:
@@ -4382,6 +4406,19 @@ def do_patch(xs, ys, **kwargs):
43824406
flier_y = stats['fliers']
43834407
fliers.append(do_plot(flier_x, flier_y, **flier_kw))
43844408

4409+
# Set legend labels
4410+
if label:
4411+
box_or_med = boxes if showbox and patch_artist else medians
4412+
if cbook.is_scalar_or_string(label):
4413+
# assign the label only to the first box
4414+
box_or_med[0].set_label(label)
4415+
else: # label is a sequence
4416+
if len(box_or_med) != len(label):
4417+
raise ValueError("There must be an equal number of legend"
4418+
" labels and boxplots.")
4419+
for artist, lbl in zip(box_or_med, label):
4420+
artist.set_label(lbl)
4421+
43854422
if manage_ticks:
43864423
axis_name = "x" if vert else "y"
43874424
interval = getattr(self.dataLim, f"interval{axis_name}")

‎lib/matplotlib/axes/_axes.pyi

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_axes.pyi
+2Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/pyplot.py
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2865,6 +2865,7 @@ def boxplot(
28652865
autorange: bool = False,
28662866
zorder: float | None = None,
28672867
capwidths: float | ArrayLike | None = None,
2868+
label: Sequence[str] | None = None,
28682869
*,
28692870
data=None,
28702871
) -> dict[str, Any]:
@@ -2896,6 +2897,7 @@ def boxplot(
28962897
autorange=autorange,
28972898
zorder=zorder,
28982899
capwidths=capwidths,
2900+
label=label,
28992901
**({"data": data} if data is not None else {}),
29002902
)
29012903

‎lib/matplotlib/tests/test_legend.py

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_legend.py
+32Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1450,3 +1450,35 @@ def test_boxplot_labels():
14501450
handles, labels = ax.get_legend_handles_labels()
14511451
assert len(handles) == 0
14521452
assert len(labels) == 0
1453+
1454+
1455+
def test_boxplot_legend_labels():
1456+
# Test that legend entries are generated when passing `label`.
1457+
np.random.seed(19680801)
1458+
data = np.random.random((10, 4))
1459+
fig, axs = plt.subplots(nrows=1, ncols=4)
1460+
legend_labels = ['box A', 'box B', 'box C', 'box D']
1461+
1462+
# Testing legend labels and patch passed to legend.
1463+
bp1 = axs[0].boxplot(data, patch_artist=True, label=legend_labels)
1464+
assert [v.get_label() for v in bp1['boxes']] == legend_labels
1465+
handles, labels = axs[0].get_legend_handles_labels()
1466+
assert labels == legend_labels
1467+
assert all(isinstance(h, mpl.patches.PathPatch) for h in handles)
1468+
1469+
# Testing legend without `box`.
1470+
bp2 = axs[1].boxplot(data, label=legend_labels, showbox=False)
1471+
# Without a box, The legend entries should be passed from the medians.
1472+
assert [v.get_label() for v in bp2['medians']] == legend_labels
1473+
handles, labels = axs[1].get_legend_handles_labels()
1474+
assert labels == legend_labels
1475+
assert all(isinstance(h, mpl.lines.Line2D) for h in handles)
1476+
1477+
# Testing legend with number of labels different from number of boxes.
1478+
with pytest.raises(ValueError, match='There must be an equal number'):
1479+
bp3 = axs[2].boxplot(data, label=legend_labels[:-1])
1480+
1481+
# Test that for a string label, only the first box gets a label.
1482+
bp4 = axs[3].boxplot(data, label='box A')
1483+
assert bp4['medians'][0].get_label() == 'box A'
1484+
assert all(x.get_label().startswith("_") for x in bp4['medians'][1:])

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.