|
1 | | -# being a bit too dynamic |
| 1 | +from typing import ( |
| 2 | + TYPE_CHECKING, |
| 3 | + Collection, |
| 4 | + Dict, |
| 5 | + Iterator, |
| 6 | + List, |
| 7 | + Optional, |
| 8 | + Sequence, |
| 9 | + Union, |
| 10 | + cast, |
| 11 | +) |
2 | 12 | import warnings |
3 | 13 |
|
4 | 14 | import matplotlib.cm as cm |
|
9 | 19 |
|
10 | 20 | import pandas.core.common as com |
11 | 21 |
|
| 22 | +if TYPE_CHECKING: |
| 23 | + from matplotlib.colors import Colormap |
| 24 | + |
| 25 | + |
| 26 | +Color = Union[str, Sequence[float]] |
| 27 | + |
12 | 28 |
|
13 | 29 | def get_standard_colors( |
14 | | - num_colors: int, colormap=None, color_type: str = "default", color=None |
| 30 | + num_colors: int, |
| 31 | + colormap: Optional["Colormap"] = None, |
| 32 | + color_type: str = "default", |
| 33 | + color: Optional[Union[Dict[str, Color], Color, Collection[Color]]] = None, |
15 | 34 | ): |
16 | | - import matplotlib.pyplot as plt |
| 35 | + """ |
| 36 | + Get standard colors based on `colormap`, `color_type` or `color` inputs. |
| 37 | +
|
| 38 | + Parameters |
| 39 | + ---------- |
| 40 | + num_colors : int |
| 41 | + Minimum number of colors to be returned. |
| 42 | + Ignored if `color` is a dictionary. |
| 43 | + colormap : :py:class:`matplotlib.colors.Colormap`, optional |
| 44 | + Matplotlib colormap. |
| 45 | + When provided, the resulting colors will be derived from the colormap. |
| 46 | + color_type : {"default", "random"}, optional |
| 47 | + Type of colors to derive. Used if provided `color` and `colormap` are None. |
| 48 | + Ignored if either `color` or `colormap` are not None. |
| 49 | + color : dict or str or sequence, optional |
| 50 | + Color(s) to be used for deriving sequence of colors. |
| 51 | + Can be either be a dictionary, or a single color (single color string, |
| 52 | + or sequence of floats representing a single color), |
| 53 | + or a sequence of colors. |
| 54 | +
|
| 55 | + Returns |
| 56 | + ------- |
| 57 | + dict or list |
| 58 | + Standard colors. Can either be a mapping if `color` was a dictionary, |
| 59 | + or a list of colors with a length of `num_colors` or more. |
| 60 | +
|
| 61 | + Warns |
| 62 | + ----- |
| 63 | + UserWarning |
| 64 | + If both `colormap` and `color` are provided. |
| 65 | + Parameter `color` will override. |
| 66 | + """ |
| 67 | + if isinstance(color, dict): |
| 68 | + return color |
| 69 | + |
| 70 | + colors = _derive_colors( |
| 71 | + color=color, |
| 72 | + colormap=colormap, |
| 73 | + color_type=color_type, |
| 74 | + num_colors=num_colors, |
| 75 | + ) |
| 76 | + |
| 77 | + return _cycle_colors(colors, num_colors=num_colors) |
| 78 | + |
| 79 | + |
| 80 | +def _derive_colors( |
| 81 | + *, |
| 82 | + color: Optional[Union[Color, Collection[Color]]], |
| 83 | + colormap: Optional[Union[str, "Colormap"]], |
| 84 | + color_type: str, |
| 85 | + num_colors: int, |
| 86 | +) -> List[Color]: |
| 87 | + """ |
| 88 | + Derive colors from either `colormap`, `color_type` or `color` inputs. |
| 89 | +
|
| 90 | + Get a list of colors either from `colormap`, or from `color`, |
| 91 | + or from `color_type` (if both `colormap` and `color` are None). |
| 92 | +
|
| 93 | + Parameters |
| 94 | + ---------- |
| 95 | + color : str or sequence, optional |
| 96 | + Color(s) to be used for deriving sequence of colors. |
| 97 | + Can be either be a single color (single color string, or sequence of floats |
| 98 | + representing a single color), or a sequence of colors. |
| 99 | + colormap : :py:class:`matplotlib.colors.Colormap`, optional |
| 100 | + Matplotlib colormap. |
| 101 | + When provided, the resulting colors will be derived from the colormap. |
| 102 | + color_type : {"default", "random"}, optional |
| 103 | + Type of colors to derive. Used if provided `color` and `colormap` are None. |
| 104 | + Ignored if either `color` or `colormap`` are not None. |
| 105 | + num_colors : int |
| 106 | + Number of colors to be extracted. |
17 | 107 |
|
| 108 | + Returns |
| 109 | + ------- |
| 110 | + list |
| 111 | + List of colors extracted. |
| 112 | +
|
| 113 | + Warns |
| 114 | + ----- |
| 115 | + UserWarning |
| 116 | + If both `colormap` and `color` are provided. |
| 117 | + Parameter `color` will override. |
| 118 | + """ |
18 | 119 | if color is None and colormap is not None: |
19 | | - if isinstance(colormap, str): |
20 | | - cmap = colormap |
21 | | - colormap = cm.get_cmap(colormap) |
22 | | - if colormap is None: |
23 | | - raise ValueError(f"Colormap {cmap} is not recognized") |
24 | | - colors = [colormap(num) for num in np.linspace(0, 1, num=num_colors)] |
| 120 | + return _get_colors_from_colormap(colormap, num_colors=num_colors) |
25 | 121 | elif color is not None: |
26 | 122 | if colormap is not None: |
27 | 123 | warnings.warn( |
28 | 124 | "'color' and 'colormap' cannot be used simultaneously. Using 'color'" |
29 | 125 | ) |
30 | | - colors = ( |
31 | | - list(color) |
32 | | - if is_list_like(color) and not isinstance(color, dict) |
33 | | - else color |
34 | | - ) |
| 126 | + return _get_colors_from_color(color) |
35 | 127 | else: |
36 | | - if color_type == "default": |
37 | | - # need to call list() on the result to copy so we don't |
38 | | - # modify the global rcParams below |
39 | | - try: |
40 | | - colors = [c["color"] for c in list(plt.rcParams["axes.prop_cycle"])] |
41 | | - except KeyError: |
42 | | - colors = list(plt.rcParams.get("axes.color_cycle", list("bgrcmyk"))) |
43 | | - if isinstance(colors, str): |
44 | | - colors = list(colors) |
45 | | - |
46 | | - colors = colors[0:num_colors] |
47 | | - elif color_type == "random": |
48 | | - |
49 | | - def random_color(column): |
50 | | - """ Returns a random color represented as a list of length 3""" |
51 | | - # GH17525 use common._random_state to avoid resetting the seed |
52 | | - rs = com.random_state(column) |
53 | | - return rs.rand(3).tolist() |
54 | | - |
55 | | - colors = [random_color(num) for num in range(num_colors)] |
56 | | - else: |
57 | | - raise ValueError("color_type must be either 'default' or 'random'") |
| 128 | + return _get_colors_from_color_type(color_type, num_colors=num_colors) |
58 | 129 |
|
59 | | - if isinstance(colors, str) and _is_single_color(colors): |
60 | | - # GH #36972 |
61 | | - colors = [colors] |
62 | 130 |
|
63 | | - # Append more colors by cycling if there is not enough color. |
64 | | - # Extra colors will be ignored by matplotlib if there are more colors |
65 | | - # than needed and nothing needs to be done here. |
| 131 | +def _cycle_colors(colors: List[Color], num_colors: int) -> List[Color]: |
| 132 | + """Append more colors by cycling if there is not enough color. |
| 133 | +
|
| 134 | + Extra colors will be ignored by matplotlib if there are more colors |
| 135 | + than needed and nothing needs to be done here. |
| 136 | + """ |
66 | 137 | if len(colors) < num_colors: |
67 | | - try: |
68 | | - multiple = num_colors // len(colors) - 1 |
69 | | - except ZeroDivisionError: |
70 | | - raise ValueError("Invalid color argument: ''") |
| 138 | + multiple = num_colors // len(colors) - 1 |
71 | 139 | mod = num_colors % len(colors) |
72 | | - |
73 | 140 | colors += multiple * colors |
74 | 141 | colors += colors[:mod] |
75 | 142 |
|
76 | 143 | return colors |
77 | 144 |
|
78 | 145 |
|
79 | | -def _is_single_color(color: str) -> bool: |
80 | | - """Check if ``color`` is a single color. |
| 146 | +def _get_colors_from_colormap( |
| 147 | + colormap: Union[str, "Colormap"], |
| 148 | + num_colors: int, |
| 149 | +) -> List[Color]: |
| 150 | + """Get colors from colormap.""" |
| 151 | + colormap = _get_cmap_instance(colormap) |
| 152 | + return [colormap(num) for num in np.linspace(0, 1, num=num_colors)] |
| 153 | + |
| 154 | + |
| 155 | +def _get_cmap_instance(colormap: Union[str, "Colormap"]) -> "Colormap": |
| 156 | + """Get instance of matplotlib colormap.""" |
| 157 | + if isinstance(colormap, str): |
| 158 | + cmap = colormap |
| 159 | + colormap = cm.get_cmap(colormap) |
| 160 | + if colormap is None: |
| 161 | + raise ValueError(f"Colormap {cmap} is not recognized") |
| 162 | + return colormap |
| 163 | + |
| 164 | + |
| 165 | +def _get_colors_from_color( |
| 166 | + color: Union[Color, Collection[Color]], |
| 167 | +) -> List[Color]: |
| 168 | + """Get colors from user input color.""" |
| 169 | + if len(color) == 0: |
| 170 | + raise ValueError(f"Invalid color argument: {color}") |
| 171 | + |
| 172 | + if _is_single_color(color): |
| 173 | + color = cast(Color, color) |
| 174 | + return [color] |
| 175 | + |
| 176 | + color = cast(Collection[Color], color) |
| 177 | + return list(_gen_list_of_colors_from_iterable(color)) |
| 178 | + |
| 179 | + |
| 180 | +def _is_single_color(color: Union[Color, Collection[Color]]) -> bool: |
| 181 | + """Check if `color` is a single color, not a sequence of colors. |
| 182 | +
|
| 183 | + Single color is of these kinds: |
| 184 | + - Named color "red", "C0", "firebrick" |
| 185 | + - Alias "g" |
| 186 | + - Sequence of floats, such as (0.1, 0.2, 0.3) or (0.1, 0.2, 0.3, 0.4). |
| 187 | +
|
| 188 | + See Also |
| 189 | + -------- |
| 190 | + _is_single_string_color |
| 191 | + """ |
| 192 | + if isinstance(color, str) and _is_single_string_color(color): |
| 193 | + # GH #36972 |
| 194 | + return True |
| 195 | + |
| 196 | + if _is_floats_color(color): |
| 197 | + return True |
| 198 | + |
| 199 | + return False |
| 200 | + |
| 201 | + |
| 202 | +def _gen_list_of_colors_from_iterable(color: Collection[Color]) -> Iterator[Color]: |
| 203 | + """ |
| 204 | + Yield colors from string of several letters or from collection of colors. |
| 205 | + """ |
| 206 | + for x in color: |
| 207 | + if _is_single_color(x): |
| 208 | + yield x |
| 209 | + else: |
| 210 | + raise ValueError(f"Invalid color {x}") |
| 211 | + |
| 212 | + |
| 213 | +def _is_floats_color(color: Union[Color, Collection[Color]]) -> bool: |
| 214 | + """Check if color comprises a sequence of floats representing color.""" |
| 215 | + return bool( |
| 216 | + is_list_like(color) |
| 217 | + and (len(color) == 3 or len(color) == 4) |
| 218 | + and all(isinstance(x, (int, float)) for x in color) |
| 219 | + ) |
| 220 | + |
| 221 | + |
| 222 | +def _get_colors_from_color_type(color_type: str, num_colors: int) -> List[Color]: |
| 223 | + """Get colors from user input color type.""" |
| 224 | + if color_type == "default": |
| 225 | + return _get_default_colors(num_colors) |
| 226 | + elif color_type == "random": |
| 227 | + return _get_random_colors(num_colors) |
| 228 | + else: |
| 229 | + raise ValueError("color_type must be either 'default' or 'random'") |
| 230 | + |
| 231 | + |
| 232 | +def _get_default_colors(num_colors: int) -> List[Color]: |
| 233 | + """Get `num_colors` of default colors from matplotlib rc params.""" |
| 234 | + import matplotlib.pyplot as plt |
| 235 | + |
| 236 | + colors = [c["color"] for c in plt.rcParams["axes.prop_cycle"]] |
| 237 | + return colors[0:num_colors] |
| 238 | + |
| 239 | + |
| 240 | +def _get_random_colors(num_colors: int) -> List[Color]: |
| 241 | + """Get `num_colors` of random colors.""" |
| 242 | + return [_random_color(num) for num in range(num_colors)] |
| 243 | + |
| 244 | + |
| 245 | +def _random_color(column: int) -> List[float]: |
| 246 | + """Get a random color represented as a list of length 3""" |
| 247 | + # GH17525 use common._random_state to avoid resetting the seed |
| 248 | + rs = com.random_state(column) |
| 249 | + return rs.rand(3).tolist() |
| 250 | + |
| 251 | + |
| 252 | +def _is_single_string_color(color: Color) -> bool: |
| 253 | + """Check if `color` is a single string color. |
81 | 254 |
|
82 | | - Examples of single colors: |
| 255 | + Examples of single string colors: |
83 | 256 | - 'r' |
84 | 257 | - 'g' |
85 | 258 | - 'red' |
86 | 259 | - 'green' |
87 | 260 | - 'C3' |
| 261 | + - 'firebrick' |
88 | 262 |
|
89 | 263 | Parameters |
90 | 264 | ---------- |
91 | | - color : string |
92 | | - Color string. |
| 265 | + color : Color |
| 266 | + Color string or sequence of floats. |
93 | 267 |
|
94 | 268 | Returns |
95 | 269 | ------- |
96 | 270 | bool |
97 | | - True if ``color`` looks like a valid color. |
| 271 | + True if `color` looks like a valid color. |
98 | 272 | False otherwise. |
99 | 273 | """ |
100 | 274 | conv = matplotlib.colors.ColorConverter() |
|
0 commit comments