style.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. from __future__ import annotations
  2. import itertools
  3. from typing import (
  4. TYPE_CHECKING,
  5. Collection,
  6. Iterator,
  7. cast,
  8. )
  9. import warnings
  10. import matplotlib as mpl
  11. import matplotlib.colors
  12. import numpy as np
  13. from pandas._typing import MatplotlibColor as Color
  14. from pandas.util._exceptions import find_stack_level
  15. from pandas.core.dtypes.common import is_list_like
  16. import pandas.core.common as com
  17. if TYPE_CHECKING:
  18. from matplotlib.colors import Colormap
  19. def get_standard_colors(
  20. num_colors: int,
  21. colormap: Colormap | None = None,
  22. color_type: str = "default",
  23. color: dict[str, Color] | Color | Collection[Color] | None = None,
  24. ):
  25. """
  26. Get standard colors based on `colormap`, `color_type` or `color` inputs.
  27. Parameters
  28. ----------
  29. num_colors : int
  30. Minimum number of colors to be returned.
  31. Ignored if `color` is a dictionary.
  32. colormap : :py:class:`matplotlib.colors.Colormap`, optional
  33. Matplotlib colormap.
  34. When provided, the resulting colors will be derived from the colormap.
  35. color_type : {"default", "random"}, optional
  36. Type of colors to derive. Used if provided `color` and `colormap` are None.
  37. Ignored if either `color` or `colormap` are not None.
  38. color : dict or str or sequence, optional
  39. Color(s) to be used for deriving sequence of colors.
  40. Can be either be a dictionary, or a single color (single color string,
  41. or sequence of floats representing a single color),
  42. or a sequence of colors.
  43. Returns
  44. -------
  45. dict or list
  46. Standard colors. Can either be a mapping if `color` was a dictionary,
  47. or a list of colors with a length of `num_colors` or more.
  48. Warns
  49. -----
  50. UserWarning
  51. If both `colormap` and `color` are provided.
  52. Parameter `color` will override.
  53. """
  54. if isinstance(color, dict):
  55. return color
  56. colors = _derive_colors(
  57. color=color,
  58. colormap=colormap,
  59. color_type=color_type,
  60. num_colors=num_colors,
  61. )
  62. return list(_cycle_colors(colors, num_colors=num_colors))
  63. def _derive_colors(
  64. *,
  65. color: Color | Collection[Color] | None,
  66. colormap: str | Colormap | None,
  67. color_type: str,
  68. num_colors: int,
  69. ) -> list[Color]:
  70. """
  71. Derive colors from either `colormap`, `color_type` or `color` inputs.
  72. Get a list of colors either from `colormap`, or from `color`,
  73. or from `color_type` (if both `colormap` and `color` are None).
  74. Parameters
  75. ----------
  76. color : str or sequence, optional
  77. Color(s) to be used for deriving sequence of colors.
  78. Can be either be a single color (single color string, or sequence of floats
  79. representing a single color), or a sequence of colors.
  80. colormap : :py:class:`matplotlib.colors.Colormap`, optional
  81. Matplotlib colormap.
  82. When provided, the resulting colors will be derived from the colormap.
  83. color_type : {"default", "random"}, optional
  84. Type of colors to derive. Used if provided `color` and `colormap` are None.
  85. Ignored if either `color` or `colormap`` are not None.
  86. num_colors : int
  87. Number of colors to be extracted.
  88. Returns
  89. -------
  90. list
  91. List of colors extracted.
  92. Warns
  93. -----
  94. UserWarning
  95. If both `colormap` and `color` are provided.
  96. Parameter `color` will override.
  97. """
  98. if color is None and colormap is not None:
  99. return _get_colors_from_colormap(colormap, num_colors=num_colors)
  100. elif color is not None:
  101. if colormap is not None:
  102. warnings.warn(
  103. "'color' and 'colormap' cannot be used simultaneously. Using 'color'",
  104. stacklevel=find_stack_level(),
  105. )
  106. return _get_colors_from_color(color)
  107. else:
  108. return _get_colors_from_color_type(color_type, num_colors=num_colors)
  109. def _cycle_colors(colors: list[Color], num_colors: int) -> Iterator[Color]:
  110. """Cycle colors until achieving max of `num_colors` or length of `colors`.
  111. Extra colors will be ignored by matplotlib if there are more colors
  112. than needed and nothing needs to be done here.
  113. """
  114. max_colors = max(num_colors, len(colors))
  115. yield from itertools.islice(itertools.cycle(colors), max_colors)
  116. def _get_colors_from_colormap(
  117. colormap: str | Colormap,
  118. num_colors: int,
  119. ) -> list[Color]:
  120. """Get colors from colormap."""
  121. cmap = _get_cmap_instance(colormap)
  122. return [cmap(num) for num in np.linspace(0, 1, num=num_colors)]
  123. def _get_cmap_instance(colormap: str | Colormap) -> Colormap:
  124. """Get instance of matplotlib colormap."""
  125. if isinstance(colormap, str):
  126. cmap = colormap
  127. colormap = mpl.colormaps[colormap]
  128. if colormap is None:
  129. raise ValueError(f"Colormap {cmap} is not recognized")
  130. return colormap
  131. def _get_colors_from_color(
  132. color: Color | Collection[Color],
  133. ) -> list[Color]:
  134. """Get colors from user input color."""
  135. if len(color) == 0:
  136. raise ValueError(f"Invalid color argument: {color}")
  137. if _is_single_color(color):
  138. color = cast(Color, color)
  139. return [color]
  140. color = cast(Collection[Color], color)
  141. return list(_gen_list_of_colors_from_iterable(color))
  142. def _is_single_color(color: Color | Collection[Color]) -> bool:
  143. """Check if `color` is a single color, not a sequence of colors.
  144. Single color is of these kinds:
  145. - Named color "red", "C0", "firebrick"
  146. - Alias "g"
  147. - Sequence of floats, such as (0.1, 0.2, 0.3) or (0.1, 0.2, 0.3, 0.4).
  148. See Also
  149. --------
  150. _is_single_string_color
  151. """
  152. if isinstance(color, str) and _is_single_string_color(color):
  153. # GH #36972
  154. return True
  155. if _is_floats_color(color):
  156. return True
  157. return False
  158. def _gen_list_of_colors_from_iterable(color: Collection[Color]) -> Iterator[Color]:
  159. """
  160. Yield colors from string of several letters or from collection of colors.
  161. """
  162. for x in color:
  163. if _is_single_color(x):
  164. yield x
  165. else:
  166. raise ValueError(f"Invalid color {x}")
  167. def _is_floats_color(color: Color | Collection[Color]) -> bool:
  168. """Check if color comprises a sequence of floats representing color."""
  169. return bool(
  170. is_list_like(color)
  171. and (len(color) == 3 or len(color) == 4)
  172. and all(isinstance(x, (int, float)) for x in color)
  173. )
  174. def _get_colors_from_color_type(color_type: str, num_colors: int) -> list[Color]:
  175. """Get colors from user input color type."""
  176. if color_type == "default":
  177. return _get_default_colors(num_colors)
  178. elif color_type == "random":
  179. return _get_random_colors(num_colors)
  180. else:
  181. raise ValueError("color_type must be either 'default' or 'random'")
  182. def _get_default_colors(num_colors: int) -> list[Color]:
  183. """Get `num_colors` of default colors from matplotlib rc params."""
  184. import matplotlib.pyplot as plt
  185. colors = [c["color"] for c in plt.rcParams["axes.prop_cycle"]]
  186. return colors[0:num_colors]
  187. def _get_random_colors(num_colors: int) -> list[Color]:
  188. """Get `num_colors` of random colors."""
  189. return [_random_color(num) for num in range(num_colors)]
  190. def _random_color(column: int) -> list[float]:
  191. """Get a random color represented as a list of length 3"""
  192. # GH17525 use common._random_state to avoid resetting the seed
  193. rs = com.random_state(column)
  194. return rs.rand(3).tolist()
  195. def _is_single_string_color(color: Color) -> bool:
  196. """Check if `color` is a single string color.
  197. Examples of single string colors:
  198. - 'r'
  199. - 'g'
  200. - 'red'
  201. - 'green'
  202. - 'C3'
  203. - 'firebrick'
  204. Parameters
  205. ----------
  206. color : Color
  207. Color string or sequence of floats.
  208. Returns
  209. -------
  210. bool
  211. True if `color` looks like a valid color.
  212. False otherwise.
  213. """
  214. conv = matplotlib.colors.ColorConverter()
  215. try:
  216. conv.to_rgba(color)
  217. except ValueError:
  218. return False
  219. else:
  220. return True