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 0b5d5d6

Browse filesBrowse files
committed
add legend support for boxplots
1 parent 76eaa96 commit 0b5d5d6
Copy full SHA for 0b5d5d6

File tree

Expand file treeCollapse file tree

12 files changed

+271
-72
lines changed
Filter options
Expand file treeCollapse file tree

12 files changed

+271
-72
lines changed
+5Lines 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.
+40Lines 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()
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add option to plot only one half of violin plot
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
Setting the parameter *side* to 'low' or 'high' allows to only plot one half of the violin plot.

‎galleries/examples/statistics/violinplot.py

Copy file name to clipboardExpand all lines: galleries/examples/statistics/violinplot.py
+29-11Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,55 +28,73 @@
2828
pos = [1, 2, 4, 5, 7, 8]
2929
data = [np.random.normal(0, std, size=100) for std in pos]
3030

31-
fig, axs = plt.subplots(nrows=2, ncols=5, figsize=(10, 6))
31+
fig, axs = plt.subplots(nrows=2, ncols=6, figsize=(10, 4))
3232

3333
axs[0, 0].violinplot(data, pos, points=20, widths=0.3,
3434
showmeans=True, showextrema=True, showmedians=True)
35-
axs[0, 0].set_title('Custom violinplot 1', fontsize=fs)
35+
axs[0, 0].set_title('Custom violin 1', fontsize=fs)
3636

3737
axs[0, 1].violinplot(data, pos, points=40, widths=0.5,
3838
showmeans=True, showextrema=True, showmedians=True,
3939
bw_method='silverman')
40-
axs[0, 1].set_title('Custom violinplot 2', fontsize=fs)
40+
axs[0, 1].set_title('Custom violin 2', fontsize=fs)
4141

4242
axs[0, 2].violinplot(data, pos, points=60, widths=0.7, showmeans=True,
4343
showextrema=True, showmedians=True, bw_method=0.5)
44-
axs[0, 2].set_title('Custom violinplot 3', fontsize=fs)
44+
axs[0, 2].set_title('Custom violin 3', fontsize=fs)
4545

4646
axs[0, 3].violinplot(data, pos, points=60, widths=0.7, showmeans=True,
4747
showextrema=True, showmedians=True, bw_method=0.5,
4848
quantiles=[[0.1], [], [], [0.175, 0.954], [0.75], [0.25]])
49-
axs[0, 3].set_title('Custom violinplot 4', fontsize=fs)
49+
axs[0, 3].set_title('Custom violin 4', fontsize=fs)
5050

5151
axs[0, 4].violinplot(data[-1:], pos[-1:], points=60, widths=0.7,
5252
showmeans=True, showextrema=True, showmedians=True,
5353
quantiles=[0.05, 0.1, 0.8, 0.9], bw_method=0.5)
54-
axs[0, 4].set_title('Custom violinplot 5', fontsize=fs)
54+
axs[0, 4].set_title('Custom violin 5', fontsize=fs)
55+
56+
axs[0, 5].violinplot(data[-1:], pos[-1:], points=60, widths=0.7,
57+
showmeans=True, showextrema=True, showmedians=True,
58+
quantiles=[0.05, 0.1, 0.8, 0.9], bw_method=0.5, side='low')
59+
60+
axs[0, 5].violinplot(data[-1:], pos[-1:], points=60, widths=0.7,
61+
showmeans=True, showextrema=True, showmedians=True,
62+
quantiles=[0.05, 0.1, 0.8, 0.9], bw_method=0.5, side='high')
63+
axs[0, 5].set_title('Custom violin 6', fontsize=fs)
5564

5665
axs[1, 0].violinplot(data, pos, points=80, vert=False, widths=0.7,
5766
showmeans=True, showextrema=True, showmedians=True)
58-
axs[1, 0].set_title('Custom violinplot 6', fontsize=fs)
67+
axs[1, 0].set_title('Custom violin 7', fontsize=fs)
5968

6069
axs[1, 1].violinplot(data, pos, points=100, vert=False, widths=0.9,
6170
showmeans=True, showextrema=True, showmedians=True,
6271
bw_method='silverman')
63-
axs[1, 1].set_title('Custom violinplot 7', fontsize=fs)
72+
axs[1, 1].set_title('Custom violin 8', fontsize=fs)
6473

6574
axs[1, 2].violinplot(data, pos, points=200, vert=False, widths=1.1,
6675
showmeans=True, showextrema=True, showmedians=True,
6776
bw_method=0.5)
68-
axs[1, 2].set_title('Custom violinplot 8', fontsize=fs)
77+
axs[1, 2].set_title('Custom violin 9', fontsize=fs)
6978

7079
axs[1, 3].violinplot(data, pos, points=200, vert=False, widths=1.1,
7180
showmeans=True, showextrema=True, showmedians=True,
7281
quantiles=[[0.1], [], [], [0.175, 0.954], [0.75], [0.25]],
7382
bw_method=0.5)
74-
axs[1, 3].set_title('Custom violinplot 9', fontsize=fs)
83+
axs[1, 3].set_title('Custom violin 10', fontsize=fs)
7584

7685
axs[1, 4].violinplot(data[-1:], pos[-1:], points=200, vert=False, widths=1.1,
7786
showmeans=True, showextrema=True, showmedians=True,
7887
quantiles=[0.05, 0.1, 0.8, 0.9], bw_method=0.5)
79-
axs[1, 4].set_title('Custom violinplot 10', fontsize=fs)
88+
axs[1, 4].set_title('Custom violin 11', fontsize=fs)
89+
90+
axs[1, 5].violinplot(data[-1:], pos[-1:], points=200, vert=False, widths=1.1,
91+
showmeans=True, showextrema=True, showmedians=True,
92+
quantiles=[0.05, 0.1, 0.8, 0.9], bw_method=0.5, side='low')
93+
94+
axs[1, 5].violinplot(data[-1:], pos[-1:], points=200, vert=False, widths=1.1,
95+
showmeans=True, showextrema=True, showmedians=True,
96+
quantiles=[0.05, 0.1, 0.8, 0.9], bw_method=0.5, side='high')
97+
axs[1, 5].set_title('Custom violin 12', fontsize=fs)
8098

8199

82100
for ax in axs.flat:

‎lib/matplotlib/axes/_axes.py

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_axes.py
+67-18Lines changed: 67 additions & 18 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,9 +3885,10 @@ 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-
Labels for each dataset (one per dataset). These are used for
3889-
x-tick labels; *not* for legend entries.
3888+
tick_labels : sequence, optional
3889+
Labels for each dataset (one per dataset).
3890+
3891+
.. versionadded:: 3.9
38903892
38913893
manage_ticks : bool, default: True
38923894
If True, the tick locations and labels will be adjusted to match
@@ -3954,6 +3956,11 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None,
39543956
The style of the median.
39553957
meanprops : dict, default: None
39563958
The style of the mean.
3959+
label : str or list of str, optional
3960+
Legend labels for each boxplot.
3961+
3962+
.. versionadded:: 3.9
3963+
39573964
data : indexable object, optional
39583965
DATA_PARAMETER_PLACEHOLDER
39593966
@@ -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
@@ -4074,7 +4084,7 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None,
40744084
meanline=meanline, showfliers=showfliers,
40754085
capprops=capprops, whiskerprops=whiskerprops,
40764086
manage_ticks=manage_ticks, zorder=zorder,
4077-
capwidths=capwidths)
4087+
capwidths=capwidths, label=label)
40784088
return artists
40794089

40804090
def bxp(self, bxpstats, positions=None, widths=None, vert=True,
@@ -4083,7 +4093,7 @@ def bxp(self, bxpstats, positions=None, widths=None, vert=True,
40834093
boxprops=None, whiskerprops=None, flierprops=None,
40844094
medianprops=None, capprops=None, meanprops=None,
40854095
meanline=False, manage_ticks=True, zorder=None,
4086-
capwidths=None):
4096+
capwidths=None, label=None):
40874097
"""
40884098
Draw a box and whisker plot from pre-computed statistics.
40894099
@@ -4169,6 +4179,9 @@ def bxp(self, bxpstats, positions=None, widths=None, vert=True,
41694179
zorder : float, default: ``Line2D.zorder = 2``
41704180
The zorder of the resulting boxplot.
41714181
4182+
label : str or list of str, optional
4183+
Legend labels for each boxplot.
4184+
41724185
Returns
41734186
-------
41744187
dict
@@ -4330,8 +4343,8 @@ def do_patch(xs, ys, **kwargs):
43304343
if showbox:
43314344
do_box = do_patch if patch_artist else do_plot
43324345
boxes.append(do_box(box_x, box_y, **box_kw))
4346+
whisker_kw.setdefault('label', '_nolegend_')
43334347
# draw the whiskers
4334-
whisker_kw.setdefault('label', '_nolegend_')
43354348
whiskers.append(do_plot(whis_x, whislo_y, **whisker_kw))
43364349
whiskers.append(do_plot(whis_x, whishi_y, **whisker_kw))
43374350
# maybe draw the caps
@@ -4358,6 +4371,15 @@ def do_patch(xs, ys, **kwargs):
43584371
flier_y = stats['fliers']
43594372
fliers.append(do_plot(flier_x, flier_y, **flier_kw))
43604373

4374+
# Set legend labels
4375+
if label:
4376+
box_or_whis = boxes if showbox else whiskers
4377+
for index, element in enumerate(box_or_whis):
4378+
try:
4379+
element.set_label(label[index])
4380+
except Exception:
4381+
IndexError('list index out of range')
4382+
43614383
if manage_ticks:
43624384
axis_name = "x" if vert else "y"
43634385
interval = getattr(self.dataLim, f"interval{axis_name}")
@@ -8205,7 +8227,7 @@ def matshow(self, Z, **kwargs):
82058227
@_preprocess_data(replace_names=["dataset"])
82068228
def violinplot(self, dataset, positions=None, vert=True, widths=0.5,
82078229
showmeans=False, showextrema=True, showmedians=False,
8208-
quantiles=None, points=100, bw_method=None):
8230+
quantiles=None, points=100, bw_method=None, side='both'):
82098231
"""
82108232
Make a violin plot.
82118233
@@ -8256,6 +8278,10 @@ def violinplot(self, dataset, positions=None, vert=True, widths=0.5,
82568278
callable, it should take a `matplotlib.mlab.GaussianKDE` instance as
82578279
its only parameter and return a float.
82588280
8281+
side : {'both', 'low', 'high'}, default: 'both'
8282+
'both' plots standard violins. 'low'/'high' only
8283+
plots the side below/above the positions value.
8284+
82598285
data : indexable object, optional
82608286
DATA_PARAMETER_PLACEHOLDER
82618287
@@ -8307,10 +8333,10 @@ def _kde_method(X, coords):
83078333
quantiles=quantiles)
83088334
return self.violin(vpstats, positions=positions, vert=vert,
83098335
widths=widths, showmeans=showmeans,
8310-
showextrema=showextrema, showmedians=showmedians)
8336+
showextrema=showextrema, showmedians=showmedians, side=side)
83118337

83128338
def violin(self, vpstats, positions=None, vert=True, widths=0.5,
8313-
showmeans=False, showextrema=True, showmedians=False):
8339+
showmeans=False, showextrema=True, showmedians=False, side='both'):
83148340
"""
83158341
Draw a violin plot from pre-computed statistics.
83168342
@@ -8366,6 +8392,10 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5,
83668392
showmedians : bool, default: False
83678393
Whether to show the median with a line.
83688394
8395+
side : {'both', 'low', 'high'}, default: 'both'
8396+
'both' plots standard violins. 'low'/'high' only
8397+
plots the side below/above the positions value.
8398+
83698399
Returns
83708400
-------
83718401
dict
@@ -8428,8 +8458,13 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5,
84288458
elif len(widths) != N:
84298459
raise ValueError(datashape_message.format("widths"))
84308460

8461+
# Validate side
8462+
_api.check_in_list(["both", "low", "high"], side=side)
8463+
84318464
# Calculate ranges for statistics lines (shape (2, N)).
8432-
line_ends = [[-0.25], [0.25]] * np.array(widths) + positions
8465+
line_ends = [[-0.25 if side in ['both', 'low'] else 0],
8466+
[0.25 if side in ['both', 'high'] else 0]] \
8467+
* np.array(widths) + positions
84338468

84348469
# Colors.
84358470
if mpl.rcParams['_internal.classic_mode']:
@@ -8441,20 +8476,34 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5,
84418476
# Check whether we are rendering vertically or horizontally
84428477
if vert:
84438478
fill = self.fill_betweenx
8444-
perp_lines = functools.partial(self.hlines, colors=linecolor)
8445-
par_lines = functools.partial(self.vlines, colors=linecolor)
8479+
if side in ['low', 'high']:
8480+
perp_lines = functools.partial(self.hlines, colors=linecolor,
8481+
capstyle='projecting')
8482+
par_lines = functools.partial(self.vlines, colors=linecolor,
8483+
capstyle='projecting')
8484+
else:
8485+
perp_lines = functools.partial(self.hlines, colors=linecolor)
8486+
par_lines = functools.partial(self.vlines, colors=linecolor)
84468487
else:
84478488
fill = self.fill_between
8448-
perp_lines = functools.partial(self.vlines, colors=linecolor)
8449-
par_lines = functools.partial(self.hlines, colors=linecolor)
8489+
if side in ['low', 'high']:
8490+
perp_lines = functools.partial(self.vlines, colors=linecolor,
8491+
capstyle='projecting')
8492+
par_lines = functools.partial(self.hlines, colors=linecolor,
8493+
capstyle='projecting')
8494+
else:
8495+
perp_lines = functools.partial(self.vlines, colors=linecolor)
8496+
par_lines = functools.partial(self.hlines, colors=linecolor)
84508497

84518498
# Render violins
84528499
bodies = []
84538500
for stats, pos, width in zip(vpstats, positions, widths):
84548501
# The 0.5 factor reflects the fact that we plot from v-p to v+p.
84558502
vals = np.array(stats['vals'])
84568503
vals = 0.5 * width * vals / vals.max()
8457-
bodies += [fill(stats['coords'], -vals + pos, vals + pos,
8504+
bodies += [fill(stats['coords'],
8505+
-vals + pos if side in ['both', 'low'] else pos,
8506+
vals + pos if side in ['both', 'high'] else pos,
84588507
facecolor=fillcolor, alpha=0.3)]
84598508
means.append(stats['mean'])
84608509
mins.append(stats['min'])

‎lib/matplotlib/axes/_axes.pyi

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_axes.pyi
+5-1Lines changed: 5 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]: ...
@@ -395,6 +396,7 @@ class Axes(_AxesBase):
395396
manage_ticks: bool = ...,
396397
zorder: float | None = ...,
397398
capwidths: float | ArrayLike | None = ...,
399+
label: Sequence[str] | None = ...,
398400
) -> dict[str, Any]: ...
399401
def scatter(
400402
self,
@@ -743,6 +745,7 @@ class Axes(_AxesBase):
743745
| float
744746
| Callable[[GaussianKDE], float]
745747
| None = ...,
748+
side: Literal["both", "low", "high"] = ...,
746749
*,
747750
data=...,
748751
) -> dict[str, Collection]: ...
@@ -755,6 +758,7 @@ class Axes(_AxesBase):
755758
showmeans: bool = ...,
756759
showextrema: bool = ...,
757760
showmedians: bool = ...,
761+
side: Literal["both", "low", "high"] = ...,
758762
) -> dict[str, Collection]: ...
759763

760764
table = mtable.table

0 commit comments

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