diff --git a/doc/users/next_whats_new/violinplot_colors.rst b/doc/users/next_whats_new/violinplot_colors.rst new file mode 100644 index 000000000000..179f868c4288 --- /dev/null +++ b/doc/users/next_whats_new/violinplot_colors.rst @@ -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. diff --git a/galleries/examples/statistics/customized_violin.py b/galleries/examples/statistics/customized_violin.py index 29ddcda92fbe..cc18e47ebd67 100644 --- a/galleries/examples/statistics/customized_violin.py +++ b/galleries/examples/statistics/customized_violin.py @@ -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) @@ -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) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index bb0adae18e37..16377baf8351 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -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. @@ -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 @@ -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. @@ -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 @@ -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': @@ -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']) diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index 1877cc192b15..c780fe012fa0 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -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( @@ -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 diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 2dd14404c98e..ffbef75aa86d 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -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]: @@ -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 {}), ) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 37f799e522a4..2fb7c273b5e2 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -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