|
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