From 0f1b99ab34fbf6c81faf08e59768797a7dc7637e Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Sat, 17 Oct 2020 23:16:28 +0700 Subject: [PATCH 01/24] REF: extract functions --- pandas/plotting/_matplotlib/style.py | 118 ++++++++++++++++++--------- 1 file changed, 79 insertions(+), 39 deletions(-) diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index b919728971505..49c1436d7981d 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -3,6 +3,7 @@ import matplotlib.cm as cm import matplotlib.colors +import matplotlib.pyplot as plt import numpy as np from pandas.core.dtypes.common import is_list_like @@ -11,55 +12,35 @@ def get_standard_colors( - num_colors: int, colormap=None, color_type: str = "default", color=None + num_colors: int, + colormap=None, + color_type: str = "default", + color=None, ): - import matplotlib.pyplot as plt + colors = _get_colors( + color=color, + colormap=colormap, + color_type=color_type, + num_colors=num_colors, + ) + return _cycle_colors(colors, num_colors=num_colors) + + +def _get_colors(*, color, colormap, color_type, num_colors): if color is None and colormap is not None: - if isinstance(colormap, str): - cmap = colormap - colormap = cm.get_cmap(colormap) - if colormap is None: - raise ValueError(f"Colormap {cmap} is not recognized") - colors = [colormap(num) for num in np.linspace(0, 1, num=num_colors)] + return _get_colors_from_colormap(colormap, num_colors=num_colors) elif color is not None: if colormap is not None: warnings.warn( "'color' and 'colormap' cannot be used simultaneously. Using 'color'" ) - colors = ( - list(color) - if is_list_like(color) and not isinstance(color, dict) - else color - ) + return _get_colors_from_color(color) else: - if color_type == "default": - # need to call list() on the result to copy so we don't - # modify the global rcParams below - try: - colors = [c["color"] for c in list(plt.rcParams["axes.prop_cycle"])] - except KeyError: - colors = list(plt.rcParams.get("axes.color_cycle", list("bgrcmyk"))) - if isinstance(colors, str): - colors = list(colors) - - colors = colors[0:num_colors] - elif color_type == "random": - - def random_color(column): - """ Returns a random color represented as a list of length 3""" - # GH17525 use common._random_state to avoid resetting the seed - rs = com.random_state(column) - return rs.rand(3).tolist() - - colors = [random_color(num) for num in range(num_colors)] - else: - raise ValueError("color_type must be either 'default' or 'random'") - - if isinstance(colors, str) and _is_single_color(colors): - # GH #36972 - colors = [colors] + return _get_colors_from_color_type(color_type, num_colors=num_colors) + +def _cycle_colors(colors, num_colors): # Append more colors by cycling if there is not enough color. # Extra colors will be ignored by matplotlib if there are more colors # than needed and nothing needs to be done here. @@ -76,6 +57,65 @@ def random_color(column): return colors +def _get_colors_from_colormap(colormap, num_colors): + colormap = _get_cmap_instance(colormap) + return [colormap(num) for num in np.linspace(0, 1, num=num_colors)] + + +def _get_cmap_instance(colormap): + if isinstance(colormap, str): + cmap = colormap + colormap = cm.get_cmap(colormap) + if colormap is None: + raise ValueError(f"Colormap {cmap} is not recognized") + return colormap + + +def _get_colors_from_color(color): + if is_list_like(color) and not isinstance(color, dict): + return list(color) + + if _is_single_color(color): + # GH #36972 + return [color] + + return color + + +def _get_colors_from_color_type(color_type, num_colors): + if color_type == "default": + return _get_default_colors(num_colors) + elif color_type == "random": + return _get_random_colors(num_colors) + else: + raise ValueError("color_type must be either 'default' or 'random'") + + +def _get_default_colors(num_colors): + # need to call list() on the result to copy so we don't + # modify the global rcParams below + try: + colors = [c["color"] for c in list(plt.rcParams["axes.prop_cycle"])] + except KeyError: + colors = list(plt.rcParams.get("axes.color_cycle", list("bgrcmyk"))) + + if isinstance(colors, str): + colors = list(colors) + + return colors[0:num_colors] + + +def _get_random_colors(num_colors): + return [_random_color(num) for num in range(num_colors)] + + +def _random_color(column): + """ Returns a random color represented as a list of length 3""" + # GH17525 use common._random_state to avoid resetting the seed + rs = com.random_state(column) + return rs.rand(3).tolist() + + def _is_single_color(color: str) -> bool: """Check if ``color`` is a single color. From 901453a06b8be5445272155bba92e6e4a2fb8acb Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Sat, 17 Oct 2020 23:35:04 +0700 Subject: [PATCH 02/24] CLN: remove try/except/ZeroDivisionError --- pandas/plotting/_matplotlib/style.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index 49c1436d7981d..cadd1377d7978 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -45,12 +45,8 @@ def _cycle_colors(colors, num_colors): # Extra colors will be ignored by matplotlib if there are more colors # than needed and nothing needs to be done here. if len(colors) < num_colors: - try: - multiple = num_colors // len(colors) - 1 - except ZeroDivisionError: - raise ValueError("Invalid color argument: ''") + multiple = num_colors // len(colors) - 1 mod = num_colors % len(colors) - colors += multiple * colors colors += colors[:mod] @@ -72,6 +68,9 @@ def _get_cmap_instance(colormap): def _get_colors_from_color(color): + if len(color) == 0: + raise ValueError("Invalid color argument: {color}") + if is_list_like(color) and not isinstance(color, dict): return list(color) From 201b25f519cb302516f128205ffa7172de42245d Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Sat, 17 Oct 2020 23:38:44 +0700 Subject: [PATCH 03/24] REF: drop unnecesasry if statement --- pandas/plotting/_matplotlib/style.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index cadd1377d7978..eefa16dc15761 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -98,9 +98,6 @@ def _get_default_colors(num_colors): except KeyError: colors = list(plt.rcParams.get("axes.color_cycle", list("bgrcmyk"))) - if isinstance(colors, str): - colors = list(colors) - return colors[0:num_colors] From 8e13df506d6fa8f143efa84473342c46984be138 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Sat, 17 Oct 2020 23:40:43 +0700 Subject: [PATCH 04/24] CLN: simplify logic --- pandas/plotting/_matplotlib/style.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index eefa16dc15761..68e6e6d2c6073 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -71,7 +71,10 @@ def _get_colors_from_color(color): if len(color) == 0: raise ValueError("Invalid color argument: {color}") - if is_list_like(color) and not isinstance(color, dict): + if isinstance(color, dict): + return color + + if is_list_like(color): return list(color) if _is_single_color(color): From 37a820dda478d7e119346c5069c8cd5007d4fa1b Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Sat, 17 Oct 2020 23:43:45 +0700 Subject: [PATCH 05/24] DOC: add short docstrings --- pandas/plotting/_matplotlib/style.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index 68e6e6d2c6073..9f17f0236c414 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -28,6 +28,7 @@ def get_standard_colors( def _get_colors(*, color, colormap, color_type, num_colors): + """Get colors from user input.""" if color is None and colormap is not None: return _get_colors_from_colormap(colormap, num_colors=num_colors) elif color is not None: @@ -41,9 +42,11 @@ def _get_colors(*, color, colormap, color_type, num_colors): def _cycle_colors(colors, num_colors): - # Append more colors by cycling if there is not enough color. - # Extra colors will be ignored by matplotlib if there are more colors - # than needed and nothing needs to be done here. + """Append more colors by cycling if there is not enough color. + + Extra colors will be ignored by matplotlib if there are more colors + than needed and nothing needs to be done here. + """ if len(colors) < num_colors: multiple = num_colors // len(colors) - 1 mod = num_colors % len(colors) @@ -54,11 +57,13 @@ def _cycle_colors(colors, num_colors): def _get_colors_from_colormap(colormap, num_colors): + """Get colors from colormap.""" colormap = _get_cmap_instance(colormap) return [colormap(num) for num in np.linspace(0, 1, num=num_colors)] def _get_cmap_instance(colormap): + """Get instance of matplotlib colormap.""" if isinstance(colormap, str): cmap = colormap colormap = cm.get_cmap(colormap) @@ -68,6 +73,7 @@ def _get_cmap_instance(colormap): def _get_colors_from_color(color): + """Get colors from user input color.""" if len(color) == 0: raise ValueError("Invalid color argument: {color}") @@ -85,6 +91,7 @@ def _get_colors_from_color(color): def _get_colors_from_color_type(color_type, num_colors): + """Get colors from user input color type.""" if color_type == "default": return _get_default_colors(num_colors) elif color_type == "random": @@ -94,6 +101,7 @@ def _get_colors_from_color_type(color_type, num_colors): def _get_default_colors(num_colors): + """Get ``num_colors`` of default colors from matplotlib rc params.""" # need to call list() on the result to copy so we don't # modify the global rcParams below try: @@ -105,11 +113,12 @@ def _get_default_colors(num_colors): def _get_random_colors(num_colors): + """Get ``num_colors`` of random colors.""" return [_random_color(num) for num in range(num_colors)] def _random_color(column): - """ Returns a random color represented as a list of length 3""" + """Get a random color represented as a list of length 3""" # GH17525 use common._random_state to avoid resetting the seed rs = com.random_state(column) return rs.rand(3).tolist() From 3883a1381572daa4afa533f62fbf7780889d4636 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Sun, 18 Oct 2020 00:06:21 +0700 Subject: [PATCH 06/24] CLN: simplify logic further --- pandas/plotting/_matplotlib/style.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index 9f17f0236c414..a28e1e66ad775 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -6,7 +6,6 @@ import matplotlib.pyplot as plt import numpy as np -from pandas.core.dtypes.common import is_list_like import pandas.core.common as com @@ -80,14 +79,11 @@ def _get_colors_from_color(color): if isinstance(color, dict): return color - if is_list_like(color): - return list(color) - if _is_single_color(color): # GH #36972 return [color] - return color + return list(color) def _get_colors_from_color_type(color_type, num_colors): From f93743c1eb0214346920c4fbfd2ffd18a6fa0775 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Sun, 18 Oct 2020 01:13:39 +0700 Subject: [PATCH 07/24] TYP: add type annotations --- pandas/plotting/_matplotlib/style.py | 64 +++++++++++++++++++++------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index a28e1e66ad775..63762f6578e3b 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -1,4 +1,13 @@ -# being a bit too dynamic +from typing import ( + TYPE_CHECKING, + Collection, + Dict, + Iterable, + List, + Optional, + Sequence, + Union, +) import warnings import matplotlib.cm as cm @@ -6,9 +15,14 @@ import matplotlib.pyplot as plt import numpy as np - import pandas.core.common as com +if TYPE_CHECKING: + from matplotlib.colors import Colormap + + +Color = Union[str, Sequence[float]] + def get_standard_colors( num_colors: int, @@ -23,10 +37,19 @@ def get_standard_colors( num_colors=num_colors, ) - return _cycle_colors(colors, num_colors=num_colors) + if isinstance(colors, dict): + return colors + return _cycle_colors(list(colors), num_colors=num_colors) -def _get_colors(*, color, colormap, color_type, num_colors): + +def _get_colors( + *, + color: Optional[Union[Color, Dict[str, Color], Collection[Color]]], + colormap: Optional[Union[str, "Colormap"]], + color_type: str, + num_colors: int, +) -> Union[Dict[str, Color], Collection[Color]]: """Get colors from user input.""" if color is None and colormap is not None: return _get_colors_from_colormap(colormap, num_colors=num_colors) @@ -40,7 +63,7 @@ def _get_colors(*, color, colormap, color_type, num_colors): return _get_colors_from_color_type(color_type, num_colors=num_colors) -def _cycle_colors(colors, num_colors): +def _cycle_colors(colors: List[Color], num_colors: int) -> List[Color]: """Append more colors by cycling if there is not enough color. Extra colors will be ignored by matplotlib if there are more colors @@ -55,13 +78,16 @@ def _cycle_colors(colors, num_colors): return colors -def _get_colors_from_colormap(colormap, num_colors): +def _get_colors_from_colormap( + colormap: Union[str, "Colormap"], + num_colors: int, +) -> Collection[Color]: """Get colors from colormap.""" colormap = _get_cmap_instance(colormap) return [colormap(num) for num in np.linspace(0, 1, num=num_colors)] -def _get_cmap_instance(colormap): +def _get_cmap_instance(colormap: Union[str, "Colormap"]) -> "Colormap": """Get instance of matplotlib colormap.""" if isinstance(colormap, str): cmap = colormap @@ -71,22 +97,30 @@ def _get_cmap_instance(colormap): return colormap -def _get_colors_from_color(color): +def _get_colors_from_color( + color: Union[Color, Dict[str, Color], Collection[Color]], +) -> Union[Dict[str, Color], Collection[Color]]: """Get colors from user input color.""" - if len(color) == 0: + if isinstance(color, Iterable) and len(color) == 0: raise ValueError("Invalid color argument: {color}") if isinstance(color, dict): return color - if _is_single_color(color): + if isinstance(color, str) and _is_single_color(color): # GH #36972 return [color] - return list(color) + # ignoring mypy error here + # error: Argument 1 to "list" has incompatible type + # "Union[Sequence[float], Collection[Union[str, Sequence[float]]]]"; + # expected "Iterable[Union[str, Sequence[float]]]" [arg-type] + # A this point color may be string with multiple letters, + # sequence of floats or series of colors, all convertible to list + return list(color) # type: ignore [arg-type] -def _get_colors_from_color_type(color_type, num_colors): +def _get_colors_from_color_type(color_type: str, num_colors: int) -> Collection[Color]: """Get colors from user input color type.""" if color_type == "default": return _get_default_colors(num_colors) @@ -96,7 +130,7 @@ def _get_colors_from_color_type(color_type, num_colors): raise ValueError("color_type must be either 'default' or 'random'") -def _get_default_colors(num_colors): +def _get_default_colors(num_colors: int) -> Collection[Color]: """Get ``num_colors`` of default colors from matplotlib rc params.""" # need to call list() on the result to copy so we don't # modify the global rcParams below @@ -108,12 +142,12 @@ def _get_default_colors(num_colors): return colors[0:num_colors] -def _get_random_colors(num_colors): +def _get_random_colors(num_colors: int) -> Sequence[Sequence[float]]: """Get ``num_colors`` of random colors.""" return [_random_color(num) for num in range(num_colors)] -def _random_color(column): +def _random_color(column: int) -> Sequence[float]: """Get a random color represented as a list of length 3""" # GH17525 use common._random_state to avoid resetting the seed rs = com.random_state(column) From b4c32673cd73f3e5052aa33f7c2120e17cbdc99b Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Sun, 18 Oct 2020 01:58:29 +0700 Subject: [PATCH 08/24] REF: more explicitly handle string color --- pandas/plotting/_matplotlib/style.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index 63762f6578e3b..9d06f3cb96638 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -107,16 +107,19 @@ def _get_colors_from_color( if isinstance(color, dict): return color - if isinstance(color, str) and _is_single_color(color): - # GH #36972 - return [color] + if isinstance(color, str): + if _is_single_color(color): + # GH #36972 + return [color] + else: + return list(color) # ignoring mypy error here # error: Argument 1 to "list" has incompatible type # "Union[Sequence[float], Collection[Union[str, Sequence[float]]]]"; # expected "Iterable[Union[str, Sequence[float]]]" [arg-type] - # A this point color may be string with multiple letters, - # sequence of floats or series of colors, all convertible to list + # A this point color may be sequence of floats or series of colors, + # all convertible to list return list(color) # type: ignore [arg-type] From 6af1543af84ef7f4b68923be0af69f6d0e64cab3 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Sun, 18 Oct 2020 01:58:47 +0700 Subject: [PATCH 09/24] FIX: fix mpl registry reset --- pandas/plotting/_matplotlib/style.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index 9d06f3cb96638..52c827ca96f51 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -12,7 +12,6 @@ import matplotlib.cm as cm import matplotlib.colors -import matplotlib.pyplot as plt import numpy as np import pandas.core.common as com @@ -135,6 +134,8 @@ def _get_colors_from_color_type(color_type: str, num_colors: int) -> Collection[ def _get_default_colors(num_colors: int) -> Collection[Color]: """Get ``num_colors`` of default colors from matplotlib rc params.""" + import matplotlib.pyplot as plt + # need to call list() on the result to copy so we don't # modify the global rcParams below try: From 31125f7542d4a58f1e47825a363632659e826c03 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Sun, 18 Oct 2020 20:01:58 +0700 Subject: [PATCH 10/24] TYP: fix typing in _get_colors_from_color --- pandas/plotting/_matplotlib/style.py | 71 ++++++++++++++++------------ 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index 52c827ca96f51..1a3f853fc1db6 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -1,12 +1,11 @@ from typing import ( TYPE_CHECKING, Collection, - Dict, - Iterable, List, Optional, Sequence, Union, + cast, ) import warnings @@ -15,6 +14,7 @@ import numpy as np import pandas.core.common as com +from pandas.core.dtypes.common import is_list_like if TYPE_CHECKING: from matplotlib.colors import Colormap @@ -29,6 +29,9 @@ def get_standard_colors( color_type: str = "default", color=None, ): + if isinstance(color, dict): + return color + colors = _get_colors( color=color, colormap=colormap, @@ -36,19 +39,16 @@ def get_standard_colors( num_colors=num_colors, ) - if isinstance(colors, dict): - return colors - - return _cycle_colors(list(colors), num_colors=num_colors) + return _cycle_colors(colors, num_colors=num_colors) def _get_colors( *, - color: Optional[Union[Color, Dict[str, Color], Collection[Color]]], + color: Optional[Union[Color, Collection[Color]]], colormap: Optional[Union[str, "Colormap"]], color_type: str, num_colors: int, -) -> Union[Dict[str, Color], Collection[Color]]: +) -> List[Color]: """Get colors from user input.""" if color is None and colormap is not None: return _get_colors_from_colormap(colormap, num_colors=num_colors) @@ -80,7 +80,7 @@ def _cycle_colors(colors: List[Color], num_colors: int) -> List[Color]: def _get_colors_from_colormap( colormap: Union[str, "Colormap"], num_colors: int, -) -> Collection[Color]: +) -> List[Color]: """Get colors from colormap.""" colormap = _get_cmap_instance(colormap) return [colormap(num) for num in np.linspace(0, 1, num=num_colors)] @@ -97,14 +97,11 @@ def _get_cmap_instance(colormap: Union[str, "Colormap"]) -> "Colormap": def _get_colors_from_color( - color: Union[Color, Dict[str, Color], Collection[Color]], -) -> Union[Dict[str, Color], Collection[Color]]: + color: Union[Color, Collection[Color]], +) -> List[Color]: """Get colors from user input color.""" - if isinstance(color, Iterable) and len(color) == 0: - raise ValueError("Invalid color argument: {color}") - - if isinstance(color, dict): - return color + if len(color) == 0: + raise ValueError(f"Invalid color argument: {color}") if isinstance(color, str): if _is_single_color(color): @@ -113,16 +110,30 @@ def _get_colors_from_color( else: return list(color) - # ignoring mypy error here - # error: Argument 1 to "list" has incompatible type - # "Union[Sequence[float], Collection[Union[str, Sequence[float]]]]"; - # expected "Iterable[Union[str, Sequence[float]]]" [arg-type] - # A this point color may be sequence of floats or series of colors, - # all convertible to list - return list(color) # type: ignore [arg-type] + if _is_floats_color(color): + color = cast(Sequence[float], color) + return [color] + + color = cast(Collection[Color], color) + colors = [] + for x in color: + if _is_single_color(x): + colors.append(x) + else: + raise ValueError(f"Invalid color {x}") + return colors + + +def _is_floats_color(color: Union[Color, Collection[Color]]) -> bool: + """Check if color comprises a sequence of floats representing color.""" + return bool( + is_list_like(color) + and (len(color) == 3 or len(color) == 4) + and all([isinstance(x, float) for x in color]) + ) -def _get_colors_from_color_type(color_type: str, num_colors: int) -> Collection[Color]: +def _get_colors_from_color_type(color_type: str, num_colors: int) -> List[Color]: """Get colors from user input color type.""" if color_type == "default": return _get_default_colors(num_colors) @@ -132,7 +143,7 @@ def _get_colors_from_color_type(color_type: str, num_colors: int) -> Collection[ raise ValueError("color_type must be either 'default' or 'random'") -def _get_default_colors(num_colors: int) -> Collection[Color]: +def _get_default_colors(num_colors: int) -> List[Color]: """Get ``num_colors`` of default colors from matplotlib rc params.""" import matplotlib.pyplot as plt @@ -146,19 +157,19 @@ def _get_default_colors(num_colors: int) -> Collection[Color]: return colors[0:num_colors] -def _get_random_colors(num_colors: int) -> Sequence[Sequence[float]]: +def _get_random_colors(num_colors: int) -> List[Color]: """Get ``num_colors`` of random colors.""" return [_random_color(num) for num in range(num_colors)] -def _random_color(column: int) -> Sequence[float]: +def _random_color(column: int) -> List[float]: """Get a random color represented as a list of length 3""" # GH17525 use common._random_state to avoid resetting the seed rs = com.random_state(column) return rs.rand(3).tolist() -def _is_single_color(color: str) -> bool: +def _is_single_color(color: Color) -> bool: """Check if ``color`` is a single color. Examples of single colors: @@ -170,8 +181,8 @@ def _is_single_color(color: str) -> bool: Parameters ---------- - color : string - Color string. + color : Color + Color string or sequence of floats. Returns ------- From 45647a49b09704b292b1eb86ec5786b30dc1fcd2 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Sun, 18 Oct 2020 20:10:48 +0700 Subject: [PATCH 11/24] CLN: eliminate use of legacy "axes.color_cycle" It turns out that even if "axes.prop_cycle" is not defined, then it will be composed of CN colors (hex notation). However, in the current matplotlib there is no "axes.color_cycle" property in rcParams at all (not even allowed to setup). --- pandas/plotting/_matplotlib/style.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index 1a3f853fc1db6..095b8755b021d 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -147,13 +147,7 @@ def _get_default_colors(num_colors: int) -> List[Color]: """Get ``num_colors`` of default colors from matplotlib rc params.""" import matplotlib.pyplot as plt - # need to call list() on the result to copy so we don't - # modify the global rcParams below - try: - colors = [c["color"] for c in list(plt.rcParams["axes.prop_cycle"])] - except KeyError: - colors = list(plt.rcParams.get("axes.color_cycle", list("bgrcmyk"))) - + colors = [c["color"] for c in plt.rcParams["axes.prop_cycle"]] return colors[0:num_colors] From 393ae463077d7c4010d94f83381471563dacdcaa Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Sun, 18 Oct 2020 20:49:03 +0700 Subject: [PATCH 12/24] REF: extract generator function to simplify logic --- pandas/plotting/_matplotlib/style.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index 095b8755b021d..8e531f59e6a46 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -1,6 +1,7 @@ from typing import ( TYPE_CHECKING, Collection, + Iterator, List, Optional, Sequence, @@ -13,9 +14,10 @@ import matplotlib.colors import numpy as np -import pandas.core.common as com from pandas.core.dtypes.common import is_list_like +import pandas.core.common as com + if TYPE_CHECKING: from matplotlib.colors import Colormap @@ -103,25 +105,30 @@ def _get_colors_from_color( if len(color) == 0: raise ValueError(f"Invalid color argument: {color}") - if isinstance(color, str): - if _is_single_color(color): - # GH #36972 - return [color] - else: - return list(color) + if isinstance(color, str) and _is_single_color(color): + # GH #36972 + return [color] if _is_floats_color(color): color = cast(Sequence[float], color) return [color] color = cast(Collection[Color], color) - colors = [] + return list(_gen_list_of_colors_from_iterable(color)) + + +def _gen_list_of_colors_from_iterable(color: Collection[Color]) -> Iterator[Color]: + """ + Yield colors from string of several letters or from collection of colors. + """ for x in color: + if isinstance(x, str): + # to avoid warnings on upper case single letter colors + x = x.lower() if _is_single_color(x): - colors.append(x) + yield x else: raise ValueError(f"Invalid color {x}") - return colors def _is_floats_color(color: Union[Color, Collection[Color]]) -> bool: @@ -172,6 +179,7 @@ def _is_single_color(color: Color) -> bool: - 'red' - 'green' - 'C3' + - 'firebrick' Parameters ---------- From fe66213f9b3296b18092257be3b6662cb8f78e89 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Sun, 18 Oct 2020 20:51:32 +0700 Subject: [PATCH 13/24] TST: add tests for get_standard_colors --- pandas/tests/plotting/test_style.py | 150 ++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 pandas/tests/plotting/test_style.py diff --git a/pandas/tests/plotting/test_style.py b/pandas/tests/plotting/test_style.py new file mode 100644 index 0000000000000..cbb8a75bbe310 --- /dev/null +++ b/pandas/tests/plotting/test_style.py @@ -0,0 +1,150 @@ +from cycler import cycler +import matplotlib as mpl +import matplotlib.colors +import pytest + +from pandas import Series + +from pandas.plotting._matplotlib.style import get_standard_colors + + +class TestGetStandardColors: + @pytest.mark.parametrize( + "num_colors, expected", + [ + (3, ["red", "green", "blue"]), + (5, ["red", "green", "blue", "red", "green"]), + (7, ["red", "green", "blue", "red", "green", "blue", "red"]), + (2, ["red", "green"]), + (1, ["red"]), + ], + ) + def test_default_colors_named_from_prop_cycle(self, num_colors, expected): + mpl_params = { + "axes.prop_cycle": cycler(color=["red", "green", "blue"]), + } + with mpl.rc_context(rc=mpl_params): + result = get_standard_colors(num_colors=num_colors) + assert result == expected + + @pytest.mark.parametrize( + "num_colors, expected", + [ + (1, ["b"]), + (3, ["b", "g", "r"]), + (4, ["b", "g", "r", "y"]), + (5, ["b", "g", "r", "y", "b"]), + (7, ["b", "g", "r", "y", "b", "g", "r"]), + ], + ) + def test_default_colors_named_from_prop_cycle_string(self, num_colors, expected): + mpl_params = { + "axes.prop_cycle": cycler(color="bgry"), + } + with mpl.rc_context(rc=mpl_params): + result = get_standard_colors(num_colors=num_colors) + assert result == expected + + @pytest.mark.parametrize( + "num_colors, expected_name", + [ + (1, ["C0"]), + (3, ["C0", "C1", "C2"]), + ( + 12, + [ + "C0", + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C0", + "C1", + ], + ), + ], + ) + def test_default_colors_named_undefined_prop_cycle(self, num_colors, expected_name): + with mpl.rc_context(rc={}): + expected = [matplotlib.colors.to_hex(x) for x in expected_name] + result = get_standard_colors(num_colors=num_colors) + assert result == expected + + @pytest.mark.parametrize( + "num_colors, expected", + [ + (1, ["red", "green", (0.1, 0.2, 0.3)]), + (2, ["red", "green", (0.1, 0.2, 0.3)]), + (3, ["red", "green", (0.1, 0.2, 0.3)]), + (4, ["red", "green", (0.1, 0.2, 0.3), "red"]), + ], + ) + def test_user_input_color_sequence(self, num_colors, expected): + color = ["red", "green", (0.1, 0.2, 0.3)] + result = get_standard_colors(color=color, num_colors=num_colors) + assert result == expected + + @pytest.mark.parametrize( + "num_colors, expected", + [ + (1, ["r", "g", "b", "k"]), + (2, ["r", "g", "b", "k"]), + (3, ["r", "g", "b", "k"]), + (4, ["r", "g", "b", "k"]), + (5, ["r", "g", "b", "k", "r"]), + (6, ["r", "g", "b", "k", "r", "g"]), + ], + ) + def test_user_input_color_string(self, num_colors, expected): + color = "rgbk" + result = get_standard_colors(color=color, num_colors=num_colors) + assert result == expected + + @pytest.mark.parametrize( + "num_colors, expected", + [ + (1, [(0.1, 0.2, 0.3)]), + (2, [(0.1, 0.2, 0.3), (0.1, 0.2, 0.3)]), + (3, [(0.1, 0.2, 0.3), (0.1, 0.2, 0.3), (0.1, 0.2, 0.3)]), + ], + ) + def test_user_input_color_floats(self, num_colors, expected): + color = (0.1, 0.2, 0.3) + result = get_standard_colors(color=color, num_colors=num_colors) + assert result == expected + + @pytest.mark.parametrize( + "color, num_colors, expected", + [ + ("Crimson", 1, ["Crimson"]), + ("DodgerBlue", 2, ["DodgerBlue", "DodgerBlue"]), + ("firebrick", 3, ["firebrick", "firebrick", "firebrick"]), + ], + ) + def test_user_input_named_color_string(self, color, num_colors, expected): + result = get_standard_colors(color=color, num_colors=num_colors) + assert result == expected + + @pytest.mark.parametrize("color", ["", [], (), Series([], dtype="object")]) + def test_empty_color_raises(self, color): + with pytest.raises(ValueError, match="Invalid color argument"): + get_standard_colors(color=color, num_colors=1) + + @pytest.mark.parametrize( + "color", + [ + "BAD_COLOR", + ("red", "green", "BAD_COLOR"), + (0.1,), + (0.1, 0.2), + (0.1, 0.2, 0.3, 0.4, 0.5), # must be either 3 or 4 floats + ], + ) + def test_bad_color_raises(self, color): + with pytest.raises(ValueError, match="Invalid color"): + get_standard_colors(color=color, num_colors=5) From 1626108adfa7075edb137e57aadd4b6542dcd1c3 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Sun, 18 Oct 2020 20:53:42 +0700 Subject: [PATCH 14/24] CLN: drop list comprehension for generator expr --- pandas/plotting/_matplotlib/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index 8e531f59e6a46..44709b486e2bf 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -136,7 +136,7 @@ def _is_floats_color(color: Union[Color, Collection[Color]]) -> bool: return bool( is_list_like(color) and (len(color) == 3 or len(color) == 4) - and all([isinstance(x, float) for x in color]) + and all(isinstance(x, float) for x in color) ) From 79b0f08cf4c0b050f3c92103872d8a089b6f89c9 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Sun, 18 Oct 2020 20:58:12 +0700 Subject: [PATCH 15/24] TYP: annotate get_standard_colors --- pandas/plotting/_matplotlib/style.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index 44709b486e2bf..da4111456a4d3 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -1,6 +1,7 @@ from typing import ( TYPE_CHECKING, Collection, + Dict, Iterator, List, Optional, @@ -27,9 +28,9 @@ def get_standard_colors( num_colors: int, - colormap=None, + colormap: Optional["Colormap"] = None, color_type: str = "default", - color=None, + color: Optional[Union[Dict[str, Color], Color, Collection[Color]]] = None, ): if isinstance(color, dict): return color From f513bdb069d2650b89a07c31cc33ed4b1ae7268a Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Sun, 18 Oct 2020 21:36:24 +0700 Subject: [PATCH 16/24] DEP: add testing dependency (cycler) --- environment.yml | 1 + pandas/plotting/_matplotlib/style.py | 3 --- pandas/tests/plotting/test_style.py | 15 ++++++++++----- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/environment.yml b/environment.yml index 77a9c5fd4822d..7efb12ffb6b19 100644 --- a/environment.yml +++ b/environment.yml @@ -52,6 +52,7 @@ dependencies: - botocore>=1.11 - hypothesis>=3.82 - moto # mock S3 + - cycler # test matplotlib colors cycle - flask - pytest>=5.0.1 - pytest-cov diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index da4111456a4d3..b8f162008f852 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -123,9 +123,6 @@ def _gen_list_of_colors_from_iterable(color: Collection[Color]) -> Iterator[Colo Yield colors from string of several letters or from collection of colors. """ for x in color: - if isinstance(x, str): - # to avoid warnings on upper case single letter colors - x = x.lower() if _is_single_color(x): yield x else: diff --git a/pandas/tests/plotting/test_style.py b/pandas/tests/plotting/test_style.py index cbb8a75bbe310..7fb041b44acb6 100644 --- a/pandas/tests/plotting/test_style.py +++ b/pandas/tests/plotting/test_style.py @@ -1,6 +1,4 @@ from cycler import cycler -import matplotlib as mpl -import matplotlib.colors import pytest from pandas import Series @@ -20,6 +18,8 @@ class TestGetStandardColors: ], ) def test_default_colors_named_from_prop_cycle(self, num_colors, expected): + import matplotlib as mpl + mpl_params = { "axes.prop_cycle": cycler(color=["red", "green", "blue"]), } @@ -38,6 +38,8 @@ def test_default_colors_named_from_prop_cycle(self, num_colors, expected): ], ) def test_default_colors_named_from_prop_cycle_string(self, num_colors, expected): + import matplotlib as mpl + mpl_params = { "axes.prop_cycle": cycler(color="bgry"), } @@ -70,8 +72,11 @@ def test_default_colors_named_from_prop_cycle_string(self, num_colors, expected) ], ) def test_default_colors_named_undefined_prop_cycle(self, num_colors, expected_name): + import matplotlib as mpl + import matplotlib.colors as mcolors + with mpl.rc_context(rc={}): - expected = [matplotlib.colors.to_hex(x) for x in expected_name] + expected = [mcolors.to_hex(x) for x in expected_name] result = get_standard_colors(num_colors=num_colors) assert result == expected @@ -138,8 +143,8 @@ def test_empty_color_raises(self, color): @pytest.mark.parametrize( "color", [ - "BAD_COLOR", - ("red", "green", "BAD_COLOR"), + "bad_color", + ("red", "green", "bad_color"), (0.1,), (0.1, 0.2), (0.1, 0.2, 0.3, 0.4, 0.5), # must be either 3 or 4 floats From 76f76631b9814987482d9b2fe0150f2527e9d25b Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Sun, 18 Oct 2020 23:15:53 +0700 Subject: [PATCH 17/24] Remove test_style temporary --- pandas/tests/plotting/test_style.py | 155 ---------------------------- 1 file changed, 155 deletions(-) delete mode 100644 pandas/tests/plotting/test_style.py diff --git a/pandas/tests/plotting/test_style.py b/pandas/tests/plotting/test_style.py deleted file mode 100644 index 7fb041b44acb6..0000000000000 --- a/pandas/tests/plotting/test_style.py +++ /dev/null @@ -1,155 +0,0 @@ -from cycler import cycler -import pytest - -from pandas import Series - -from pandas.plotting._matplotlib.style import get_standard_colors - - -class TestGetStandardColors: - @pytest.mark.parametrize( - "num_colors, expected", - [ - (3, ["red", "green", "blue"]), - (5, ["red", "green", "blue", "red", "green"]), - (7, ["red", "green", "blue", "red", "green", "blue", "red"]), - (2, ["red", "green"]), - (1, ["red"]), - ], - ) - def test_default_colors_named_from_prop_cycle(self, num_colors, expected): - import matplotlib as mpl - - mpl_params = { - "axes.prop_cycle": cycler(color=["red", "green", "blue"]), - } - with mpl.rc_context(rc=mpl_params): - result = get_standard_colors(num_colors=num_colors) - assert result == expected - - @pytest.mark.parametrize( - "num_colors, expected", - [ - (1, ["b"]), - (3, ["b", "g", "r"]), - (4, ["b", "g", "r", "y"]), - (5, ["b", "g", "r", "y", "b"]), - (7, ["b", "g", "r", "y", "b", "g", "r"]), - ], - ) - def test_default_colors_named_from_prop_cycle_string(self, num_colors, expected): - import matplotlib as mpl - - mpl_params = { - "axes.prop_cycle": cycler(color="bgry"), - } - with mpl.rc_context(rc=mpl_params): - result = get_standard_colors(num_colors=num_colors) - assert result == expected - - @pytest.mark.parametrize( - "num_colors, expected_name", - [ - (1, ["C0"]), - (3, ["C0", "C1", "C2"]), - ( - 12, - [ - "C0", - "C1", - "C2", - "C3", - "C4", - "C5", - "C6", - "C7", - "C8", - "C9", - "C0", - "C1", - ], - ), - ], - ) - def test_default_colors_named_undefined_prop_cycle(self, num_colors, expected_name): - import matplotlib as mpl - import matplotlib.colors as mcolors - - with mpl.rc_context(rc={}): - expected = [mcolors.to_hex(x) for x in expected_name] - result = get_standard_colors(num_colors=num_colors) - assert result == expected - - @pytest.mark.parametrize( - "num_colors, expected", - [ - (1, ["red", "green", (0.1, 0.2, 0.3)]), - (2, ["red", "green", (0.1, 0.2, 0.3)]), - (3, ["red", "green", (0.1, 0.2, 0.3)]), - (4, ["red", "green", (0.1, 0.2, 0.3), "red"]), - ], - ) - def test_user_input_color_sequence(self, num_colors, expected): - color = ["red", "green", (0.1, 0.2, 0.3)] - result = get_standard_colors(color=color, num_colors=num_colors) - assert result == expected - - @pytest.mark.parametrize( - "num_colors, expected", - [ - (1, ["r", "g", "b", "k"]), - (2, ["r", "g", "b", "k"]), - (3, ["r", "g", "b", "k"]), - (4, ["r", "g", "b", "k"]), - (5, ["r", "g", "b", "k", "r"]), - (6, ["r", "g", "b", "k", "r", "g"]), - ], - ) - def test_user_input_color_string(self, num_colors, expected): - color = "rgbk" - result = get_standard_colors(color=color, num_colors=num_colors) - assert result == expected - - @pytest.mark.parametrize( - "num_colors, expected", - [ - (1, [(0.1, 0.2, 0.3)]), - (2, [(0.1, 0.2, 0.3), (0.1, 0.2, 0.3)]), - (3, [(0.1, 0.2, 0.3), (0.1, 0.2, 0.3), (0.1, 0.2, 0.3)]), - ], - ) - def test_user_input_color_floats(self, num_colors, expected): - color = (0.1, 0.2, 0.3) - result = get_standard_colors(color=color, num_colors=num_colors) - assert result == expected - - @pytest.mark.parametrize( - "color, num_colors, expected", - [ - ("Crimson", 1, ["Crimson"]), - ("DodgerBlue", 2, ["DodgerBlue", "DodgerBlue"]), - ("firebrick", 3, ["firebrick", "firebrick", "firebrick"]), - ], - ) - def test_user_input_named_color_string(self, color, num_colors, expected): - result = get_standard_colors(color=color, num_colors=num_colors) - assert result == expected - - @pytest.mark.parametrize("color", ["", [], (), Series([], dtype="object")]) - def test_empty_color_raises(self, color): - with pytest.raises(ValueError, match="Invalid color argument"): - get_standard_colors(color=color, num_colors=1) - - @pytest.mark.parametrize( - "color", - [ - "bad_color", - ("red", "green", "bad_color"), - (0.1,), - (0.1, 0.2), - (0.1, 0.2, 0.3, 0.4, 0.5), # must be either 3 or 4 floats - ], - ) - def test_bad_color_raises(self, color): - with pytest.raises(ValueError, match="Invalid color"): - get_standard_colors(color=color, num_colors=5) From 0f0f4bc917d9630d7c37a05a7108c3c865e5b898 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Mon, 19 Oct 2020 00:29:44 +0700 Subject: [PATCH 18/24] BLD: remove cycler from dependencies temporary --- environment.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/environment.yml b/environment.yml index 7efb12ffb6b19..77a9c5fd4822d 100644 --- a/environment.yml +++ b/environment.yml @@ -52,7 +52,6 @@ dependencies: - botocore>=1.11 - hypothesis>=3.82 - moto # mock S3 - - cycler # test matplotlib colors cycle - flask - pytest>=5.0.1 - pytest-cov From b8daf79ac5b4b97f611f4d010644d6550bd270d2 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Mon, 19 Oct 2020 11:57:28 +0700 Subject: [PATCH 19/24] Revert "Remove test_style temporary" This reverts commit 76f76631b9814987482d9b2fe0150f2527e9d25b. --- pandas/tests/plotting/test_style.py | 155 ++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 pandas/tests/plotting/test_style.py diff --git a/pandas/tests/plotting/test_style.py b/pandas/tests/plotting/test_style.py new file mode 100644 index 0000000000000..7fb041b44acb6 --- /dev/null +++ b/pandas/tests/plotting/test_style.py @@ -0,0 +1,155 @@ +from cycler import cycler +import pytest + +from pandas import Series + +from pandas.plotting._matplotlib.style import get_standard_colors + + +class TestGetStandardColors: + @pytest.mark.parametrize( + "num_colors, expected", + [ + (3, ["red", "green", "blue"]), + (5, ["red", "green", "blue", "red", "green"]), + (7, ["red", "green", "blue", "red", "green", "blue", "red"]), + (2, ["red", "green"]), + (1, ["red"]), + ], + ) + def test_default_colors_named_from_prop_cycle(self, num_colors, expected): + import matplotlib as mpl + + mpl_params = { + "axes.prop_cycle": cycler(color=["red", "green", "blue"]), + } + with mpl.rc_context(rc=mpl_params): + result = get_standard_colors(num_colors=num_colors) + assert result == expected + + @pytest.mark.parametrize( + "num_colors, expected", + [ + (1, ["b"]), + (3, ["b", "g", "r"]), + (4, ["b", "g", "r", "y"]), + (5, ["b", "g", "r", "y", "b"]), + (7, ["b", "g", "r", "y", "b", "g", "r"]), + ], + ) + def test_default_colors_named_from_prop_cycle_string(self, num_colors, expected): + import matplotlib as mpl + + mpl_params = { + "axes.prop_cycle": cycler(color="bgry"), + } + with mpl.rc_context(rc=mpl_params): + result = get_standard_colors(num_colors=num_colors) + assert result == expected + + @pytest.mark.parametrize( + "num_colors, expected_name", + [ + (1, ["C0"]), + (3, ["C0", "C1", "C2"]), + ( + 12, + [ + "C0", + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C0", + "C1", + ], + ), + ], + ) + def test_default_colors_named_undefined_prop_cycle(self, num_colors, expected_name): + import matplotlib as mpl + import matplotlib.colors as mcolors + + with mpl.rc_context(rc={}): + expected = [mcolors.to_hex(x) for x in expected_name] + result = get_standard_colors(num_colors=num_colors) + assert result == expected + + @pytest.mark.parametrize( + "num_colors, expected", + [ + (1, ["red", "green", (0.1, 0.2, 0.3)]), + (2, ["red", "green", (0.1, 0.2, 0.3)]), + (3, ["red", "green", (0.1, 0.2, 0.3)]), + (4, ["red", "green", (0.1, 0.2, 0.3), "red"]), + ], + ) + def test_user_input_color_sequence(self, num_colors, expected): + color = ["red", "green", (0.1, 0.2, 0.3)] + result = get_standard_colors(color=color, num_colors=num_colors) + assert result == expected + + @pytest.mark.parametrize( + "num_colors, expected", + [ + (1, ["r", "g", "b", "k"]), + (2, ["r", "g", "b", "k"]), + (3, ["r", "g", "b", "k"]), + (4, ["r", "g", "b", "k"]), + (5, ["r", "g", "b", "k", "r"]), + (6, ["r", "g", "b", "k", "r", "g"]), + ], + ) + def test_user_input_color_string(self, num_colors, expected): + color = "rgbk" + result = get_standard_colors(color=color, num_colors=num_colors) + assert result == expected + + @pytest.mark.parametrize( + "num_colors, expected", + [ + (1, [(0.1, 0.2, 0.3)]), + (2, [(0.1, 0.2, 0.3), (0.1, 0.2, 0.3)]), + (3, [(0.1, 0.2, 0.3), (0.1, 0.2, 0.3), (0.1, 0.2, 0.3)]), + ], + ) + def test_user_input_color_floats(self, num_colors, expected): + color = (0.1, 0.2, 0.3) + result = get_standard_colors(color=color, num_colors=num_colors) + assert result == expected + + @pytest.mark.parametrize( + "color, num_colors, expected", + [ + ("Crimson", 1, ["Crimson"]), + ("DodgerBlue", 2, ["DodgerBlue", "DodgerBlue"]), + ("firebrick", 3, ["firebrick", "firebrick", "firebrick"]), + ], + ) + def test_user_input_named_color_string(self, color, num_colors, expected): + result = get_standard_colors(color=color, num_colors=num_colors) + assert result == expected + + @pytest.mark.parametrize("color", ["", [], (), Series([], dtype="object")]) + def test_empty_color_raises(self, color): + with pytest.raises(ValueError, match="Invalid color argument"): + get_standard_colors(color=color, num_colors=1) + + @pytest.mark.parametrize( + "color", + [ + "bad_color", + ("red", "green", "bad_color"), + (0.1,), + (0.1, 0.2), + (0.1, 0.2, 0.3, 0.4, 0.5), # must be either 3 or 4 floats + ], + ) + def test_bad_color_raises(self, color): + with pytest.raises(ValueError, match="Invalid color"): + get_standard_colors(color=color, num_colors=5) From 37734e847e2b68c3d86e9607e700e2a860277ad5 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Mon, 19 Oct 2020 12:18:53 +0700 Subject: [PATCH 20/24] REF: import cycler from matplotlib.pyplot --- pandas/tests/plotting/test_style.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/tests/plotting/test_style.py b/pandas/tests/plotting/test_style.py index 7fb041b44acb6..57cb7d7c6ba67 100644 --- a/pandas/tests/plotting/test_style.py +++ b/pandas/tests/plotting/test_style.py @@ -1,4 +1,3 @@ -from cycler import cycler import pytest from pandas import Series @@ -19,6 +18,7 @@ class TestGetStandardColors: ) def test_default_colors_named_from_prop_cycle(self, num_colors, expected): import matplotlib as mpl + from matplotlib.pyplot import cycler mpl_params = { "axes.prop_cycle": cycler(color=["red", "green", "blue"]), @@ -39,6 +39,7 @@ def test_default_colors_named_from_prop_cycle(self, num_colors, expected): ) def test_default_colors_named_from_prop_cycle_string(self, num_colors, expected): import matplotlib as mpl + from matplotlib.pyplot import cycler mpl_params = { "axes.prop_cycle": cycler(color="bgry"), From 765836f3a7ae90c878b6044597a355b91d585fbc Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Mon, 19 Oct 2020 12:59:55 +0700 Subject: [PATCH 21/24] TST: mark test skip if no mpl --- pandas/tests/plotting/test_style.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pandas/tests/plotting/test_style.py b/pandas/tests/plotting/test_style.py index 57cb7d7c6ba67..d58983a2352da 100644 --- a/pandas/tests/plotting/test_style.py +++ b/pandas/tests/plotting/test_style.py @@ -1,10 +1,16 @@ +from contextlib import suppress + import pytest +import pandas.util._test_decorators as td + from pandas import Series -from pandas.plotting._matplotlib.style import get_standard_colors +with suppress(ImportError): + from pandas.plotting._matplotlib.style import get_standard_colors +@td.skip_if_no_mpl class TestGetStandardColors: @pytest.mark.parametrize( "num_colors, expected", From f0ea70176ba7f78feadacb0fcba6f7ed96a1a47b Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Fri, 23 Oct 2020 23:32:55 +0700 Subject: [PATCH 22/24] REF: use pytest.importorskip --- pandas/tests/plotting/test_style.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pandas/tests/plotting/test_style.py b/pandas/tests/plotting/test_style.py index d58983a2352da..665bda15724fd 100644 --- a/pandas/tests/plotting/test_style.py +++ b/pandas/tests/plotting/test_style.py @@ -1,16 +1,11 @@ -from contextlib import suppress - import pytest -import pandas.util._test_decorators as td - from pandas import Series -with suppress(ImportError): - from pandas.plotting._matplotlib.style import get_standard_colors +pytest.importorskip("matplotlib") +from pandas.plotting._matplotlib.style import get_standard_colors -@td.skip_if_no_mpl class TestGetStandardColors: @pytest.mark.parametrize( "num_colors, expected", From dedd0dd266e5d25a289371a7273d2ce710e58cf1 Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Fri, 30 Oct 2020 15:35:01 +0700 Subject: [PATCH 23/24] REF: extract new method _is_single_color 1. Rename _is_single_color -> _is_single_string_color 2. Extract new method _is_single_color, which checks whether the color provided is a single color, that can be either string or a sequence of floats. 3. Allow integers to be a part of float sequence color, (1, 0.4, 0.2, 0.5), for example. --- pandas/plotting/_matplotlib/style.py | 34 +++++++++++++++++++++------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index b8f162008f852..e5ede22ca7126 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -106,18 +106,36 @@ def _get_colors_from_color( if len(color) == 0: raise ValueError(f"Invalid color argument: {color}") - if isinstance(color, str) and _is_single_color(color): - # GH #36972 - return [color] - - if _is_floats_color(color): - color = cast(Sequence[float], color) + if _is_single_color(color): + color = cast(Color, color) return [color] color = cast(Collection[Color], color) return list(_gen_list_of_colors_from_iterable(color)) +def _is_single_color(color: Union[Color, Collection[Color]]) -> bool: + """Check if ``color`` is a single color, not a sequence of colors. + + Single color is of these kinds: + - Named color "red" + - Alias "g" + - Sequence of floats, such as (0.1, 0.2, 0.3) or (0.1, 0.2, 0.3, 0.4). + + See Also + -------- + _is_single_string_color + """ + if isinstance(color, str) and _is_single_string_color(color): + # GH #36972 + return True + + if _is_floats_color(color): + return True + + return False + + def _gen_list_of_colors_from_iterable(color: Collection[Color]) -> Iterator[Color]: """ Yield colors from string of several letters or from collection of colors. @@ -134,7 +152,7 @@ def _is_floats_color(color: Union[Color, Collection[Color]]) -> bool: return bool( is_list_like(color) and (len(color) == 3 or len(color) == 4) - and all(isinstance(x, float) for x in color) + and all(isinstance(x, (int, float)) for x in color) ) @@ -168,7 +186,7 @@ def _random_color(column: int) -> List[float]: return rs.rand(3).tolist() -def _is_single_color(color: Color) -> bool: +def _is_single_string_color(color: Color) -> bool: """Check if ``color`` is a single color. Examples of single colors: From b36983414ba8663173af4f421286d286737a4c0a Mon Sep 17 00:00:00 2001 From: Maxim Ivanov Date: Tue, 3 Nov 2020 19:38:52 +0700 Subject: [PATCH 24/24] DOC: add/update docstrings --- pandas/plotting/_matplotlib/style.py | 83 ++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 10 deletions(-) diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index e5ede22ca7126..b2c7b2610845c 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -32,10 +32,42 @@ def get_standard_colors( color_type: str = "default", color: Optional[Union[Dict[str, Color], Color, Collection[Color]]] = None, ): + """ + Get standard colors based on `colormap`, `color_type` or `color` inputs. + + Parameters + ---------- + num_colors : int + Minimum number of colors to be returned. + Ignored if `color` is a dictionary. + colormap : :py:class:`matplotlib.colors.Colormap`, optional + Matplotlib colormap. + When provided, the resulting colors will be derived from the colormap. + color_type : {"default", "random"}, optional + Type of colors to derive. Used if provided `color` and `colormap` are None. + Ignored if either `color` or `colormap` are not None. + color : dict or str or sequence, optional + Color(s) to be used for deriving sequence of colors. + Can be either be a dictionary, or a single color (single color string, + or sequence of floats representing a single color), + or a sequence of colors. + + Returns + ------- + dict or list + Standard colors. Can either be a mapping if `color` was a dictionary, + or a list of colors with a length of `num_colors` or more. + + Warns + ----- + UserWarning + If both `colormap` and `color` are provided. + Parameter `color` will override. + """ if isinstance(color, dict): return color - colors = _get_colors( + colors = _derive_colors( color=color, colormap=colormap, color_type=color_type, @@ -45,14 +77,45 @@ def get_standard_colors( return _cycle_colors(colors, num_colors=num_colors) -def _get_colors( +def _derive_colors( *, color: Optional[Union[Color, Collection[Color]]], colormap: Optional[Union[str, "Colormap"]], color_type: str, num_colors: int, ) -> List[Color]: - """Get colors from user input.""" + """ + Derive colors from either `colormap`, `color_type` or `color` inputs. + + Get a list of colors either from `colormap`, or from `color`, + or from `color_type` (if both `colormap` and `color` are None). + + Parameters + ---------- + color : str or sequence, optional + Color(s) to be used for deriving sequence of colors. + Can be either be a single color (single color string, or sequence of floats + representing a single color), or a sequence of colors. + colormap : :py:class:`matplotlib.colors.Colormap`, optional + Matplotlib colormap. + When provided, the resulting colors will be derived from the colormap. + color_type : {"default", "random"}, optional + Type of colors to derive. Used if provided `color` and `colormap` are None. + Ignored if either `color` or `colormap`` are not None. + num_colors : int + Number of colors to be extracted. + + Returns + ------- + list + List of colors extracted. + + Warns + ----- + UserWarning + If both `colormap` and `color` are provided. + Parameter `color` will override. + """ if color is None and colormap is not None: return _get_colors_from_colormap(colormap, num_colors=num_colors) elif color is not None: @@ -115,10 +178,10 @@ def _get_colors_from_color( def _is_single_color(color: Union[Color, Collection[Color]]) -> bool: - """Check if ``color`` is a single color, not a sequence of colors. + """Check if `color` is a single color, not a sequence of colors. Single color is of these kinds: - - Named color "red" + - Named color "red", "C0", "firebrick" - Alias "g" - Sequence of floats, such as (0.1, 0.2, 0.3) or (0.1, 0.2, 0.3, 0.4). @@ -167,7 +230,7 @@ def _get_colors_from_color_type(color_type: str, num_colors: int) -> List[Color] def _get_default_colors(num_colors: int) -> List[Color]: - """Get ``num_colors`` of default colors from matplotlib rc params.""" + """Get `num_colors` of default colors from matplotlib rc params.""" import matplotlib.pyplot as plt colors = [c["color"] for c in plt.rcParams["axes.prop_cycle"]] @@ -175,7 +238,7 @@ def _get_default_colors(num_colors: int) -> List[Color]: def _get_random_colors(num_colors: int) -> List[Color]: - """Get ``num_colors`` of random colors.""" + """Get `num_colors` of random colors.""" return [_random_color(num) for num in range(num_colors)] @@ -187,9 +250,9 @@ def _random_color(column: int) -> List[float]: def _is_single_string_color(color: Color) -> bool: - """Check if ``color`` is a single color. + """Check if `color` is a single string color. - Examples of single colors: + Examples of single string colors: - 'r' - 'g' - 'red' @@ -205,7 +268,7 @@ def _is_single_string_color(color: Color) -> bool: Returns ------- bool - True if ``color`` looks like a valid color. + True if `color` looks like a valid color. False otherwise. """ conv = matplotlib.colors.ColorConverter()