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

Skip to content

Allow user to specify colors in violin plots with constructor method #27304

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jan 30, 2025
8 changes: 8 additions & 0 deletions doc/users/next_whats_new/violinplot_colors.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
``violinplot`` now accepts color arguments
-------------------------------------------

`~.Axes.violinplot` and `~.Axes.violin` now accept ``facecolor`` and
``linecolor`` as input arguments. This means that users can set the color of
violinplots as they make them, rather than setting the color of individual
objects afterwards. It is possible to pass a single color to be used for all
violins, or pass a sequence of colors.
30 changes: 20 additions & 10 deletions galleries/examples/statistics/customized_violin.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,30 @@ def set_axis_style(ax, labels):
np.random.seed(19680801)
data = [sorted(np.random.normal(0, std, 100)) for std in range(1, 5)]

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

ax1.set_title('Default violin plot')
ax1.set_ylabel('Observed values')
ax1.violinplot(data)

ax2.set_title('Customized violin plot')
parts = ax2.violinplot(
data, showmeans=False, showmedians=False,
showextrema=False)
ax2.set_title('Set colors of violins')
ax2.set_ylabel('Observed values')
ax2.violinplot(
data,
facecolor=[('yellow', 0.3), ('blue', 0.3), ('red', 0.3), ('green', 0.3)],
linecolor='black',
)
# Note that when passing a sequence of colors, the method will repeat the sequence if
# less colors are provided than data distributions.

ax3.set_title('Customized violin plot')
parts = ax3.violinplot(
data, showmeans=False, showmedians=False, showextrema=False,
facecolor='#D43F3A', linecolor='black')

for pc in parts['bodies']:
pc.set_facecolor('#D43F3A')
pc.set_edgecolor('black')
pc.set_linewidth(1)
pc.set_alpha(1)

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

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

# set style for the axes
labels = ['A', 'B', 'C', 'D']
for ax in [ax1, ax2]:
for ax in [ax1, ax2, ax3]:
set_axis_style(ax, labels)

plt.subplots_adjust(bottom=0.15, wspace=0.05)
Expand Down
77 changes: 68 additions & 9 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8439,7 +8439,8 @@ def matshow(self, Z, **kwargs):
def violinplot(self, dataset, positions=None, vert=None,
orientation='vertical', widths=0.5, showmeans=False,
showextrema=True, showmedians=False, quantiles=None,
points=100, bw_method=None, side='both',):
points=100, bw_method=None, side='both',
facecolor=None, linecolor=None):
"""
Make a violin plot.

Expand Down Expand Up @@ -8506,6 +8507,17 @@ def violinplot(self, dataset, positions=None, vert=None,
'both' plots standard violins. 'low'/'high' only
plots the side below/above the positions value.

facecolor : :mpltype:`color` or list of :mpltype:`color`, optional
If provided, will set the face color(s) of the violins.

.. versionadded:: 3.11

linecolor : :mpltype:`color` or list of :mpltype:`color`, optional
If provided, will set the line color(s) of the violins (the
horizontal and vertical spines and body edges).

.. versionadded:: 3.11

data : indexable object, optional
DATA_PARAMETER_PLACEHOLDER

Expand Down Expand Up @@ -8558,12 +8570,14 @@ def _kde_method(X, coords):
return self.violin(vpstats, positions=positions, vert=vert,
orientation=orientation, widths=widths,
showmeans=showmeans, showextrema=showextrema,
showmedians=showmedians, side=side)
showmedians=showmedians, side=side,
facecolor=facecolor, linecolor=linecolor)

@_api.make_keyword_only("3.9", "vert")
def violin(self, vpstats, positions=None, vert=None,
orientation='vertical', widths=0.5, showmeans=False,
showextrema=True, showmedians=False, side='both'):
showextrema=True, showmedians=False, side='both',
facecolor=None, linecolor=None):
"""
Draw a violin plot from pre-computed statistics.

Expand Down Expand Up @@ -8635,6 +8649,17 @@ def violin(self, vpstats, positions=None, vert=None,
'both' plots standard violins. 'low'/'high' only
plots the side below/above the positions value.

facecolor : :mpltype:`color` or list of :mpltype:`color`, optional
If provided, will set the face color(s) of the violins.

.. versionadded:: 3.11

linecolor : :mpltype:`color` or list of :mpltype:`color`, optional
If provided, will set the line color(s) of the violins (the
horizontal and vertical spines and body edges).

.. versionadded:: 3.11

Returns
-------
dict
Expand Down Expand Up @@ -8717,12 +8742,45 @@ def violin(self, vpstats, positions=None, vert=None,
[0.25 if side in ['both', 'high'] else 0]] \
* np.array(widths) + positions

# Colors.
# Make a cycle of color to iterate through, using 'none' as fallback
def cycle_color(color, alpha=None):
rgba = mcolors.to_rgba_array(color, alpha=alpha)
color_cycler = itertools.chain(itertools.cycle(rgba),
itertools.repeat('none'))
color_list = []
for _ in range(N):
color_list.append(next(color_cycler))
return color_list

# Convert colors to chain (number of colors can be different from len(vpstats))
if facecolor is None or linecolor is None:
if not mpl.rcParams['_internal.classic_mode']:
next_color = self._get_lines.get_next_color()

if facecolor is not None:
facecolor = cycle_color(facecolor)
else:
default_facealpha = 0.3
# Use default colors if user doesn't provide them
if mpl.rcParams['_internal.classic_mode']:
facecolor = cycle_color('y', alpha=default_facealpha)
else:
facecolor = cycle_color(next_color, alpha=default_facealpha)

if mpl.rcParams['_internal.classic_mode']:
fillcolor = 'y'
linecolor = 'r'
# Classic mode uses patch.force_edgecolor=True, so we need to
# set the edgecolor to make sure it has an alpha.
body_edgecolor = ("k", 0.3)
else:
body_edgecolor = None

if linecolor is not None:
linecolor = cycle_color(linecolor)
else:
fillcolor = linecolor = self._get_lines.get_next_color()
if mpl.rcParams['_internal.classic_mode']:
linecolor = cycle_color('r')
else:
linecolor = cycle_color(next_color)

# Check whether we are rendering vertically or horizontally
if orientation == 'vertical':
Expand All @@ -8748,14 +8806,15 @@ def violin(self, vpstats, positions=None, vert=None,

# Render violins
bodies = []
for stats, pos, width in zip(vpstats, positions, widths):
bodies_zip = zip(vpstats, positions, widths, facecolor)
for stats, pos, width, facecolor in bodies_zip:
# The 0.5 factor reflects the fact that we plot from v-p to v+p.
vals = np.array(stats['vals'])
vals = 0.5 * width * vals / vals.max()
bodies += [fill(stats['coords'],
-vals + pos if side in ['both', 'low'] else pos,
vals + pos if side in ['both', 'high'] else pos,
facecolor=fillcolor, alpha=0.3)]
facecolor=facecolor, edgecolor=body_edgecolor)]
means.append(stats['mean'])
mins.append(stats['min'])
maxes.append(stats['max'])
Expand Down
4 changes: 4 additions & 0 deletions lib/matplotlib/axes/_axes.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,8 @@ class Axes(_AxesBase):
| Callable[[GaussianKDE], float]
| None = ...,
side: Literal["both", "low", "high"] = ...,
facecolor: Sequence[ColorType] | ColorType | None = ...,
linecolor: Sequence[ColorType] | ColorType | None = ...,
data=...,
) -> dict[str, Collection]: ...
def violin(
Expand All @@ -769,6 +771,8 @@ class Axes(_AxesBase):
showextrema: bool = ...,
showmedians: bool = ...,
side: Literal["both", "low", "high"] = ...,
facecolor: Sequence[ColorType] | ColorType | None = ...,
linecolor: Sequence[ColorType] | ColorType | None = ...,
) -> dict[str, Collection]: ...

table = mtable.table
Expand Down
4 changes: 4 additions & 0 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4302,6 +4302,8 @@ def violinplot(
| Callable[[GaussianKDE], float]
| None = None,
side: Literal["both", "low", "high"] = "both",
facecolor: Sequence[ColorType] | ColorType | None = None,
linecolor: Sequence[ColorType] | ColorType | None = None,
*,
data=None,
) -> dict[str, Collection]:
Expand All @@ -4318,6 +4320,8 @@ def violinplot(
points=points,
bw_method=bw_method,
side=side,
facecolor=facecolor,
linecolor=linecolor,
**({"data": data} if data is not None else {}),
)

Expand Down
73 changes: 73 additions & 0 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4021,6 +4021,79 @@ def test_violinplot_outofrange_quantiles():
ax.violinplot(data, quantiles=[[-0.05, 0.2, 0.3, 0.75]])


@check_figures_equal(extensions=["png"])
def test_violinplot_color_specification(fig_test, fig_ref):
# Ensures that setting colors in violinplot constructor works
# the same way as setting the color of each object manually
np.random.seed(19680801)
data = [sorted(np.random.normal(0, std, 100)) for std in range(1, 4)]
kwargs = {'showmeans': True,
'showextrema': True,
'showmedians': True
}

def color_violins(parts, facecolor=None, linecolor=None):
"""Helper to color parts manually."""
if facecolor is not None:
for pc in parts['bodies']:
pc.set_facecolor(facecolor)
if linecolor is not None:
for partname in ('cbars', 'cmins', 'cmaxes', 'cmeans', 'cmedians'):
if partname in parts:
lc = parts[partname]
lc.set_edgecolor(linecolor)

# Reference image
ax = fig_ref.subplots(1, 3)
parts0 = ax[0].violinplot(data, **kwargs)
parts1 = ax[1].violinplot(data, **kwargs)
parts2 = ax[2].violinplot(data, **kwargs)

color_violins(parts0, facecolor=('r', 0.5), linecolor=('r', 0.2))
color_violins(parts1, facecolor='r')
color_violins(parts2, linecolor='r')

# Test image
ax = fig_test.subplots(1, 3)
ax[0].violinplot(data, facecolor=('r', 0.5), linecolor=('r', 0.2), **kwargs)
ax[1].violinplot(data, facecolor='r', **kwargs)
ax[2].violinplot(data, linecolor='r', **kwargs)


def test_violinplot_color_sequence():
# Ensures that setting a sequence of colors works the same as setting
# each color independently
np.random.seed(19680801)
data = [sorted(np.random.normal(0, std, 100)) for std in range(1, 5)]
kwargs = {'showmeans': True, 'showextrema': True, 'showmedians': True}

def assert_colors_equal(colors1, colors2):
assert all(mcolors.same_color(c1, c2)
for c1, c2 in zip(colors1, colors2))

# Color sequence
N = len(data)
positions = range(N)
facecolors = ['k', 'r', ('b', 0.5), ('g', 0.2)]
linecolors = [('y', 0.4), 'b', 'm', ('k', 0.8)]

# Test image
fig_test = plt.figure()
ax = fig_test.gca()
parts_test = ax.violinplot(data,
positions=positions,
facecolor=facecolors,
linecolor=linecolors,
**kwargs)

body_colors = [p.get_facecolor() for p in parts_test["bodies"]]
assert_colors_equal(body_colors, mcolors.to_rgba_array(facecolors))

for part in ["cbars", "cmins", "cmaxes", "cmeans", "cmedians"]:
colors_test = parts_test[part].get_edgecolor()
assert_colors_equal(colors_test, mcolors.to_rgba_array(linecolors))


@check_figures_equal(extensions=["png"])
def test_violinplot_single_list_quantiles(fig_test, fig_ref):
# Ensures quantile list for 1D can be passed in as single list
Expand Down