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 96f5603

Browse filesBrowse files
landoskapetimhoffm
andauthored
Allow user to specify colors in violin plots with constructor method (#27304)
* Add color specification and pyi docs Fix formatting issues Fix argument order in stub Update pyplot.py Show new features Added versionadded line to new arguments Add test for violinplot color arguments Add file to "whats new" Formatting issues fixed More formatting fixes... Linting Fix violinplot test Transitioning to facecolor and edgecolor Fixed argument name Update doc/users/next_whats_new/violinplot_colors.rst Co-authored-by: Oscar Gustafsson <oscar.gustafsson@gmail.com> Update galleries/examples/statistics/customized_violin.py Co-authored-by: Oscar Gustafsson <oscar.gustafsson@gmail.com> Update test_axes.py Consolidate versionadded directive Enable vectorized color control Fix test - ignore xaxis rendering Correct x positions in test Correct positions in test Intelligent alpha handling Cycler bug fix Added has_alpha for classic_mode No special alpha control Removed colors, added alpha parameter Add alpha control to default color sequence correct arguments in type hinting update docs to newest argument grouping remove obsolete explanation Correct testing with new color arguments * edgecolor to linecolor, no more alpha * Arg documentation Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> * completeness, formatting Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> * Default logic, docstring, straightforward testing * Spelling error Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> * single "next color" for default face/line * clarify color specification * axes bug -- set with facecolor * direct comparison to source colors, face!=line * adjust behavior for classic mode * Apply suggestions from code review Minor formatting and code cleanup --------- Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
1 parent 6758f8a commit 96f5603
Copy full SHA for 96f5603

File tree

Expand file treeCollapse file tree

6 files changed

+177
-19
lines changed
Filter options
Expand file treeCollapse file tree

6 files changed

+177
-19
lines changed
+8Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
``violinplot`` now accepts color arguments
2+
-------------------------------------------
3+
4+
`~.Axes.violinplot` and `~.Axes.violin` now accept ``facecolor`` and
5+
``linecolor`` as input arguments. This means that users can set the color of
6+
violinplots as they make them, rather than setting the color of individual
7+
objects afterwards. It is possible to pass a single color to be used for all
8+
violins, or pass a sequence of colors.

‎galleries/examples/statistics/customized_violin.py

Copy file name to clipboardExpand all lines: galleries/examples/statistics/customized_violin.py
+20-10Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,30 @@ def set_axis_style(ax, labels):
3636
np.random.seed(19680801)
3737
data = [sorted(np.random.normal(0, std, 100)) for std in range(1, 5)]
3838

39-
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(9, 4), sharey=True)
39+
fig, (ax1, ax2, ax3) = plt.subplots(nrows=1, ncols=3, figsize=(9, 3), sharey=True)
4040

4141
ax1.set_title('Default violin plot')
4242
ax1.set_ylabel('Observed values')
4343
ax1.violinplot(data)
4444

45-
ax2.set_title('Customized violin plot')
46-
parts = ax2.violinplot(
47-
data, showmeans=False, showmedians=False,
48-
showextrema=False)
45+
ax2.set_title('Set colors of violins')
46+
ax2.set_ylabel('Observed values')
47+
ax2.violinplot(
48+
data,
49+
facecolor=[('yellow', 0.3), ('blue', 0.3), ('red', 0.3), ('green', 0.3)],
50+
linecolor='black',
51+
)
52+
# Note that when passing a sequence of colors, the method will repeat the sequence if
53+
# less colors are provided than data distributions.
54+
55+
ax3.set_title('Customized violin plot')
56+
parts = ax3.violinplot(
57+
data, showmeans=False, showmedians=False, showextrema=False,
58+
facecolor='#D43F3A', linecolor='black')
4959

5060
for pc in parts['bodies']:
51-
pc.set_facecolor('#D43F3A')
5261
pc.set_edgecolor('black')
62+
pc.set_linewidth(1)
5363
pc.set_alpha(1)
5464

5565
quartile1, medians, quartile3 = np.percentile(data, [25, 50, 75], axis=1)
@@ -59,13 +69,13 @@ def set_axis_style(ax, labels):
5969
whiskers_min, whiskers_max = whiskers[:, 0], whiskers[:, 1]
6070

6171
inds = np.arange(1, len(medians) + 1)
62-
ax2.scatter(inds, medians, marker='o', color='white', s=30, zorder=3)
63-
ax2.vlines(inds, quartile1, quartile3, color='k', linestyle='-', lw=5)
64-
ax2.vlines(inds, whiskers_min, whiskers_max, color='k', linestyle='-', lw=1)
72+
ax3.scatter(inds, medians, marker='o', color='white', s=30, zorder=3)
73+
ax3.vlines(inds, quartile1, quartile3, color='k', linestyle='-', lw=5)
74+
ax3.vlines(inds, whiskers_min, whiskers_max, color='k', linestyle='-', lw=1)
6575

6676
# set style for the axes
6777
labels = ['A', 'B', 'C', 'D']
68-
for ax in [ax1, ax2]:
78+
for ax in [ax1, ax2, ax3]:
6979
set_axis_style(ax, labels)
7080

7181
plt.subplots_adjust(bottom=0.15, wspace=0.05)

‎lib/matplotlib/axes/_axes.py

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_axes.py
+68-9Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8464,7 +8464,8 @@ def matshow(self, Z, **kwargs):
84648464
def violinplot(self, dataset, positions=None, vert=None,
84658465
orientation='vertical', widths=0.5, showmeans=False,
84668466
showextrema=True, showmedians=False, quantiles=None,
8467-
points=100, bw_method=None, side='both',):
8467+
points=100, bw_method=None, side='both',
8468+
facecolor=None, linecolor=None):
84688469
"""
84698470
Make a violin plot.
84708471
@@ -8531,6 +8532,17 @@ def violinplot(self, dataset, positions=None, vert=None,
85318532
'both' plots standard violins. 'low'/'high' only
85328533
plots the side below/above the positions value.
85338534
8535+
facecolor : :mpltype:`color` or list of :mpltype:`color`, optional
8536+
If provided, will set the face color(s) of the violins.
8537+
8538+
.. versionadded:: 3.11
8539+
8540+
linecolor : :mpltype:`color` or list of :mpltype:`color`, optional
8541+
If provided, will set the line color(s) of the violins (the
8542+
horizontal and vertical spines and body edges).
8543+
8544+
.. versionadded:: 3.11
8545+
85348546
data : indexable object, optional
85358547
DATA_PARAMETER_PLACEHOLDER
85368548
@@ -8583,12 +8595,14 @@ def _kde_method(X, coords):
85838595
return self.violin(vpstats, positions=positions, vert=vert,
85848596
orientation=orientation, widths=widths,
85858597
showmeans=showmeans, showextrema=showextrema,
8586-
showmedians=showmedians, side=side)
8598+
showmedians=showmedians, side=side,
8599+
facecolor=facecolor, linecolor=linecolor)
85878600

85888601
@_api.make_keyword_only("3.9", "vert")
85898602
def violin(self, vpstats, positions=None, vert=None,
85908603
orientation='vertical', widths=0.5, showmeans=False,
8591-
showextrema=True, showmedians=False, side='both'):
8604+
showextrema=True, showmedians=False, side='both',
8605+
facecolor=None, linecolor=None):
85928606
"""
85938607
Draw a violin plot from pre-computed statistics.
85948608
@@ -8660,6 +8674,17 @@ def violin(self, vpstats, positions=None, vert=None,
86608674
'both' plots standard violins. 'low'/'high' only
86618675
plots the side below/above the positions value.
86628676
8677+
facecolor : :mpltype:`color` or list of :mpltype:`color`, optional
8678+
If provided, will set the face color(s) of the violins.
8679+
8680+
.. versionadded:: 3.11
8681+
8682+
linecolor : :mpltype:`color` or list of :mpltype:`color`, optional
8683+
If provided, will set the line color(s) of the violins (the
8684+
horizontal and vertical spines and body edges).
8685+
8686+
.. versionadded:: 3.11
8687+
86638688
Returns
86648689
-------
86658690
dict
@@ -8742,12 +8767,45 @@ def violin(self, vpstats, positions=None, vert=None,
87428767
[0.25 if side in ['both', 'high'] else 0]] \
87438768
* np.array(widths) + positions
87448769

8745-
# Colors.
8770+
# Make a cycle of color to iterate through, using 'none' as fallback
8771+
def cycle_color(color, alpha=None):
8772+
rgba = mcolors.to_rgba_array(color, alpha=alpha)
8773+
color_cycler = itertools.chain(itertools.cycle(rgba),
8774+
itertools.repeat('none'))
8775+
color_list = []
8776+
for _ in range(N):
8777+
color_list.append(next(color_cycler))
8778+
return color_list
8779+
8780+
# Convert colors to chain (number of colors can be different from len(vpstats))
8781+
if facecolor is None or linecolor is None:
8782+
if not mpl.rcParams['_internal.classic_mode']:
8783+
next_color = self._get_lines.get_next_color()
8784+
8785+
if facecolor is not None:
8786+
facecolor = cycle_color(facecolor)
8787+
else:
8788+
default_facealpha = 0.3
8789+
# Use default colors if user doesn't provide them
8790+
if mpl.rcParams['_internal.classic_mode']:
8791+
facecolor = cycle_color('y', alpha=default_facealpha)
8792+
else:
8793+
facecolor = cycle_color(next_color, alpha=default_facealpha)
8794+
87468795
if mpl.rcParams['_internal.classic_mode']:
8747-
fillcolor = 'y'
8748-
linecolor = 'r'
8796+
# Classic mode uses patch.force_edgecolor=True, so we need to
8797+
# set the edgecolor to make sure it has an alpha.
8798+
body_edgecolor = ("k", 0.3)
8799+
else:
8800+
body_edgecolor = None
8801+
8802+
if linecolor is not None:
8803+
linecolor = cycle_color(linecolor)
87498804
else:
8750-
fillcolor = linecolor = self._get_lines.get_next_color()
8805+
if mpl.rcParams['_internal.classic_mode']:
8806+
linecolor = cycle_color('r')
8807+
else:
8808+
linecolor = cycle_color(next_color)
87518809

87528810
# Check whether we are rendering vertically or horizontally
87538811
if orientation == 'vertical':
@@ -8773,14 +8831,15 @@ def violin(self, vpstats, positions=None, vert=None,
87738831

87748832
# Render violins
87758833
bodies = []
8776-
for stats, pos, width in zip(vpstats, positions, widths):
8834+
bodies_zip = zip(vpstats, positions, widths, facecolor)
8835+
for stats, pos, width, facecolor in bodies_zip:
87778836
# The 0.5 factor reflects the fact that we plot from v-p to v+p.
87788837
vals = np.array(stats['vals'])
87798838
vals = 0.5 * width * vals / vals.max()
87808839
bodies += [fill(stats['coords'],
87818840
-vals + pos if side in ['both', 'low'] else pos,
87828841
vals + pos if side in ['both', 'high'] else pos,
8783-
facecolor=fillcolor, alpha=0.3)]
8842+
facecolor=facecolor, edgecolor=body_edgecolor)]
87848843
means.append(stats['mean'])
87858844
mins.append(stats['min'])
87868845
maxes.append(stats['max'])

‎lib/matplotlib/axes/_axes.pyi

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_axes.pyi
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,8 @@ class Axes(_AxesBase):
755755
| Callable[[GaussianKDE], float]
756756
| None = ...,
757757
side: Literal["both", "low", "high"] = ...,
758+
facecolor: Sequence[ColorType] | ColorType | None = ...,
759+
linecolor: Sequence[ColorType] | ColorType | None = ...,
758760
data=...,
759761
) -> dict[str, Collection]: ...
760762
def violin(
@@ -769,6 +771,8 @@ class Axes(_AxesBase):
769771
showextrema: bool = ...,
770772
showmedians: bool = ...,
771773
side: Literal["both", "low", "high"] = ...,
774+
facecolor: Sequence[ColorType] | ColorType | None = ...,
775+
linecolor: Sequence[ColorType] | ColorType | None = ...,
772776
) -> dict[str, Collection]: ...
773777

774778
table = mtable.table

‎lib/matplotlib/pyplot.py

Copy file name to clipboardExpand all lines: lib/matplotlib/pyplot.py
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4296,6 +4296,8 @@ def violinplot(
42964296
| Callable[[GaussianKDE], float]
42974297
| None = None,
42984298
side: Literal["both", "low", "high"] = "both",
4299+
facecolor: Sequence[ColorType] | ColorType | None = None,
4300+
linecolor: Sequence[ColorType] | ColorType | None = None,
42994301
*,
43004302
data=None,
43014303
) -> dict[str, Collection]:
@@ -4312,6 +4314,8 @@ def violinplot(
43124314
points=points,
43134315
bw_method=bw_method,
43144316
side=side,
4317+
facecolor=facecolor,
4318+
linecolor=linecolor,
43154319
**({"data": data} if data is not None else {}),
43164320
)
43174321

‎lib/matplotlib/tests/test_axes.py

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_axes.py
+73Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4022,6 +4022,79 @@ def test_violinplot_outofrange_quantiles():
40224022
ax.violinplot(data, quantiles=[[-0.05, 0.2, 0.3, 0.75]])
40234023

40244024

4025+
@check_figures_equal(extensions=["png"])
4026+
def test_violinplot_color_specification(fig_test, fig_ref):
4027+
# Ensures that setting colors in violinplot constructor works
4028+
# the same way as setting the color of each object manually
4029+
np.random.seed(19680801)
4030+
data = [sorted(np.random.normal(0, std, 100)) for std in range(1, 4)]
4031+
kwargs = {'showmeans': True,
4032+
'showextrema': True,
4033+
'showmedians': True
4034+
}
4035+
4036+
def color_violins(parts, facecolor=None, linecolor=None):
4037+
"""Helper to color parts manually."""
4038+
if facecolor is not None:
4039+
for pc in parts['bodies']:
4040+
pc.set_facecolor(facecolor)
4041+
if linecolor is not None:
4042+
for partname in ('cbars', 'cmins', 'cmaxes', 'cmeans', 'cmedians'):
4043+
if partname in parts:
4044+
lc = parts[partname]
4045+
lc.set_edgecolor(linecolor)
4046+
4047+
# Reference image
4048+
ax = fig_ref.subplots(1, 3)
4049+
parts0 = ax[0].violinplot(data, **kwargs)
4050+
parts1 = ax[1].violinplot(data, **kwargs)
4051+
parts2 = ax[2].violinplot(data, **kwargs)
4052+
4053+
color_violins(parts0, facecolor=('r', 0.5), linecolor=('r', 0.2))
4054+
color_violins(parts1, facecolor='r')
4055+
color_violins(parts2, linecolor='r')
4056+
4057+
# Test image
4058+
ax = fig_test.subplots(1, 3)
4059+
ax[0].violinplot(data, facecolor=('r', 0.5), linecolor=('r', 0.2), **kwargs)
4060+
ax[1].violinplot(data, facecolor='r', **kwargs)
4061+
ax[2].violinplot(data, linecolor='r', **kwargs)
4062+
4063+
4064+
def test_violinplot_color_sequence():
4065+
# Ensures that setting a sequence of colors works the same as setting
4066+
# each color independently
4067+
np.random.seed(19680801)
4068+
data = [sorted(np.random.normal(0, std, 100)) for std in range(1, 5)]
4069+
kwargs = {'showmeans': True, 'showextrema': True, 'showmedians': True}
4070+
4071+
def assert_colors_equal(colors1, colors2):
4072+
assert all(mcolors.same_color(c1, c2)
4073+
for c1, c2 in zip(colors1, colors2))
4074+
4075+
# Color sequence
4076+
N = len(data)
4077+
positions = range(N)
4078+
facecolors = ['k', 'r', ('b', 0.5), ('g', 0.2)]
4079+
linecolors = [('y', 0.4), 'b', 'm', ('k', 0.8)]
4080+
4081+
# Test image
4082+
fig_test = plt.figure()
4083+
ax = fig_test.gca()
4084+
parts_test = ax.violinplot(data,
4085+
positions=positions,
4086+
facecolor=facecolors,
4087+
linecolor=linecolors,
4088+
**kwargs)
4089+
4090+
body_colors = [p.get_facecolor() for p in parts_test["bodies"]]
4091+
assert_colors_equal(body_colors, mcolors.to_rgba_array(facecolors))
4092+
4093+
for part in ["cbars", "cmins", "cmaxes", "cmeans", "cmedians"]:
4094+
colors_test = parts_test[part].get_edgecolor()
4095+
assert_colors_equal(colors_test, mcolors.to_rgba_array(linecolors))
4096+
4097+
40254098
@check_figures_equal(extensions=["png"])
40264099
def test_violinplot_single_list_quantiles(fig_test, fig_ref):
40274100
# Ensures quantile list for 1D can be passed in as single list

0 commit comments

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