From 4003d1c95f24ad6fa1a632300b9b4d8b34c34291 Mon Sep 17 00:00:00 2001 From: sfoo Date: Mon, 3 Apr 2017 01:47:54 -0700 Subject: [PATCH 1/4] VIS: Allow 'C0'-like plotting for plotting colors --- doc/source/whatsnew/v0.20.0.txt | 1 + pandas/tests/plotting/test_frame.py | 10 ++++++++++ pandas/tools/plotting.py | 11 +++++++---- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/doc/source/whatsnew/v0.20.0.txt b/doc/source/whatsnew/v0.20.0.txt index fa24c973a7549..78ea84bd43deb 100644 --- a/doc/source/whatsnew/v0.20.0.txt +++ b/doc/source/whatsnew/v0.20.0.txt @@ -177,6 +177,7 @@ Other enhancements - The ``usecols`` argument in ``pd.read_csv`` now accepts a callable function as a value (:issue:`14154`) - The ``skiprows`` argument in ``pd.read_csv`` now accepts a callable function as a value (:issue:`10882`) - ``pd.DataFrame.plot`` now prints a title above each subplot if ``suplots=True`` and ``title`` is a list of strings (:issue:`14753`) +- ``pd.DataFrame.plot`` can pass matplotlib 2.0 default color cycle as a single string as color parameter (:issue:`15516`) - ``pd.Series.interpolate`` now supports timedelta as an index type with ``method='time'`` (:issue:`6424`) - ``Timedelta.isoformat`` method added for formatting Timedeltas as an `ISO 8601 duration`_. See the :ref:`Timedelta docs ` (:issue:`15136`) - ``pandas.io.json.json_normalize()`` gained the option ``errors='ignore'|'raise'``; the default is ``errors='raise'`` which is backward compatible. (:issue:`14583`) diff --git a/pandas/tests/plotting/test_frame.py b/pandas/tests/plotting/test_frame.py index 48af366f24ea4..96f642e6f2c70 100644 --- a/pandas/tests/plotting/test_frame.py +++ b/pandas/tests/plotting/test_frame.py @@ -141,6 +141,12 @@ def test_plot(self): result = ax.get_axes() # deprecated self.assertIs(result, axes[0]) + def test_mpl2_color_cycle_str(self): + colors = ['C' + str(x) for x in range(10)] + df = DataFrame(randn(10, 3), columns=['a', 'b', 'c']) + for c in colors: + _check_plot_works(df.plot, color=c) + def test_color_and_style_arguments(self): df = DataFrame({'x': [1, 2], 'y': [3, 4]}) # passing both 'color' and 'style' arguments should be allowed @@ -1600,6 +1606,10 @@ def test_line_colors(self): self._check_colors(ax.get_lines(), linecolors=['red'] * 5) tm.close() + ax = df.plot(color='C0') + self._check_colors(ax.get_lines(), linecolors=['C0']) + tm.close() + # GH 10299 custom_colors = ['#FF0000', '#0000FF', '#FFFF00', '#000000', '#FFFFFF'] ax = df.plot(color=custom_colors) diff --git a/pandas/tools/plotting.py b/pandas/tools/plotting.py index d46c38c117445..fe36a65bdfa9a 100644 --- a/pandas/tools/plotting.py +++ b/pandas/tools/plotting.py @@ -217,10 +217,13 @@ def _maybe_valid_colors(colors): # check whether each character can be convertable to colors maybe_color_cycle = _maybe_valid_colors(list(colors)) if maybe_single_color and maybe_color_cycle and len(colors) > 1: - msg = ("'{0}' can be parsed as both single color and " - "color cycle. Specify each color using a list " - "like ['{0}'] or {1}") - raise ValueError(msg.format(colors, list(colors))) + if color[0] == 'C' and len(color) == 2: + colors = [colors] + else: + msg = ("'{0}' can be parsed as both single color and " + "color cycle. Specify each color using a list " + "like ['{0}'] or {1}") + raise ValueError(msg.format(colors, list(colors))) elif maybe_single_color: colors = [colors] else: From e2497222ecf64e07f4bd7873a1b860021b0b605c Mon Sep 17 00:00:00 2001 From: sfoo Date: Mon, 3 Apr 2017 23:54:22 -0700 Subject: [PATCH 2/4] Added case color='' and support for mpl < 2.0 --- doc/source/whatsnew/v0.20.0.txt | 2 +- pandas/tests/plotting/test_frame.py | 6 +++++- pandas/tools/plotting.py | 14 +++++++++++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/doc/source/whatsnew/v0.20.0.txt b/doc/source/whatsnew/v0.20.0.txt index 78ea84bd43deb..f390e7eeb98bc 100644 --- a/doc/source/whatsnew/v0.20.0.txt +++ b/doc/source/whatsnew/v0.20.0.txt @@ -177,7 +177,7 @@ Other enhancements - The ``usecols`` argument in ``pd.read_csv`` now accepts a callable function as a value (:issue:`14154`) - The ``skiprows`` argument in ``pd.read_csv`` now accepts a callable function as a value (:issue:`10882`) - ``pd.DataFrame.plot`` now prints a title above each subplot if ``suplots=True`` and ``title`` is a list of strings (:issue:`14753`) -- ``pd.DataFrame.plot`` can pass matplotlib 2.0 default color cycle as a single string as color parameter (:issue:`15516`) +- ``pd.DataFrame.plot`` can pass `matplotlib 2.0 default color cycle as a single string as color parameter `__. (:issue:`15516`) - ``pd.Series.interpolate`` now supports timedelta as an index type with ``method='time'`` (:issue:`6424`) - ``Timedelta.isoformat`` method added for formatting Timedeltas as an `ISO 8601 duration`_. See the :ref:`Timedelta docs ` (:issue:`15136`) - ``pandas.io.json.json_normalize()`` gained the option ``errors='ignore'|'raise'``; the default is ``errors='raise'`` which is backward compatible. (:issue:`14583`) diff --git a/pandas/tests/plotting/test_frame.py b/pandas/tests/plotting/test_frame.py index 96f642e6f2c70..5659532e2811f 100644 --- a/pandas/tests/plotting/test_frame.py +++ b/pandas/tests/plotting/test_frame.py @@ -141,11 +141,15 @@ def test_plot(self): result = ax.get_axes() # deprecated self.assertIs(result, axes[0]) + # GH 15516 def test_mpl2_color_cycle_str(self): + # test C[0-9] as string colors = ['C' + str(x) for x in range(10)] df = DataFrame(randn(10, 3), columns=['a', 'b', 'c']) for c in colors: _check_plot_works(df.plot, color=c) + with tm.assertRaises(ValueError): + df.plot(color='') def test_color_and_style_arguments(self): df = DataFrame({'x': [1, 2], 'y': [3, 4]}) @@ -1607,7 +1611,7 @@ def test_line_colors(self): tm.close() ax = df.plot(color='C0') - self._check_colors(ax.get_lines(), linecolors=['C0']) + self._check_colors(ax.get_lines(), linecolors=['C0'] * 5) tm.close() # GH 10299 diff --git a/pandas/tools/plotting.py b/pandas/tools/plotting.py index fe36a65bdfa9a..e696a2d8fbd43 100644 --- a/pandas/tools/plotting.py +++ b/pandas/tools/plotting.py @@ -217,9 +217,14 @@ def _maybe_valid_colors(colors): # check whether each character can be convertable to colors maybe_color_cycle = _maybe_valid_colors(list(colors)) if maybe_single_color and maybe_color_cycle and len(colors) > 1: - if color[0] == 'C' and len(color) == 2: - colors = [colors] + # Special case for single str 'CN' match and convert to hex + # for supporting matplotlib < 2.0.0 + if re.match(r'C[0-9]', colors) and len(colors) == 2: + prop_cycler = plt.rcParams['axes.prop_cycle'] + hex_color = prop_cycler.by_key().get('color', ['k']) + colors = [hex_color[int(colors[1])]] else: + # non-default cycle may be parsed as single or many colors msg = ("'{0}' can be parsed as both single color and " "color cycle. Specify each color using a list " "like ['{0}'] or {1}") @@ -232,7 +237,10 @@ def _maybe_valid_colors(colors): pass if len(colors) != num_colors: - multiple = num_colors // len(colors) - 1 + try: + multiple = num_colors // len(colors) - 1 + except ZeroDivisionError: + raise ValueError("Invalid color argument: ''") mod = num_colors % len(colors) colors += multiple * colors From 3cb9e0b98d35b8ee0b9d81a9357e206d9f4a78d7 Mon Sep 17 00:00:00 2001 From: sfoo Date: Wed, 5 Apr 2017 18:29:40 -0700 Subject: [PATCH 3/4] Updated prop_cycle references to be compatible with matplotlib 1.5 and 2.0 --- pandas/tests/plotting/test_frame.py | 11 ++++++++--- pandas/tools/plotting.py | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pandas/tests/plotting/test_frame.py b/pandas/tests/plotting/test_frame.py index 5659532e2811f..8963dfa7a9789 100644 --- a/pandas/tests/plotting/test_frame.py +++ b/pandas/tests/plotting/test_frame.py @@ -143,11 +143,16 @@ def test_plot(self): # GH 15516 def test_mpl2_color_cycle_str(self): - # test C[0-9] as string - colors = ['C' + str(x) for x in range(10)] + # test CN mpl 2.0 color cycle + import matplotlib.pyplot as plt + # mpl 1.5 color cycle is 'bgrcmyk' and + # tests up to 'C6' for compatibility + colors = ['C' + str(x) for x in range(7)] + hex_color = [c['color'] + for c in list(plt.rcParams['axes.prop_cycle'])] df = DataFrame(randn(10, 3), columns=['a', 'b', 'c']) for c in colors: - _check_plot_works(df.plot, color=c) + _check_plot_works(df.plot, color=hex_color[int(c[1])]) with tm.assertRaises(ValueError): df.plot(color='') diff --git a/pandas/tools/plotting.py b/pandas/tools/plotting.py index 0178170fb904e..b2fbf7e6f6d19 100644 --- a/pandas/tools/plotting.py +++ b/pandas/tools/plotting.py @@ -228,8 +228,8 @@ def _maybe_valid_colors(colors): # Special case for single str 'CN' match and convert to hex # for supporting matplotlib < 2.0.0 if re.match(r'C[0-9]', colors) and len(colors) == 2: - prop_cycler = plt.rcParams['axes.prop_cycle'] - hex_color = prop_cycler.by_key().get('color', ['k']) + hex_color = [c['color'] + for c in list(plt.rcParams['axes.prop_cycle'])] colors = [hex_color[int(colors[1])]] else: # non-default cycle may be parsed as single or many colors From 9d15e7d36c7db2ea554849f9617a5684b2ccc646 Mon Sep 17 00:00:00 2001 From: sfoo Date: Sun, 9 Apr 2017 03:11:23 -0700 Subject: [PATCH 4/4] Separated test; Used more consise regex --- pandas/tests/plotting/test_frame.py | 23 ++++++++++------------- pandas/tools/plotting.py | 4 ++-- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/pandas/tests/plotting/test_frame.py b/pandas/tests/plotting/test_frame.py index 8963dfa7a9789..474230369d1e1 100644 --- a/pandas/tests/plotting/test_frame.py +++ b/pandas/tests/plotting/test_frame.py @@ -144,15 +144,16 @@ def test_plot(self): # GH 15516 def test_mpl2_color_cycle_str(self): # test CN mpl 2.0 color cycle - import matplotlib.pyplot as plt - # mpl 1.5 color cycle is 'bgrcmyk' and - # tests up to 'C6' for compatibility - colors = ['C' + str(x) for x in range(7)] - hex_color = [c['color'] - for c in list(plt.rcParams['axes.prop_cycle'])] - df = DataFrame(randn(10, 3), columns=['a', 'b', 'c']) - for c in colors: - _check_plot_works(df.plot, color=hex_color[int(c[1])]) + if self.mpl_ge_2_0_0: + colors = ['C' + str(x) for x in range(10)] + df = DataFrame(randn(10, 3), columns=['a', 'b', 'c']) + for c in colors: + _check_plot_works(df.plot, color=c) + else: + pytest.skip("not supported in matplotlib < 2.0.0") + + def test_color_empty_string(self): + df = DataFrame(randn(10, 2)) with tm.assertRaises(ValueError): df.plot(color='') @@ -1615,10 +1616,6 @@ def test_line_colors(self): self._check_colors(ax.get_lines(), linecolors=['red'] * 5) tm.close() - ax = df.plot(color='C0') - self._check_colors(ax.get_lines(), linecolors=['C0'] * 5) - tm.close() - # GH 10299 custom_colors = ['#FF0000', '#0000FF', '#FFFF00', '#000000', '#FFFFFF'] ax = df.plot(color=custom_colors) diff --git a/pandas/tools/plotting.py b/pandas/tools/plotting.py index b2fbf7e6f6d19..99e56ca80cf97 100644 --- a/pandas/tools/plotting.py +++ b/pandas/tools/plotting.py @@ -227,12 +227,12 @@ def _maybe_valid_colors(colors): if maybe_single_color and maybe_color_cycle and len(colors) > 1: # Special case for single str 'CN' match and convert to hex # for supporting matplotlib < 2.0.0 - if re.match(r'C[0-9]', colors) and len(colors) == 2: + if re.match(r'\AC[0-9]\Z', colors) and _mpl_ge_2_0_0(): hex_color = [c['color'] for c in list(plt.rcParams['axes.prop_cycle'])] colors = [hex_color[int(colors[1])]] else: - # non-default cycle may be parsed as single or many colors + # this may no longer be required msg = ("'{0}' can be parsed as both single color and " "color cycle. Specify each color using a list " "like ['{0}'] or {1}")