diff --git a/doc/source/user_guide/visualization.rst b/doc/source/user_guide/visualization.rst index 6ba5cab71bf14..27826e7cde9e1 100644 --- a/doc/source/user_guide/visualization.rst +++ b/doc/source/user_guide/visualization.rst @@ -1108,6 +1108,34 @@ shown by default. plt.close('all') + +Controlling the labels +~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.1.0 + +You may set the ``xlabel`` and ``ylabel`` arguments to give the plot custom labels +for x and y axis. By default, pandas will pick up index name as xlabel, while leaving +it empty for ylabel. + +.. ipython:: python + :suppress: + + plt.figure() + +.. ipython:: python + + df.plot() + + @savefig plot_xlabel_ylabel.png + df.plot(xlabel="new x", ylabel="new y") + +.. ipython:: python + :suppress: + + plt.close('all') + + Scales ~~~~~~ diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index 10dac7e2863f9..64e327797808e 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -303,6 +303,7 @@ Other enhancements :class:`~pandas.io.stata.StataWriter`, :class:`~pandas.io.stata.StataWriter117`, and :class:`~pandas.io.stata.StataWriterUTF8` (:issue:`26599`). - :meth:`HDFStore.put` now accepts `track_times` parameter. Parameter is passed to ``create_table`` method of ``PyTables`` (:issue:`32682`). +- :meth:`Series.plot` and :meth:`DataFrame.plot` now accepts `xlabel` and `ylabel` parameters to present labels on x and y axis (:issue:`9093`). - Make :class:`pandas.core.window.Rolling` and :class:`pandas.core.window.Expanding` iterable(:issue:`11704`) - Make ``option_context`` a :class:`contextlib.ContextDecorator`, which allows it to be used as a decorator over an entire function (:issue:`34253`). - :meth:`DataFrame.to_csv` and :meth:`Series.to_csv` now accept an ``errors`` argument (:issue:`22610`) diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py index 4eb68367560b6..3a8cc5c299640 100644 --- a/pandas/plotting/_core.py +++ b/pandas/plotting/_core.py @@ -673,6 +673,16 @@ class PlotAccessor(PandasObject): Set the x limits of the current axes. ylim : 2-tuple/list Set the y limits of the current axes. + xlabel : label, optional + Name to use for the xlabel on x-axis. Default uses index name as xlabel. + + .. versionadded:: 1.1.0 + + ylabel : label, optional + Name to use for the ylabel on y-axis. Default will show no ylabel. + + .. versionadded:: 1.1.0 + rot : int, default None Rotation for ticks (xticks for vertical, yticks for horizontal plots). @@ -779,6 +789,8 @@ def _get_call_args(backend_name, data, args, kwargs): ("xerr", None), ("label", None), ("secondary_y", False), + ("xlabel", None), + ("ylabel", None), ] elif isinstance(data, ABCDataFrame): arg_def = [ @@ -811,6 +823,8 @@ def _get_call_args(backend_name, data, args, kwargs): ("xerr", None), ("secondary_y", False), ("sort_columns", False), + ("xlabel", None), + ("ylabel", None), ] else: raise TypeError( diff --git a/pandas/plotting/_matplotlib/core.py b/pandas/plotting/_matplotlib/core.py index f3682e0a008a6..e510f7140519a 100644 --- a/pandas/plotting/_matplotlib/core.py +++ b/pandas/plotting/_matplotlib/core.py @@ -1,9 +1,11 @@ import re -from typing import Optional +from typing import List, Optional import warnings +from matplotlib.artist import Artist import numpy as np +from pandas._typing import Label from pandas.errors import AbstractMethodError from pandas.util._decorators import cache_readonly @@ -97,6 +99,8 @@ def __init__( ylim=None, xticks=None, yticks=None, + xlabel: Optional[Label] = None, + ylabel: Optional[Label] = None, sort_columns=False, fontsize=None, secondary_y=False, @@ -138,6 +142,8 @@ def __init__( self.ylim = ylim self.title = title self.use_index = use_index + self.xlabel = xlabel + self.ylabel = ylabel self.fontsize = fontsize @@ -155,8 +161,8 @@ def __init__( self.grid = grid self.legend = legend - self.legend_handles = [] - self.legend_labels = [] + self.legend_handles: List[Artist] = [] + self.legend_labels: List[Label] = [] for attr in self._pop_attributes: value = kwds.pop(attr, self._attr_defaults.get(attr, None)) @@ -482,6 +488,11 @@ def _adorn_subplots(self): if self.xlim is not None: ax.set_xlim(self.xlim) + # GH9093, currently Pandas does not show ylabel, so if users provide + # ylabel will set it as ylabel in the plot. + if self.ylabel is not None: + ax.set_ylabel(pprint_thing(self.ylabel)) + ax.grid(self.grid) if self.title: @@ -668,6 +679,10 @@ def _get_index_name(self): if name is not None: name = pprint_thing(name) + # GH 9093, override the default xlabel if xlabel is provided. + if self.xlabel is not None: + name = pprint_thing(self.xlabel) + return name @classmethod diff --git a/pandas/tests/plotting/test_frame.py b/pandas/tests/plotting/test_frame.py index e4299490e7601..3d85e79b15c4c 100644 --- a/pandas/tests/plotting/test_frame.py +++ b/pandas/tests/plotting/test_frame.py @@ -3363,6 +3363,62 @@ def test_colors_of_columns_with_same_name(self): for legend, line in zip(result.get_legend().legendHandles, result.lines): assert legend.get_color() == line.get_color() + @pytest.mark.parametrize( + "index_name, old_label, new_label", + [ + (None, "", "new"), + ("old", "old", "new"), + (None, "", ""), + (None, "", 1), + (None, "", [1, 2]), + ], + ) + @pytest.mark.parametrize("kind", ["line", "area", "bar"]) + def test_xlabel_ylabel_dataframe_single_plot( + self, kind, index_name, old_label, new_label + ): + # GH 9093 + df = pd.DataFrame([[1, 2], [2, 5]], columns=["Type A", "Type B"]) + df.index.name = index_name + + # default is the ylabel is not shown and xlabel is index name + ax = df.plot(kind=kind) + assert ax.get_xlabel() == old_label + assert ax.get_ylabel() == "" + + # old xlabel will be overriden and assigned ylabel will be used as ylabel + ax = df.plot(kind=kind, ylabel=new_label, xlabel=new_label) + assert ax.get_ylabel() == str(new_label) + assert ax.get_xlabel() == str(new_label) + + @pytest.mark.parametrize( + "index_name, old_label, new_label", + [ + (None, "", "new"), + ("old", "old", "new"), + (None, "", ""), + (None, "", 1), + (None, "", [1, 2]), + ], + ) + @pytest.mark.parametrize("kind", ["line", "area", "bar"]) + def test_xlabel_ylabel_dataframe_subplots( + self, kind, index_name, old_label, new_label + ): + # GH 9093 + df = pd.DataFrame([[1, 2], [2, 5]], columns=["Type A", "Type B"]) + df.index.name = index_name + + # default is the ylabel is not shown and xlabel is index name + axes = df.plot(kind=kind, subplots=True) + assert all(ax.get_ylabel() == "" for ax in axes) + assert all(ax.get_xlabel() == old_label for ax in axes) + + # old xlabel will be overriden and assigned ylabel will be used as ylabel + axes = df.plot(kind=kind, ylabel=new_label, xlabel=new_label, subplots=True) + assert all(ax.get_ylabel() == str(new_label) for ax in axes) + assert all(ax.get_xlabel() == str(new_label) for ax in axes) + def _generate_4_axes_via_gridspec(): import matplotlib.pyplot as plt diff --git a/pandas/tests/plotting/test_misc.py b/pandas/tests/plotting/test_misc.py index 0b0d23632e827..75eeede472fe9 100644 --- a/pandas/tests/plotting/test_misc.py +++ b/pandas/tests/plotting/test_misc.py @@ -54,7 +54,7 @@ def test_get_accessor_args(): assert x is None assert y is None assert kind == "line" - assert len(kwargs) == 22 + assert len(kwargs) == 24 @td.skip_if_no_mpl diff --git a/pandas/tests/plotting/test_series.py b/pandas/tests/plotting/test_series.py index 6da892c15f489..64da98f57676f 100644 --- a/pandas/tests/plotting/test_series.py +++ b/pandas/tests/plotting/test_series.py @@ -934,3 +934,23 @@ def test_style_single_ok(self): s = pd.Series([1, 2]) ax = s.plot(style="s", color="C3") assert ax.lines[0].get_color() == ["C3"] + + @pytest.mark.parametrize( + "index_name, old_label, new_label", + [(None, "", "new"), ("old", "old", "new"), (None, "", "")], + ) + @pytest.mark.parametrize("kind", ["line", "area", "bar"]) + def test_xlabel_ylabel_series(self, kind, index_name, old_label, new_label): + # GH 9093 + ser = pd.Series([1, 2, 3, 4]) + ser.index.name = index_name + + # default is the ylabel is not shown and xlabel is index name + ax = ser.plot(kind=kind) + assert ax.get_ylabel() == "" + assert ax.get_xlabel() == old_label + + # old xlabel will be overriden and assigned ylabel will be used as ylabel + ax = ser.plot(kind=kind, ylabel=new_label, xlabel=new_label) + assert ax.get_ylabel() == new_label + assert ax.get_xlabel() == new_label