From 716f0a7f991d187cc00e92eb3eb381b71241efad Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 30 Jul 2022 17:44:59 -0400 Subject: [PATCH 01/11] Add labels argument to bar(). --- lib/matplotlib/axes/_axes.py | 24 +++++++++++++++++++----- lib/matplotlib/tests/test_axes.py | 16 ++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 25b3cf36651f..f5d053569b01 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2236,6 +2236,10 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", To align the bars on the right edge pass a negative *width* and ``align='edge'``. + labels : str or list of str, optional + A sequence of labels to assign to each data series. + If unspecified, then ``'_nolegend_'`` will be applied to each bar. + Returns ------- `.BarContainer` @@ -2302,8 +2306,6 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", """ kwargs = cbook.normalize_kwargs(kwargs, mpatches.Patch) color = kwargs.pop('color', None) - if color is None: - color = self._get_patches_for_fill.get_next_color() edgecolor = kwargs.pop('edgecolor', None) linewidth = kwargs.pop('linewidth', None) hatch = kwargs.pop('hatch', None) @@ -2381,6 +2383,18 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", tick_label_axis = self.yaxis tick_label_position = y + patch_labels = kwargs.pop('labels', ['_nolegend_'] * len(x)) + if len(patch_labels) != len(x): + raise ValueError(f'number of labels ({len(patch_labels)}) ' + f'does not match number of bars ({len(x)}).') + if patch_labels[0] != '_nolegend_' and color is None: + color = [ + self._get_patches_for_fill.get_next_color() + for _ in patch_labels + ] + elif color is None: + color = self._get_patches_for_fill.get_next_color() + linewidth = itertools.cycle(np.atleast_1d(linewidth)) hatch = itertools.cycle(np.atleast_1d(hatch)) color = itertools.chain(itertools.cycle(mcolors.to_rgba_array(color)), @@ -2420,14 +2434,14 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", patches = [] args = zip(left, bottom, width, height, color, edgecolor, linewidth, - hatch) - for l, b, w, h, c, e, lw, htch in args: + hatch, patch_labels) + for l, b, w, h, c, e, lw, htch, lbl in args: r = mpatches.Rectangle( xy=(l, b), width=w, height=h, facecolor=c, edgecolor=e, linewidth=lw, - label='_nolegend_', + label=lbl, hatch=htch, ) r._internal_update(kwargs) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 51fbea3cfaaa..d90f0656957f 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1886,6 +1886,22 @@ def test_bar_hatches(fig_test, fig_ref): ax_test.bar(x, y, hatch=hatches) +@pytest.mark.parametrize( + ("x", "width", "labels", "color"), + [ + ("x", 1, "x", None), + ("x", 1, "x", "black"), + (["a", "b", "c"], [10, 20, 15], ["A", "B", "C"], None), + (["a", "b", "c"], [10, 20, 15], ["A", "B", "C"], + ["black", "blue", "orange"]), + ] +) +def test_bar_labels(x, width, labels, color): + _, ax = plt.subplots() + ax.bar(x, width, labels=labels, color=color) + ax.legend() + + def test_pandas_minimal_plot(pd): # smoke test that series and index objects do not warn for x in [pd.Series([1, 2], dtype="float64"), From 03b652483cdbfc13ab105de41b896613c6c8bc39 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 30 Jul 2022 17:58:57 -0400 Subject: [PATCH 02/11] Add what's new users entry. --- doc/users/next_whats_new/bar_plot_labels | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 doc/users/next_whats_new/bar_plot_labels diff --git a/doc/users/next_whats_new/bar_plot_labels b/doc/users/next_whats_new/bar_plot_labels new file mode 100644 index 000000000000..c57eb2bc283d --- /dev/null +++ b/doc/users/next_whats_new/bar_plot_labels @@ -0,0 +1,16 @@ +Easier labelling of bars in bar plot +------------------------------------ + +The new ``labels`` argument of `~matplotlib.axes.Axes.bar` can now +be used to label each of the bars. + +.. code-block:: python + + import matplotlib.pyplot as plt + + x = ["a", "b", "c"] + y = [10, 20, 15] + + fig, ax = plt.subplots() + _ = ax.barh(x, y, labels=x) + ax.legend() From 3dbe540cc34e4b26ee083ca1472a0d2e9840d4eb Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 30 Jul 2022 18:19:31 -0400 Subject: [PATCH 03/11] Fix doc entry. --- .../next_whats_new/{bar_plot_labels => bar_plot_labels.rst} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename doc/users/next_whats_new/{bar_plot_labels => bar_plot_labels.rst} (98%) diff --git a/doc/users/next_whats_new/bar_plot_labels b/doc/users/next_whats_new/bar_plot_labels.rst similarity index 98% rename from doc/users/next_whats_new/bar_plot_labels rename to doc/users/next_whats_new/bar_plot_labels.rst index c57eb2bc283d..1cd942fa96e4 100644 --- a/doc/users/next_whats_new/bar_plot_labels +++ b/doc/users/next_whats_new/bar_plot_labels.rst @@ -1,7 +1,7 @@ Easier labelling of bars in bar plot ------------------------------------ -The new ``labels`` argument of `~matplotlib.axes.Axes.bar` can now +The new ``labels`` argument of `~matplotlib.axes.Axes.bar` can now be used to label each of the bars. .. code-block:: python From b975db21474baf6bd88ba5c1d0abdbac9df1c87e Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 30 Jul 2022 18:40:18 -0400 Subject: [PATCH 04/11] Add test for mismatch in label length and data length. --- lib/matplotlib/tests/test_axes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index d90f0656957f..2b8cee19c8c1 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1902,6 +1902,12 @@ def test_bar_labels(x, width, labels, color): ax.legend() +def test_bar_labels_length(): + _, ax = plt.subplots() + with pytest.raises(ValueError): + ax.bar(["x", "y"], [1, 2], labels="X") + + def test_pandas_minimal_plot(pd): # smoke test that series and index objects do not warn for x in [pd.Series([1, 2], dtype="float64"), From a0ed19657013515b87505d928dd339da91c0fdb6 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 31 Jul 2022 11:23:14 -0400 Subject: [PATCH 05/11] Switch to 'label' argument. --- doc/users/next_whats_new/bar_plot_labels.rst | 8 +++---- lib/matplotlib/axes/_axes.py | 22 +++++++------------- lib/matplotlib/tests/test_axes.py | 19 +++++++++-------- 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/doc/users/next_whats_new/bar_plot_labels.rst b/doc/users/next_whats_new/bar_plot_labels.rst index 1cd942fa96e4..6da57a317f59 100644 --- a/doc/users/next_whats_new/bar_plot_labels.rst +++ b/doc/users/next_whats_new/bar_plot_labels.rst @@ -1,8 +1,8 @@ Easier labelling of bars in bar plot ------------------------------------ -The new ``labels`` argument of `~matplotlib.axes.Axes.bar` can now -be used to label each of the bars. +The ``label`` argument of `~matplotlib.axes.Axes.bar` can now +be passed a list of labels for the bars. .. code-block:: python @@ -12,5 +12,5 @@ be used to label each of the bars. y = [10, 20, 15] fig, ax = plt.subplots() - _ = ax.barh(x, y, labels=x) - ax.legend() + bar_container = ax.barh(x, y, label=x) + [bar.get_label() for bar in bar_container] diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index f5d053569b01..f9f579e4995f 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2236,10 +2236,6 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", To align the bars on the right edge pass a negative *width* and ``align='edge'``. - labels : str or list of str, optional - A sequence of labels to assign to each data series. - If unspecified, then ``'_nolegend_'`` will be applied to each bar. - Returns ------- `.BarContainer` @@ -2306,6 +2302,8 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", """ kwargs = cbook.normalize_kwargs(kwargs, mpatches.Patch) color = kwargs.pop('color', None) + if color is None: + color = self._get_patches_for_fill.get_next_color() edgecolor = kwargs.pop('edgecolor', None) linewidth = kwargs.pop('linewidth', None) hatch = kwargs.pop('hatch', None) @@ -2383,17 +2381,13 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", tick_label_axis = self.yaxis tick_label_position = y - patch_labels = kwargs.pop('labels', ['_nolegend_'] * len(x)) + patch_labels = np.atleast_1d(label) if len(patch_labels) != len(x): - raise ValueError(f'number of labels ({len(patch_labels)}) ' - f'does not match number of bars ({len(x)}).') - if patch_labels[0] != '_nolegend_' and color is None: - color = [ - self._get_patches_for_fill.get_next_color() - for _ in patch_labels - ] - elif color is None: - color = self._get_patches_for_fill.get_next_color() + if len(patch_labels) == 1: + patch_labels = ['_nolegend_'] * len(x) + else: + raise ValueError(f'number of labels ({len(patch_labels)}) ' + f'does not match number of bars ({len(x)}).') linewidth = itertools.cycle(np.atleast_1d(linewidth)) hatch = itertools.cycle(np.atleast_1d(hatch)) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 2b8cee19c8c1..63c058b4974c 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1887,25 +1887,26 @@ def test_bar_hatches(fig_test, fig_ref): @pytest.mark.parametrize( - ("x", "width", "labels", "color"), + ("x", "width", "label", "expected_labels"), [ - ("x", 1, "x", None), - ("x", 1, "x", "black"), - (["a", "b", "c"], [10, 20, 15], ["A", "B", "C"], None), + ("x", 1, "x", ["x"]), (["a", "b", "c"], [10, 20, 15], ["A", "B", "C"], - ["black", "blue", "orange"]), + ["A", "B", "C"]), + (["a", "b", "c"], [10, 20, 15], "bars", + ["_nolegend_", "_nolegend_", "_nolegend_"]), ] ) -def test_bar_labels(x, width, labels, color): +def test_bar_labels(x, width, label, expected_labels): _, ax = plt.subplots() - ax.bar(x, width, labels=labels, color=color) - ax.legend() + bar_container = ax.bar(x, width, label=label) + bar_labels = [bar.get_label() for bar in bar_container] + assert expected_labels == bar_labels def test_bar_labels_length(): _, ax = plt.subplots() with pytest.raises(ValueError): - ax.bar(["x", "y"], [1, 2], labels="X") + ax.bar(["x", "y"], [1, 2], label=["X", "Y", "Z"]) def test_pandas_minimal_plot(pd): From bc5b1a1ef03521777dffca4d29a74ad2e3bb75c1 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Wed, 17 Aug 2022 22:11:46 -0400 Subject: [PATCH 06/11] Handle duplicate bar labels in legend; address PR comments. --- lib/matplotlib/axes/_axes.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index f9f579e4995f..d1c5989b8299 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2256,6 +2256,13 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", The tick labels of the bars. Default: None (Use default numeric labels.) + label : str or list of str, optional + A single label is attached to the resulting BarContainer as a + label for the whole dataset. + If a list is given, it must be the same length as *x* and + labels the individual bars. For example this may used with + lists of *color*. + xerr, yerr : float or array-like of shape(N,) or shape(2, N), optional If not *None*, add horizontal / vertical errorbars to the bar tips. The values are +/- sizes relative to the data: @@ -2381,13 +2388,21 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", tick_label_axis = self.yaxis tick_label_position = y - patch_labels = np.atleast_1d(label) + if not isinstance(label, str) and np.iterable(label): + bar_container_label = '_nolegend_' + seen_labels = set() + patch_labels = label + for i, patch_label in enumerate(patch_labels): + if patch_label in seen_labels: + patch_labels[i] = '_nolegend_' + else: + seen_labels.add(patch_label) + else: + bar_container_label = label + patch_labels = ['_nolegend_'] * len(x) if len(patch_labels) != len(x): - if len(patch_labels) == 1: - patch_labels = ['_nolegend_'] * len(x) - else: - raise ValueError(f'number of labels ({len(patch_labels)}) ' - f'does not match number of bars ({len(x)}).') + raise ValueError(f'number of labels ({len(patch_labels)}) ' + f'does not match number of bars ({len(x)}).') linewidth = itertools.cycle(np.atleast_1d(linewidth)) hatch = itertools.cycle(np.atleast_1d(hatch)) @@ -2474,7 +2489,8 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", datavalues = width bar_container = BarContainer(patches, errorbar, datavalues=datavalues, - orientation=orientation, label=label) + orientation=orientation, + label=bar_container_label) self.add_container(bar_container) if tick_labels is not None: From 72fd2b4689936a02e4318e6d74e8298c0f746d5f Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Wed, 17 Aug 2022 22:29:37 -0400 Subject: [PATCH 07/11] Add tests. --- lib/matplotlib/tests/test_axes.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 63c058b4974c..387cdfe70642 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1887,26 +1887,41 @@ def test_bar_hatches(fig_test, fig_ref): @pytest.mark.parametrize( - ("x", "width", "label", "expected_labels"), + ("x", "width", "label", "expected_labels", "container_label"), [ - ("x", 1, "x", ["x"]), + ("x", 1, "x", ["_nolegend_"], "x"), (["a", "b", "c"], [10, 20, 15], ["A", "B", "C"], - ["A", "B", "C"]), + ["A", "B", "C"], "_nolegend_"), + (["a", "b", "c"], [10, 20, 15], ["R", "Y", "R"], + ["R", "Y", "_nolegend_"], "_nolegend_"), (["a", "b", "c"], [10, 20, 15], "bars", - ["_nolegend_", "_nolegend_", "_nolegend_"]), + ["_nolegend_", "_nolegend_", "_nolegend_"], "bars"), ] ) -def test_bar_labels(x, width, label, expected_labels): +def test_bar_labels(x, width, label, expected_labels, container_label): _, ax = plt.subplots() bar_container = ax.bar(x, width, label=label) bar_labels = [bar.get_label() for bar in bar_container] assert expected_labels == bar_labels + assert bar_container.get_label() == container_label def test_bar_labels_length(): _, ax = plt.subplots() with pytest.raises(ValueError): ax.bar(["x", "y"], [1, 2], label=["X", "Y", "Z"]) + _, ax = plt.subplots() + with pytest.raises(ValueError): + ax.bar(["x", "y"], [1, 2], label=["X"]) + + +def test_duplicate_bar_labels_in_legend(): + _, ax = plt.subplots() + x = ["a", "b", "c"] + y = [2, 1, 3] + labels = ["Red", "Yellow", "Red"] + ax.bar(x, y, label=labels) + assert [text.get_text() for text in ax.legend().texts] == labels[:2] def test_pandas_minimal_plot(pd): From 8a5ab0e409e45e7f0073b1e2b7c76036a67aeb66 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Wed, 17 Aug 2022 22:31:49 -0400 Subject: [PATCH 08/11] Update label description in docstring. --- lib/matplotlib/axes/_axes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index d1c5989b8299..b75ffe895955 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2257,10 +2257,10 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", Default: None (Use default numeric labels.) label : str or list of str, optional - A single label is attached to the resulting BarContainer as a + A single label is attached to the resulting `.BarContainer` as a label for the whole dataset. - If a list is given, it must be the same length as *x* and - labels the individual bars. For example this may used with + If a list is provided, it must be the same length as *x* and + labels the individual bars. For example, this may used with lists of *color*. xerr, yerr : float or array-like of shape(N,) or shape(2, N), optional From f71dc0c62591078a8e2372097bcd15b0776850ab Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Wed, 17 Aug 2022 22:37:05 -0400 Subject: [PATCH 09/11] Remove duplicate label filtering. --- lib/matplotlib/axes/_axes.py | 6 ------ lib/matplotlib/tests/test_axes.py | 11 +---------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index b75ffe895955..b9353b9b1617 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2390,13 +2390,7 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", if not isinstance(label, str) and np.iterable(label): bar_container_label = '_nolegend_' - seen_labels = set() patch_labels = label - for i, patch_label in enumerate(patch_labels): - if patch_label in seen_labels: - patch_labels[i] = '_nolegend_' - else: - seen_labels.add(patch_label) else: bar_container_label = label patch_labels = ['_nolegend_'] * len(x) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 387cdfe70642..3a1ba341b2e0 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1892,7 +1892,7 @@ def test_bar_hatches(fig_test, fig_ref): ("x", 1, "x", ["_nolegend_"], "x"), (["a", "b", "c"], [10, 20, 15], ["A", "B", "C"], ["A", "B", "C"], "_nolegend_"), - (["a", "b", "c"], [10, 20, 15], ["R", "Y", "R"], + (["a", "b", "c"], [10, 20, 15], ["R", "Y", "_nolegend_"], ["R", "Y", "_nolegend_"], "_nolegend_"), (["a", "b", "c"], [10, 20, 15], "bars", ["_nolegend_", "_nolegend_", "_nolegend_"], "bars"), @@ -1915,15 +1915,6 @@ def test_bar_labels_length(): ax.bar(["x", "y"], [1, 2], label=["X"]) -def test_duplicate_bar_labels_in_legend(): - _, ax = plt.subplots() - x = ["a", "b", "c"] - y = [2, 1, 3] - labels = ["Red", "Yellow", "Red"] - ax.bar(x, y, label=labels) - assert [text.get_text() for text in ax.legend().texts] == labels[:2] - - def test_pandas_minimal_plot(pd): # smoke test that series and index objects do not warn for x in [pd.Series([1, 2], dtype="float64"), From 6ad5dcf409ab17021f0a877730034b29be2274f1 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Thu, 18 Aug 2022 08:38:27 -0400 Subject: [PATCH 10/11] Add note on repeated labels. --- lib/matplotlib/axes/_axes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index b9353b9b1617..e265da224bdc 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2261,7 +2261,8 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", label for the whole dataset. If a list is provided, it must be the same length as *x* and labels the individual bars. For example, this may used with - lists of *color*. + lists of *color*. Note that behavior for repeated labels is + not defined and may change in the future. xerr, yerr : float or array-like of shape(N,) or shape(2, N), optional If not *None*, add horizontal / vertical errorbars to the bar tips. From 9f55a015650b81cbd75b22f922dffe7bba8934c0 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Thu, 18 Aug 2022 17:40:01 -0400 Subject: [PATCH 11/11] Update docstring per PR. Co-authored-by: Elliott Sales de Andrade --- lib/matplotlib/axes/_axes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index e265da224bdc..3159a55b7e83 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2260,9 +2260,9 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", A single label is attached to the resulting `.BarContainer` as a label for the whole dataset. If a list is provided, it must be the same length as *x* and - labels the individual bars. For example, this may used with - lists of *color*. Note that behavior for repeated labels is - not defined and may change in the future. + labels the individual bars. Repeated labels are not de-duplicated + and will cause repeated label entries, so this is best used when + bars also differ in style (e.g., by passing a list to *color*.) xerr, yerr : float or array-like of shape(N,) or shape(2, N), optional If not *None*, add horizontal / vertical errorbars to the bar tips.