From 9a999fe795bae42cde90f6af865be5088479dbbb Mon Sep 17 00:00:00 2001 From: Leonardo Medeiros Date: Fri, 27 Sep 2019 15:52:04 -0300 Subject: [PATCH 1/4] Fixes issue #8193 --- doc/source/whatsnew/v1.0.0.rst | 1 + pandas/plotting/_matplotlib/core.py | 8 ++++++-- pandas/plotting/_matplotlib/style.py | 6 +++++- pandas/tests/plotting/test_misc.py | 21 +++++++++++++++++++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v1.0.0.rst b/doc/source/whatsnew/v1.0.0.rst index ec6ad38bbc7cf..0ed26abf087e5 100755 --- a/doc/source/whatsnew/v1.0.0.rst +++ b/doc/source/whatsnew/v1.0.0.rst @@ -1110,6 +1110,7 @@ Plotting - Bug in color validation incorrectly raising for non-color styles (:issue:`29122`). - Allow :meth:`DataFrame.plot.scatter` to plot ``objects`` and ``datetime`` type data (:issue:`18755`, :issue:`30391`) - Bug in :meth:`DataFrame.hist`, ``xrot=0`` does not work with ``by`` and subplots (:issue:`30288`). +- :func:`.plot` for line/bar now accepts color by dictonary (:issue:`8193`). Groupby/resample/rolling ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/pandas/plotting/_matplotlib/core.py b/pandas/plotting/_matplotlib/core.py index 2d68bb46a8ada..6ab7342978ab5 100644 --- a/pandas/plotting/_matplotlib/core.py +++ b/pandas/plotting/_matplotlib/core.py @@ -726,7 +726,10 @@ def _apply_style_colors(self, colors, kwds, col_num, label): has_color = "color" in kwds or self.colormap is not None nocolor_style = style is None or re.match("[a-z]+", style) is None if (has_color or self.subplots) and nocolor_style: - kwds["color"] = colors[col_num % len(colors)] + if isinstance(colors, dict): + kwds["color"] = colors[label] + else: + kwds["color"] = colors[col_num % len(colors)] return style, kwds def _get_colors(self, num_colors=None, color_kwds="color"): @@ -1341,12 +1344,13 @@ def _make_plot(self): pos_prior = neg_prior = np.zeros(len(self.data)) K = self.nseries - for i, (label, y) in enumerate(self._iter_data(fillna=0)): ax = self._get_ax(i) kwds = self.kwds.copy() if self._is_series: kwds["color"] = colors + elif isinstance(colors, dict): + kwds["color"] = colors[label] else: kwds["color"] = colors[i % ncolors] diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index fd69265b18a5b..7990bff4f517c 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -27,7 +27,11 @@ def _get_standard_colors( warnings.warn( "'color' and 'colormap' cannot be used simultaneously. Using 'color'" ) - colors = list(color) if is_list_like(color) else color + colors = ( + list(color) + if is_list_like(color) and not isinstance(color, dict) + else color + ) else: if color_type == "default": # need to call list() on the result to copy so we don't diff --git a/pandas/tests/plotting/test_misc.py b/pandas/tests/plotting/test_misc.py index 228c84528e882..168e8c7de0b83 100644 --- a/pandas/tests/plotting/test_misc.py +++ b/pandas/tests/plotting/test_misc.py @@ -406,3 +406,24 @@ def test_get_standard_colors_no_appending(self): color_list = cm.gnuplot(np.linspace(0, 1, 16)) p = df.A.plot.bar(figsize=(16, 7), color=color_list) assert p.patches[1].get_facecolor() == p.patches[17].get_facecolor() + + @pytest.mark.slow + def test_dictionary_color(self): + # issue-8193 + # Test plot color dictionary format + data_files = ["a", "b"] + + expected = [(0.5, 0.24, 0.6), (0.3, 0.7, 0.7)] + + df1 = DataFrame(np.random.rand(2, 2), columns=data_files) + dic_color = {"b": (0.3, 0.7, 0.7), "a": (0.5, 0.24, 0.6)} + + # Bar color test + ax = df1.plot(kind="bar", color=dic_color) + colors = [rect.get_facecolor()[0:-1] for rect in ax.get_children()[0:3:2]] + assert all(color == expected[index] for index, color in enumerate(colors)) + + # Line color test + ax = df1.plot(kind="line", color=dic_color) + colors = [rect.get_color() for rect in ax.get_lines()[0:2]] + assert all(color == expected[index] for index, color in enumerate(colors)) From 81dea8ae234e3a82fd942646db552bf51dcd79f1 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Thu, 16 Jan 2020 11:21:08 +0000 Subject: [PATCH 2/4] Rebase, add docstrings --- pandas/plotting/_core.py | 73 +++++++++++++++++++++++++++++ pandas/plotting/_matplotlib/core.py | 1 + 2 files changed, 74 insertions(+) diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py index c239f11d5c6a1..0aa326cf2424d 100644 --- a/pandas/plotting/_core.py +++ b/pandas/plotting/_core.py @@ -865,6 +865,21 @@ def line(self, x=None, y=None, **kwargs): The values to be plotted. Either the location or the label of the columns to be used. By default, it will use the remaining DataFrame numeric columns. + color : str, int, array_like, or dict, optional + The color of each line for each row. Possible values are: + + - A single color string referred to by name, RGB or RGBA code, + for instance 'red' or '#a98d19'. + + - A sequence of color strings referred to by name, RGB or RGBA + code, which will be used for each line for each row recursively. For + instance ['green','yellow'] all lines for each row will be filled in green + or yellow, alternatively. + + - A dict of the form {column name : color}, so that each row's lines will be + colored accordingly. For example, if your columns are called `a` and `b`, + then passing {'a': 'green', 'b': 'red'} will color the lines for column + `a` in green and lines for column `b` in red. **kwargs Keyword arguments to pass on to :meth:`DataFrame.plot`. @@ -907,6 +922,16 @@ def line(self, x=None, y=None, **kwargs): >>> type(axes) + .. plot:: + :context: close-figs + + Let's repeat the same example, but specifying colors for + each column (in this case, for each animal). + + >>> axes = df.plot.line( + ... subplots=True, color={"pig": "pink", "horse": "#742802"} + ... ) + .. plot:: :context: close-figs @@ -935,6 +960,21 @@ def bar(self, x=None, y=None, **kwargs): y : label or position, optional Allows plotting of one column versus another. If not specified, all numerical columns are used. + color : str, int, array_like, or dict, optional + The color of each bar for each row. Possible values are: + + - A single color string referred to by name, RGB or RGBA code, + for instance 'red' or '#a98d19'. + + - A sequence of color strings referred to by name, RGB or RGBA + code, which will be used for each bar for each row recursively. For + instance ['green','yellow'] all bars for each row will be filled in green + or yellow, alternatively. + + - A dict of the form {column name : color}, so that each row's bars will be + colored accordingly. For example, if your columns are called `a` and `b`, + then passing {'a': 'green', 'b': 'red'} will color bars for column `a` in + green and bars for column `b` in red. **kwargs Additional keyword arguments are documented in :meth:`DataFrame.plot`. @@ -986,6 +1026,17 @@ def bar(self, x=None, y=None, **kwargs): >>> axes = df.plot.bar(rot=0, subplots=True) >>> axes[1].legend(loc=2) # doctest: +SKIP + If we don't like the default colours, we can specify how we'd + like each column to be colored. + + .. plot:: + :context: close-figs + + >>> axes = df.plot.bar( + ... rot=0, subplots=True, color={"speed": "red", "lifespan": "green"} + ... ) + >>> axes[1].legend(loc=2) # doctest: +SKIP + Plot a single column. .. plot:: @@ -1018,6 +1069,21 @@ def barh(self, x=None, y=None, **kwargs): Column to be used for categories. y : label or position, default All numeric columns in dataframe Columns to be plotted from the DataFrame. + color : str, int, array_like, or dict, optional + The color of each bar for each row. Possible values are: + + - A single color string referred to by name, RGB or RGBA code, + for instance 'red' or '#a98d19'. + + - A sequence of color strings referred to by name, RGB or RGBA + code, which will be used for each bar for each row recursively. For + instance ['green','yellow'] all bars for each row will be filled in green + or yellow, alternatively. + + - A dict of the form {column name : color}, so that each row's bars will be + colored accordingly. For example, if your columns are called `a` and `b`, + then passing {'a': 'green', 'b': 'red'} will color bars for column `a` in + green and bars for column `b` in red. **kwargs Keyword arguments to pass on to :meth:`DataFrame.plot`. @@ -1054,6 +1120,13 @@ def barh(self, x=None, y=None, **kwargs): ... 'lifespan': lifespan}, index=index) >>> ax = df.plot.barh() + We can specify colors for each column + + .. plot:: + :context: close-figs + + >>> ax = df.plot.barh(color={"speed": "red", "lifespan": "green"}) + Plot a column of the DataFrame to a horizontal bar plot .. plot:: diff --git a/pandas/plotting/_matplotlib/core.py b/pandas/plotting/_matplotlib/core.py index 6ab7342978ab5..de09460bb833d 100644 --- a/pandas/plotting/_matplotlib/core.py +++ b/pandas/plotting/_matplotlib/core.py @@ -1344,6 +1344,7 @@ def _make_plot(self): pos_prior = neg_prior = np.zeros(len(self.data)) K = self.nseries + for i, (label, y) in enumerate(self._iter_data(fillna=0)): ax = self._get_ax(i) kwds = self.kwds.copy() From 08a63fb41a6729cebbb1d1f74ed4948f5dabbd53 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Fri, 17 Jan 2020 11:37:41 +0000 Subject: [PATCH 3/4] :pencil: move whatsnew entry, reword docstrings --- doc/source/whatsnew/v1.0.0.rst | 1 - doc/source/whatsnew/v1.1.0.rst | 2 +- pandas/plotting/_core.py | 36 +++++++++++++++++----------------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/doc/source/whatsnew/v1.0.0.rst b/doc/source/whatsnew/v1.0.0.rst index 0ed26abf087e5..ec6ad38bbc7cf 100755 --- a/doc/source/whatsnew/v1.0.0.rst +++ b/doc/source/whatsnew/v1.0.0.rst @@ -1110,7 +1110,6 @@ Plotting - Bug in color validation incorrectly raising for non-color styles (:issue:`29122`). - Allow :meth:`DataFrame.plot.scatter` to plot ``objects`` and ``datetime`` type data (:issue:`18755`, :issue:`30391`) - Bug in :meth:`DataFrame.hist`, ``xrot=0`` does not work with ``by`` and subplots (:issue:`30288`). -- :func:`.plot` for line/bar now accepts color by dictonary (:issue:`8193`). Groupby/resample/rolling ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index c8e811ce82b1f..61e907acfd54e 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -162,7 +162,7 @@ I/O Plotting ^^^^^^^^ -- +- :func:`.plot` for line/bar now accepts color by dictonary (:issue:`8193`). - Groupby/resample/rolling diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py index 0aa326cf2424d..db031f500c8bc 100644 --- a/pandas/plotting/_core.py +++ b/pandas/plotting/_core.py @@ -866,20 +866,20 @@ def line(self, x=None, y=None, **kwargs): Either the location or the label of the columns to be used. By default, it will use the remaining DataFrame numeric columns. color : str, int, array_like, or dict, optional - The color of each line for each row. Possible values are: + The color for each of the DataFrame's columns. Possible values are: - A single color string referred to by name, RGB or RGBA code, for instance 'red' or '#a98d19'. - A sequence of color strings referred to by name, RGB or RGBA - code, which will be used for each line for each row recursively. For - instance ['green','yellow'] all lines for each row will be filled in green - or yellow, alternatively. + code, which will be used for each column recursively. For + instance ['green','yellow'] each column's line will be coloured in + green or yellow, alternatively. - - A dict of the form {column name : color}, so that each row's lines will be + - A dict of the form {column name : color}, so that each column will be colored accordingly. For example, if your columns are called `a` and `b`, - then passing {'a': 'green', 'b': 'red'} will color the lines for column - `a` in green and lines for column `b` in red. + then passing {'a': 'green', 'b': 'red'} will color lines for column `a` in + green and lines for column `b` in red. **kwargs Keyword arguments to pass on to :meth:`DataFrame.plot`. @@ -961,17 +961,17 @@ def bar(self, x=None, y=None, **kwargs): Allows plotting of one column versus another. If not specified, all numerical columns are used. color : str, int, array_like, or dict, optional - The color of each bar for each row. Possible values are: + The color for each of the DataFrame's columns. Possible values are: - A single color string referred to by name, RGB or RGBA code, for instance 'red' or '#a98d19'. - A sequence of color strings referred to by name, RGB or RGBA - code, which will be used for each bar for each row recursively. For - instance ['green','yellow'] all bars for each row will be filled in green - or yellow, alternatively. + code, which will be used for each column recursively. For + instance ['green','yellow'] each column's bar will be filled in + green or yellow, alternatively. - - A dict of the form {column name : color}, so that each row's bars will be + - A dict of the form {column name : color}, so that each column will be colored accordingly. For example, if your columns are called `a` and `b`, then passing {'a': 'green', 'b': 'red'} will color bars for column `a` in green and bars for column `b` in red. @@ -1026,7 +1026,7 @@ def bar(self, x=None, y=None, **kwargs): >>> axes = df.plot.bar(rot=0, subplots=True) >>> axes[1].legend(loc=2) # doctest: +SKIP - If we don't like the default colours, we can specify how we'd + If you don't like the default colours, you can specify how you'd like each column to be colored. .. plot:: @@ -1070,17 +1070,17 @@ def barh(self, x=None, y=None, **kwargs): y : label or position, default All numeric columns in dataframe Columns to be plotted from the DataFrame. color : str, int, array_like, or dict, optional - The color of each bar for each row. Possible values are: + The color for each of the DataFrame's columns. Possible values are: - A single color string referred to by name, RGB or RGBA code, for instance 'red' or '#a98d19'. - A sequence of color strings referred to by name, RGB or RGBA - code, which will be used for each bar for each row recursively. For - instance ['green','yellow'] all bars for each row will be filled in green - or yellow, alternatively. + code, which will be used for each column recursively. For + instance ['green','yellow'] each column's bar will be filled in + green or yellow, alternatively. - - A dict of the form {column name : color}, so that each row's bars will be + - A dict of the form {column name : color}, so that each column will be colored accordingly. For example, if your columns are called `a` and `b`, then passing {'a': 'green', 'b': 'red'} will color bars for column `a` in green and bars for column `b` in red. From a71f49aa26ff41f5dd03e7b92e3e00f659caf512 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Thu, 23 Jan 2020 16:00:01 +0000 Subject: [PATCH 4/4] :twisted_rightwards_arrows: fix conflict --- pandas/plotting/_core.py | 190 ++++++++++++++++----------------------- 1 file changed, 75 insertions(+), 115 deletions(-) diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py index db031f500c8bc..139e0f2bbad8b 100644 --- a/pandas/plotting/_core.py +++ b/pandas/plotting/_core.py @@ -385,6 +385,45 @@ def hist_frame( """ +_bar_or_line_doc = """ + Parameters + ---------- + x : label or position, optional + Allows plotting of one column versus another. If not specified, + the index of the DataFrame is used. + y : label or position, optional + Allows plotting of one column versus another. If not specified, + all numerical columns are used. + color : str, array_like, or dict, optional + The color for each of the DataFrame's columns. Possible values are: + + - A single color string referred to by name, RGB or RGBA code, + for instance 'red' or '#a98d19'. + + - A sequence of color strings referred to by name, RGB or RGBA + code, which will be used for each column recursively. For + instance ['green','yellow'] each column's %(kind)s will be filled in + green or yellow, alternatively. + + - A dict of the form {column name : color}, so that each column will be + colored accordingly. For example, if your columns are called `a` and + `b`, then passing {'a': 'green', 'b': 'red'} will color %(kind)ss for + column `a` in green and %(kind)ss for column `b` in red. + + .. versionadded:: 1.1.0 + + **kwargs + Additional keyword arguments are documented in + :meth:`DataFrame.plot`. + + Returns + ------- + matplotlib.axes.Axes or np.ndarray of them + An ndarray is returned with one :class:`matplotlib.axes.Axes` + per column when ``subplots=True``. +""" + + @Substitution(backend="") @Appender(_boxplot_doc) def boxplot( @@ -848,46 +887,8 @@ def __call__(self, *args, **kwargs): __call__.__doc__ = __doc__ - def line(self, x=None, y=None, **kwargs): + @Appender( """ - Plot Series or DataFrame as lines. - - This function is useful to plot lines using DataFrame's values - as coordinates. - - Parameters - ---------- - x : int or str, optional - Columns to use for the horizontal axis. - Either the location or the label of the columns to be used. - By default, it will use the DataFrame indices. - y : int, str, or list of them, optional - The values to be plotted. - Either the location or the label of the columns to be used. - By default, it will use the remaining DataFrame numeric columns. - color : str, int, array_like, or dict, optional - The color for each of the DataFrame's columns. Possible values are: - - - A single color string referred to by name, RGB or RGBA code, - for instance 'red' or '#a98d19'. - - - A sequence of color strings referred to by name, RGB or RGBA - code, which will be used for each column recursively. For - instance ['green','yellow'] each column's line will be coloured in - green or yellow, alternatively. - - - A dict of the form {column name : color}, so that each column will be - colored accordingly. For example, if your columns are called `a` and `b`, - then passing {'a': 'green', 'b': 'red'} will color lines for column `a` in - green and lines for column `b` in red. - **kwargs - Keyword arguments to pass on to :meth:`DataFrame.plot`. - - Returns - ------- - :class:`matplotlib.axes.Axes` or :class:`numpy.ndarray` - Return an ndarray when ``subplots=True``. - See Also -------- matplotlib.pyplot.plot : Plot y versus x as lines and/or markers. @@ -940,51 +941,20 @@ def line(self, x=None, y=None, **kwargs): >>> lines = df.plot.line(x='pig', y='horse') """ - return self(kind="line", x=x, y=y, **kwargs) - - def bar(self, x=None, y=None, **kwargs): + ) + @Substitution(kind="line") + @Appender(_bar_or_line_doc) + def line(self, x=None, y=None, **kwargs): """ - Vertical bar plot. - - A bar plot is a plot that presents categorical data with - rectangular bars with lengths proportional to the values that they - represent. A bar plot shows comparisons among discrete categories. One - axis of the plot shows the specific categories being compared, and the - other axis represents a measured value. - - Parameters - ---------- - x : label or position, optional - Allows plotting of one column versus another. If not specified, - the index of the DataFrame is used. - y : label or position, optional - Allows plotting of one column versus another. If not specified, - all numerical columns are used. - color : str, int, array_like, or dict, optional - The color for each of the DataFrame's columns. Possible values are: - - - A single color string referred to by name, RGB or RGBA code, - for instance 'red' or '#a98d19'. - - - A sequence of color strings referred to by name, RGB or RGBA - code, which will be used for each column recursively. For - instance ['green','yellow'] each column's bar will be filled in - green or yellow, alternatively. - - - A dict of the form {column name : color}, so that each column will be - colored accordingly. For example, if your columns are called `a` and `b`, - then passing {'a': 'green', 'b': 'red'} will color bars for column `a` in - green and bars for column `b` in red. - **kwargs - Additional keyword arguments are documented in - :meth:`DataFrame.plot`. + Plot Series or DataFrame as lines. - Returns - ------- - matplotlib.axes.Axes or np.ndarray of them - An ndarray is returned with one :class:`matplotlib.axes.Axes` - per column when ``subplots=True``. + This function is useful to plot lines using DataFrame's values + as coordinates. + """ + return self(kind="line", x=x, y=y, **kwargs) + @Appender( + """ See Also -------- DataFrame.plot.barh : Horizontal bar plot. @@ -1050,47 +1020,24 @@ def bar(self, x=None, y=None, **kwargs): :context: close-figs >>> ax = df.plot.bar(x='lifespan', rot=0) + """ + ) + @Substitution(kind="bar") + @Appender(_bar_or_line_doc) + def bar(self, x=None, y=None, **kwargs): """ - return self(kind="bar", x=x, y=y, **kwargs) - - def barh(self, x=None, y=None, **kwargs): - """ - Make a horizontal bar plot. + Vertical bar plot. - A horizontal bar plot is a plot that presents quantitative data with + A bar plot is a plot that presents categorical data with rectangular bars with lengths proportional to the values that they represent. A bar plot shows comparisons among discrete categories. One axis of the plot shows the specific categories being compared, and the other axis represents a measured value. + """ + return self(kind="bar", x=x, y=y, **kwargs) - Parameters - ---------- - x : label or position, default DataFrame.index - Column to be used for categories. - y : label or position, default All numeric columns in dataframe - Columns to be plotted from the DataFrame. - color : str, int, array_like, or dict, optional - The color for each of the DataFrame's columns. Possible values are: - - - A single color string referred to by name, RGB or RGBA code, - for instance 'red' or '#a98d19'. - - - A sequence of color strings referred to by name, RGB or RGBA - code, which will be used for each column recursively. For - instance ['green','yellow'] each column's bar will be filled in - green or yellow, alternatively. - - - A dict of the form {column name : color}, so that each column will be - colored accordingly. For example, if your columns are called `a` and `b`, - then passing {'a': 'green', 'b': 'red'} will color bars for column `a` in - green and bars for column `b` in red. - **kwargs - Keyword arguments to pass on to :meth:`DataFrame.plot`. - - Returns - ------- - :class:`matplotlib.axes.Axes` or numpy.ndarray of them - + @Appender( + """ See Also -------- DataFrame.plot.bar: Vertical bar plot. @@ -1152,6 +1099,19 @@ def barh(self, x=None, y=None, **kwargs): >>> df = pd.DataFrame({'speed': speed, ... 'lifespan': lifespan}, index=index) >>> ax = df.plot.barh(x='lifespan') + """ + ) + @Substitution(kind="bar") + @Appender(_bar_or_line_doc) + def barh(self, x=None, y=None, **kwargs): + """ + Make a horizontal bar plot. + + A horizontal bar plot is a plot that presents quantitative data with + rectangular bars with lengths proportional to the values that they + represent. A bar plot shows comparisons among discrete categories. One + axis of the plot shows the specific categories being compared, and the + other axis represents a measured value. """ return self(kind="barh", x=x, y=y, **kwargs)