Skip to content

ENH: Implement xlabel and ylabel options in Series.plot and DataFrame.plot #34223

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 32 commits into from
Jun 26, 2020
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7e461a1
remove \n from docstring
charlesdong1991 Dec 3, 2018
1314059
fix conflicts
charlesdong1991 Jan 19, 2019
8bcb313
Merge remote-tracking branch 'upstream/master'
charlesdong1991 Jul 30, 2019
24c3ede
Merge remote-tracking branch 'upstream/master'
charlesdong1991 Jan 14, 2020
dea38f2
fix issue 17038
charlesdong1991 Jan 14, 2020
cd9e7ac
revert change
charlesdong1991 Jan 14, 2020
e5e912b
revert change
charlesdong1991 Jan 14, 2020
045a76f
Merge remote-tracking branch 'upstream/master'
charlesdong1991 Apr 6, 2020
e69c080
Merge remote-tracking branch 'upstream/master' into fix_issue_9093
charlesdong1991 May 17, 2020
bc7bad4
allow xlabel ylabel
charlesdong1991 May 17, 2020
4ae4f17
fixup
charlesdong1991 May 17, 2020
0d32836
add more tests and fix docs
charlesdong1991 May 17, 2020
4d7216b
annotation
charlesdong1991 May 17, 2020
cc1b6d6
add whatsnew
charlesdong1991 May 17, 2020
dbf8f1a
linting
charlesdong1991 May 17, 2020
3fb1faa
fix annotation
charlesdong1991 May 17, 2020
f8ad37a
fix conflict
charlesdong1991 May 18, 2020
3f34099
allow number as labels
charlesdong1991 May 25, 2020
4246f6e
Merge remote-tracking branch 'upstream/master' into fix_issue_9093
charlesdong1991 Jun 14, 2020
408d08f
simplify test
charlesdong1991 Jun 14, 2020
54824ab
fix linting
charlesdong1991 Jun 14, 2020
8a24a40
fix linting
charlesdong1991 Jun 14, 2020
6bb571a
Merge remote-tracking branch 'upstream/master' into fix_issue_9093
charlesdong1991 Jun 14, 2020
66cf42f
Merge remote-tracking branch 'upstream/master' into fix_issue_9093
charlesdong1991 Jun 17, 2020
7e2a7c9
Merge remote-tracking branch 'upstream/master' into fix_issue_9093
charlesdong1991 Jun 20, 2020
31c8ccf
add user guide
charlesdong1991 Jun 20, 2020
9d1d942
fix test
charlesdong1991 Jun 20, 2020
5335de8
use current df
charlesdong1991 Jun 20, 2020
2a8406d
Merge remote-tracking branch 'upstream/master' into fix_issue_9093
charlesdong1991 Jun 20, 2020
8f12150
fix typo
charlesdong1991 Jun 20, 2020
e1ade53
code change on reviews
charlesdong1991 Jun 25, 2020
26bfa6e
Merge remote-tracking branch 'upstream/master' into fix_issue_9093
charlesdong1991 Jun 25, 2020
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
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,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`)

.. ---------------------------------------------------------------------------
Expand Down
14 changes: 14 additions & 0 deletions pandas/plotting/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,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 : str, default None
Name to use for the xlabel on x-axis. Default uses index name as xlabel.

.. versionadded:: 1.1.0

ylabel : str, default None
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).
Expand Down Expand Up @@ -759,6 +769,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 = [
Expand Down Expand Up @@ -791,6 +803,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(
Expand Down
19 changes: 16 additions & 3 deletions pandas/plotting/_matplotlib/core.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from typing import Optional
from typing import List, Optional
import warnings

import numpy as np
Expand Down Expand Up @@ -95,6 +95,8 @@ def __init__(
ylim=None,
xticks=None,
yticks=None,
xlabel: Optional[str] = None,
ylabel: Optional[str] = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this have to be string? Should work with e.g. int / float

Anyway, I tried running this and it looks good! Nice tests

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I think it should be string? this is not calling any column of dataframe, but explicitly defined by users who want to give labels for x/y axis. I am not very sure about if users would name it using int/float, quite rare case though

I will later check to see if matplotlib allows to do so or not, if so, maybe we could support too! thanks!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried running it passing an int, and it worked fine - but yeah, typical case would be to label with a str, so it's fine as is, sorry for the noise :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will change to Label then and add a test case, so that we also support numbers!

Thanks for the feedback!

sort_columns=False,
fontsize=None,
secondary_y=False,
Expand Down Expand Up @@ -136,6 +138,8 @@ def __init__(
self.ylim = ylim
self.title = title
self.use_index = use_index
self.xlabel = xlabel
self.ylabel = ylabel

self.fontsize = fontsize

Expand All @@ -153,8 +157,8 @@ def __init__(

self.grid = grid
self.legend = legend
self.legend_handles = []
self.legend_labels = []
self.legend_handles: List = []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What error does this solve?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this solves mypy complaint, exactly the same as in #28373

self.legend_labels: List = []

for attr in self._pop_attributes:
value = kwds.pop(attr, self._attr_defaults.get(attr, None))
Expand Down Expand Up @@ -480,6 +484,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(self.ylabel)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this consistent with xlabel, which seems to pprint the value?

May be I'm wrong, but feels like df.plot(xlabel=a_complex_structure) will make a_complex_structure (may be a dict of lists) look "nice". While df.plot(ylabel=a_complex_structure) may fail, or just convert to string with str instead of pprint_thing.

Would be probably good to add a test in the parametrization to make sure this works as expected.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call! i think matplotlib's ax.set_ylabel already could make the complex structure case look nice by converting to string automatically without using pprint_thing, e.g. inputs like [1, 2, 3] will be shown as '[1, 2, 3]' same in matplotlib plot.

But i think it might be indeed nicer to be consistent with xlabel so I added pprint_thing for ylabel as well, and add the test case in parametrization! thanks!


ax.grid(self.grid)

if self.title:
Expand Down Expand Up @@ -666,6 +675,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
Expand Down
56 changes: 56 additions & 0 deletions pandas/tests/plotting/test_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -3350,6 +3350,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_xlabel, new_xlabel, old_ylabel, new_ylabel",
[
(None, "", "new_x", "", ""),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a huge issue but instead of parametrizing xlabel / ylabel as individual arguments can you not just pass one set of labels and maybe have another parametrization on transpose to test the other axis? Would probably help readability

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are right, actually we can also remove the need of transpose, and simply use old_label and new_label instead. As commented in the PR, for default, ylabel should always be empty string, while xlabel be the index name. And if assigned, they will be overriden by assigned value.

Pls let me know your thoughts on the simplified tests! thanks! @WillAyd

("old_x", "old_x", "new_x", "", ""),
(None, "", "", "", ""),
(None, "", "new_x", "", "new_y"),
("old_x", "old_x", "new_x", "", "new_y"),
],
)
@pytest.mark.parametrize("kind", ["line", "area", "bar"])
def test_xlabel_ylabel_dataframe_single_plot(
self, kind, index_name, old_xlabel, old_ylabel, new_xlabel, new_ylabel
):
# 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_ylabel() == old_ylabel
assert ax.get_xlabel() == old_xlabel

# old xlabel will be overriden and assigned ylabel will be used as ylabel
ax = df.plot(kind=kind, ylabel=new_ylabel, xlabel=new_xlabel)
assert ax.get_ylabel() == new_ylabel
assert ax.get_xlabel() == new_xlabel

@pytest.mark.parametrize(
"index_name, old_xlabel, new_xlabel, old_ylabel, new_ylabel",
[
(None, "", "new_x", "", ""),
("old_x", "old_x", "new_x", "", ""),
(None, "", "", "", ""),
(None, "", "new_x", "", "new_y"),
("old_x", "old_x", "new_x", "", "new_y"),
],
)
@pytest.mark.parametrize("kind", ["line", "area", "bar"])
def test_xlabel_ylabel_dataframe_subplots(
self, kind, index_name, old_xlabel, old_ylabel, new_xlabel, new_ylabel
):
# 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() == old_ylabel for ax in axes)
assert all(ax.get_xlabel() == old_xlabel for ax in axes)

# old xlabel will be overriden and assigned ylabel will be used as ylabel
axes = df.plot(kind=kind, ylabel=new_ylabel, xlabel=new_xlabel, subplots=True)
assert all(ax.get_ylabel() == new_ylabel for ax in axes)
assert all(ax.get_xlabel() == new_xlabel for ax in axes)


def _generate_4_axes_via_gridspec():
import matplotlib.pyplot as plt
Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/plotting/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions pandas/tests/plotting/test_series.py
Original file line number Diff line number Diff line change
Expand Up @@ -934,3 +934,31 @@ 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_xlabel, new_xlabel, old_ylabel, new_ylabel",
[
(None, "", "new_x", "", ""),
("old_x", "old_x", "new_x", "", ""),
(None, "", "", "", ""),
(None, "", "new_x", "", "new_y"),
("old_x", "old_x", "new_x", "", "new_y"),
],
)
@pytest.mark.parametrize("kind", ["line", "area", "bar"])
def test_xlabel_ylabel_series(
self, kind, index_name, old_xlabel, old_ylabel, new_xlabel, new_ylabel
):
# 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() == old_ylabel
assert ax.get_xlabel() == old_xlabel

# old xlabel will be overriden and assigned ylabel will be used as ylabel
ax = ser.plot(kind=kind, ylabel=new_ylabel, xlabel=new_xlabel)
assert ax.get_ylabel() == new_ylabel
assert ax.get_xlabel() == new_xlabel