Allow user to specify colors in violin plots with constructor method by landoskape · Pull Request #27304 · matplotlib/matplotlib · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions doc/users/next_whats_new/violinplot_colors.rst
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')
Comment thread
landoskape marked this conversation as resolved.
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.

Comment thread
oscargus marked this conversation as resolved.
.. 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