| from __future__ import annotations |
|
|
| from collections.abc import ( |
| Collection, |
| Iterator, |
| ) |
| import itertools |
| from typing import ( |
| TYPE_CHECKING, |
| cast, |
| ) |
| import warnings |
|
|
| import matplotlib as mpl |
| import matplotlib.colors |
| import numpy as np |
|
|
| from pandas._typing import MatplotlibColor as Color |
| from pandas.util._exceptions import find_stack_level |
|
|
| from pandas.core.dtypes.common import is_list_like |
|
|
| import pandas.core.common as com |
|
|
| if TYPE_CHECKING: |
| from matplotlib.colors import Colormap |
|
|
|
|
| def get_standard_colors( |
| num_colors: int, |
| colormap: Colormap | None = None, |
| color_type: str = "default", |
| color: dict[str, Color] | Color | Collection[Color] | None = 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 = _derive_colors( |
| color=color, |
| colormap=colormap, |
| color_type=color_type, |
| num_colors=num_colors, |
| ) |
|
|
| return list(_cycle_colors(colors, num_colors=num_colors)) |
|
|
|
|
| def _derive_colors( |
| *, |
| color: Color | Collection[Color] | None, |
| colormap: str | Colormap | None, |
| color_type: str, |
| num_colors: int, |
| ) -> list[Color]: |
| """ |
| 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: |
| if colormap is not None: |
| warnings.warn( |
| "'color' and 'colormap' cannot be used simultaneously. Using 'color'", |
| stacklevel=find_stack_level(), |
| ) |
| return _get_colors_from_color(color) |
| else: |
| return _get_colors_from_color_type(color_type, num_colors=num_colors) |
|
|
|
|
| def _cycle_colors(colors: list[Color], num_colors: int) -> Iterator[Color]: |
| """Cycle colors until achieving max of `num_colors` or length of `colors`. |
| |
| Extra colors will be ignored by matplotlib if there are more colors |
| than needed and nothing needs to be done here. |
| """ |
| max_colors = max(num_colors, len(colors)) |
| yield from itertools.islice(itertools.cycle(colors), max_colors) |
|
|
|
|
| def _get_colors_from_colormap( |
| colormap: str | Colormap, |
| num_colors: int, |
| ) -> list[Color]: |
| """Get colors from colormap.""" |
| cmap = _get_cmap_instance(colormap) |
| return [cmap(num) for num in np.linspace(0, 1, num=num_colors)] |
|
|
|
|
| def _get_cmap_instance(colormap: str | Colormap) -> Colormap: |
| """Get instance of matplotlib colormap.""" |
| if isinstance(colormap, str): |
| cmap = colormap |
| colormap = mpl.colormaps[colormap] |
| if colormap is None: |
| raise ValueError(f"Colormap {cmap} is not recognized") |
| return colormap |
|
|
|
|
| def _get_colors_from_color( |
| color: Color | Collection[Color], |
| ) -> list[Color]: |
| """Get colors from user input color.""" |
| if len(color) == 0: |
| raise ValueError(f"Invalid color argument: {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: 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", "C0", "firebrick" |
| - 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): |
| |
| 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. |
| """ |
| for x in color: |
| if _is_single_color(x): |
| yield x |
| else: |
| raise ValueError(f"Invalid color {x}") |
|
|
|
|
| def _is_floats_color(color: 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, (int, float)) for x in 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) |
| 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: int) -> list[Color]: |
| """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"]] |
| return colors[0:num_colors] |
|
|
|
|
| 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) -> list[float]: |
| """Get a random color represented as a list of length 3""" |
| |
| rs = com.random_state(column) |
| return rs.rand(3).tolist() |
|
|
|
|
| def _is_single_string_color(color: Color) -> bool: |
| """Check if `color` is a single string color. |
| |
| Examples of single string colors: |
| - 'r' |
| - 'g' |
| - 'red' |
| - 'green' |
| - 'C3' |
| - 'firebrick' |
| |
| Parameters |
| ---------- |
| color : Color |
| Color string or sequence of floats. |
| |
| Returns |
| ------- |
| bool |
| True if `color` looks like a valid color. |
| False otherwise. |
| """ |
| conv = matplotlib.colors.ColorConverter() |
| try: |
| |
| |
| conv.to_rgba(color) |
| except ValueError: |
| return False |
| else: |
| return True |
|
|