Source code for excolor.colortools

#!/usr/bin/env python
# -*- coding: utf8 -*-

"""
This module contains functions to manipulate colors.
"""

import os
import colorsys
import numpy as np
import matplotlib.pyplot as plt
from cycler import cycler
import matplotlib.colors as mc
from matplotlib import colormaps
from matplotlib.colors import ListedColormap, LinearSegmentedColormap, Colormap
from matplotlib.patches import Rectangle
from matplotlib.axes import Axes
from .utils import _aspect_ratio, _is_cmap
from .palette import generate_stepwise_palette
from .colortypes import _is_arraylike, _get_color_type, _is_color, _is_rgb
from .colortypes import _to_formatted_rgb, _to_formatted_hls, _to_formatted_hsl, _to_formatted_hsv, _to_formatted_oklch
from .colortypes import to_hex, to_rgb, to_rgb255, to_hsv, to_hls, to_oklch, rgb_to_rgb255
from .utils import get_colors, _is_qualitative
from typing import Union, Optional, Tuple, List, Any

import warnings
warnings.filterwarnings("ignore")


[docs] def show_colors( c: Optional[Union[Colormap, str, List[str], Tuple[float, ...], List[Tuple[float, ...]]]] = None, names: Optional[List[str]] = None, title: str = "", size: Optional[Tuple[int, int]] = None, fmt: str = 'hex', verbose: bool = True, ax: Optional[plt.Axes] = None ) -> None: """ Displays a set of colors as a grid layout with color names. This function creates a visualization of colors, either from a list, a single color, or a colormap. The colors are displayed in a grid with their hex values, and the text color is automatically chosen for readability based on the background color. Parameters ---------- c : list of str or tuple, str or tuple, or matplotlib.colors.Colormap, optional Input colors to display. Can be: - A colormap name or instance - A single color str or rgb tuple - A list of colors str or rgb tuples If None, the matplotlib default color palettes will be shown. names : list of str, optional List of color names. If not provided, the hex values will be used. title : str, default='' Title to display above the color grid size : tuple of int, optional Size of the color grid (width, height) fmt: str, default='hex' Output format of color names ('hex', 'rgb', 'hsv', 'hsl') verbose : bool, default=True If True, prints the list of colors to the console ax : matplotlib.axes.Axes, optional Axes to plot on. If None, a new figure is created. Returns ------- None Displays the color visualization using matplotlib Examples -------- >>> show_colors(['#FF0000', '#00FF00', '#0000FF']) # Display RGB colors >>> show_colors('viridis') # Display viridis colormap colors >>> show_colors('viridis', size=(10, 5)) # Custom size >>> show_colors(None) # Display matplotlib default color palettes """ def _to_255(x): x255 = tuple([int(np.round(x_ * 255)) for x_ in x]) return x255 def _to_name(x: Tuple[float, ...]) -> str: name = [f'{int(np.round(x_ * 255)):3d}' for x_ in x] name = ' ' + ' '.join(name) return name # If None or empty list, show default colors if c is None: list_colors() return if _is_arraylike(c) and len(c) == 0: list_colors() return # Type-safe color extraction colors: List[Union[str, Tuple[float, ...]]] colormap_title = title # If c is a colormap, extract colors if _is_cmap(c): colors = get_colors(c, exclude_extreme=False) if not title: colormap_title = c if isinstance(c, str) else c.name # If color or list of colors, convert to list elif _is_arraylike(c) and not _is_rgb(c): colors = c elif _is_color(c): colors = [c] else: raise ValueError("Input must be a colormap, a color name, or a list of colors.") # Convert colors to RGB tuples rgbcolors = [to_rgb(color) for color in colors] # Format color names if fmt == 'hex': colors = [to_hex(color) for color in colors] cnames = colors elif fmt == 'rgb': colors = rgbcolors cnames = [_to_name(color) for color in colors if isinstance(color, tuple)] elif fmt == 'hsv': colors = [colorsys.rgb_to_hsv(*color) for color in rgbcolors] cnames = [_to_name(color) for color in rgbcolors if isinstance(color, tuple)] elif fmt == 'hsl': colors = [colorsys.rgb_to_hls(*color) for color in rgbcolors] cnames = [_to_name(color) for color in rgbcolors if isinstance(color, tuple)] if verbose: if fmt == 'hex': print(colors) else: print([_to_255(color) for color in rgbcolors]) d = 0.05 width = 1 - 2 * d if size is None: n, m = _aspect_ratio(len(rgbcolors), lmin=12) size = (2*n+4,2*m) else: n, m = size m = int(np.ceil(len(rgbcolors) / n)) size = (2*n+4,2*m) fontsize = 12 if size[1] < 2 else 28 # Display colors new_figure = ax is None if new_figure: plt.figure(figsize=size, facecolor="#00000000") else: ax.set_facecolor("#00000000") plt.title(colormap_title, fontsize=fontsize, color="grey") for k, rgb in enumerate(rgbcolors): i = k % n j = k // n if rgb is not None: r = Rectangle((i, -j), width, -width, facecolor=rgb, fill=True) plt.gca().add_patch(r) h, s, v = to_hsv(rgb) h, l, s = to_hls(rgb) fontcolor = "white" if v < 0.4 or l < 0.3 else "black" x, y = i + 0.55 - 2 * d, -j - 0.5 name = names[k] if names is not None and k < len(names) else (cnames[k] if k < len(cnames) else "") if name is None: name = "" plt.text(x, y, str(name), fontsize=20, color=fontcolor, ha="center", va="center") plt.xlim(-d, n - d) plt.ylim(-m + d, d) plt.gca().set_axis_off() plt.tight_layout() if new_figure: plt.show() plt.close() return
[docs] def list_colors() -> None: """ Displays a list of default colors as a grid layout with their names. This function creates a visualization of colors, either from a dictionary, or from the default color palettes. The colors are displayed in a grid with their hex values, and the text color is automatically chosen for readability based on the background color. Returns ------- None Displays the color visualization using matplotlib """ groups = { 'Reds': [0.000, 0.050], 'Oranges': [0.050, 0.110], 'Yellows': [0.110, 0.150], 'Yellow-Greens': [0.150, 0.220], 'Greens': [0.220, 0.440], 'Cyans': [0.440, 0.530], 'Blues': [0.530, 0.730], 'Violets': [0.730, 0.880], 'Purples': [0.800, 1.100], } dcts = { 'Base Colors': mc.BASE_COLORS, 'Tableau Palette': mc.TABLEAU_COLORS, 'CSS Colors': mc.CSS4_COLORS, 'XKCD Colors': mc.XKCD_COLORS, } size = (10,1) # width, height for title, dct in dcts.items(): cname = np.array(list(dct.keys())) color = np.array(list(dct.values())) hue, lightness, saturation = np.array([to_hls(v) for v in color]).T # Show small palettes if len(color) < 20: subtitle = f'{title}' value = hue + (saturation > 0).astype(float) idx = np.argsort(value) cnames = cname[idx] colors = color[idx] hexnames = [to_hex(c) for c in colors] cnames = [f'{cnames[i]}\n{hexnames[i]}' for i in range(len(colors))] show_colors(colors, names=cnames, size=size, title=subtitle) # Show large palettes by hue groups else: # Show grays subtitle = f'{title} (Grays)' mask = saturation == 0 cnames = cname[mask] colors = color[mask] lights = lightness[mask] idx = np.argsort(lights) cnames = cnames[idx] colors = colors[idx] if title.startswith('CSS'): cnames = [f'{cnames[i]}\n{colors[i]}' for i in range(len(colors))] else: cnames = [f'xkcd:\n{cnames[i][5:]}\n{colors[i]}' for i in range(len(colors))] show_colors(colors, names=cnames, size=size, title=subtitle) # Show color groups by hue for group, bins in groups.items(): subtitle = f'{title} ({group})' hue0, hue1 = bins mask = (hue >= hue0) & (hue < hue1) & (saturation > 0) cnames = cname[mask] colors = color[mask] lights = lightness[mask] idx = np.argsort(lights) cnames = cnames[idx] colors = colors[idx] if title.startswith('CSS'): cnames = [f'{cnames[i]}\n{colors[i]}' for i in range(len(colors))] else: cnames = [f'xkcd:\n{cnames[i][5:]}\n{colors[i]}' for i in range(len(colors))] show_colors(colors, names=cnames, size=size, title=subtitle) return
[docs] def set_color_cycler(c: Union[List[str], str, Colormap], n: int = 6, globally: bool = False) -> None: """ Sets the color cycler for matplotlib. This function configures the color cycling behavior for the current matplotlib axis using either a list of colors or a colormap. The colors will be used in sequence when plotting multiple lines or other elements. Parameters ---------- c : list of str, str, or matplotlib.colors.Colormap Input colors to use in the cycler. Can be: - A list of color strings or rgb tuples - A colormap name or instance n : int, default=6 Number of colors to extract from the colormap if a colormap is provided. If a list of colors is provided, this parameter is ignored. globally : bool, default=False If True, set the color cycler for all matplotlib axes in current python session. To reset to defaults use: plt.rcdefaults() Returns ------- None This function does not return anything; it modifies the matplotlib state. Examples -------- >>> set_color_cycler(['red', 'green', 'blue']) # Use specific colors >>> set_color_cycler('viridis', n=5) # Use 5 colors from viridis >>> plt.plot(x1, y1) # Will use first color >>> plt.plot(x2, y2) # Will use second color """ try: if isinstance(c, str): c = colormaps[c] if isinstance(c, Colormap): colors = generate_stepwise_palette(c, n, use_hue=False) m = len(colors) // 2 colors1, colors2 = colors[:m], colors[m:] colors = [] while colors1 or colors2: if colors1: colors.append(colors1.pop(0)) if colors2: colors.append(colors2.pop(0)) else: colors = c # Set color cycler globally or locally if globally: plt.rcParams['axes.prop_cycle'] = cycler(color=colors) else: plt.gca().set_prop_cycle(cycler(color=colors)) except: raise ValueError("Invalid color input. Must be a list of colors, a colormap name, or a Colormap instance.") return
[docs] def lighten( c: Union[Colormap, str, List[str], Tuple[float, ...], List[Tuple[float, ...]]], factor: float = 0.1, keep_alpha: bool = False, mode: str = 'hls' ) -> Union[str, List[str], Tuple[float, ...], List[Tuple[float, ...]], Colormap, ListedColormap, LinearSegmentedColormap, None]: """ Lightens color(s) or a colormap by increasing lightness. This function takes colors or a colormap and returns lighter versions by increasing their lightness in HLS color space. Parameters ---------- c : list of str or tuple, str or tuple, or matplotlib.colors.Colormap Input colors to display. Can be: - A colormap name or instance - A single color str or rgb tuple - A list of colors str or rgb tuples factor : float, default=0.2 Increment in lightness between 0 and 1: keep_alpha : bool, default=False If True, keep the alpha channel mode : str, default='hls' If 'hls' or 'hsl', use HLS color space to lighten the colors If 'hsv', use HSV color space to lighten the colors Returns ------- list of str or tuple, str or tuple, or matplotlib.colors.Colormap Lightened version of the input color or colors. Returns same type as input. Examples -------- >>> lighten('#FF0000', factor=0.5) # Lighten red >>> lighten(['#FF0000', '#00FF00'], factor=0.3) # Lighten multiple colors >>> lighten('viridis', factor=0.3) # Lighten entire colormap """ if mode not in ['hls', 'hsl', 'hsv']: raise ValueError("mode must be 'hls' or 'hsv'") factor = np.clip(factor, 0, 1) try: colors = None category = None if _is_cmap(c): if isinstance(c, str): c = colormaps[c] colors = get_colors(c, exclude_extreme=False) if not _is_qualitative(c): colors = get_colors(c, 256, exclude_extreme=False) category = "cmap" elif _is_color(c): colors = [c] category = _get_color_type(c) elif _is_arraylike(c): colors = c category = _get_color_type(c[0]) except: colors = None category = None if colors is None: return None if len(colors) > 1: colors = [lighten(color, factor, keep_alpha, mode) for color in colors] if category == 'cmap': name = c.name + "_light" if _is_qualitative(c): colors = ListedColormap(colors, name=name) else: colors = LinearSegmentedColormap.from_list(name, colors) else: try: rgb = to_rgb(colors[0], keep_alpha=True) alpha = rgb[3] if len(rgb) == 4 else None if mode == 'hsv': hsv = np.array(to_hsv(rgb)) hsv[2] = np.clip(hsv[2] + factor, 0, 1) rgb = mc.hsv_to_rgb(hsv[:3]) elif mode in ['hls', 'hsl']: hls = np.array(to_hls(rgb)) hls[1] = np.clip(hls[1] + factor, 0, 1) rgb = colorsys.hls_to_rgb(*hls[:3]) rgb = rgb + (alpha,) if alpha is not None else rgb rgb = tuple([float(np.round(x, 4)) for x in rgb]) # Format output colors = _format_output_color(rgb, category) except: colors = None return colors
[docs] def darken( c: Union[Colormap, str, List[str], Tuple[float, ...], List[Tuple[float, ...]]], factor: float = 0.1, keep_alpha: bool = False, mode: str = 'hls' ) -> Union[str, List[str], Tuple[float, ...], List[Tuple[float, ...]], Colormap, ListedColormap, LinearSegmentedColormap, None]: """ Darkens color(s) or a colormap by decreasing lightness. This function takes colors or a colormap and returns darker versions by decreasing their lightness in HLS color space. Parameters ---------- c : list of str or tuple, str or tuple, or matplotlib.colors.Colormap Input colors to display. Can be: - A colormap name or instance - A single color str or rgb tuple - A list of colors str or rgb tuples factor : float, default=0.2 Decrement in lightness between 0 and 1: keep_alpha : bool, default=False If True, keep the alpha channel mode : str, default='hls' If 'hls' or 'hsl', use HLS color space to darken the colors If 'hsv', use HSV color space to darken the colors Returns ------- list of str or tuple, str or tuple, or matplotlib.colors.Colormap Darkened version of the input color or colors. Returns same type as input. Examples -------- >>> darken('#FF0000', factor=0.5) # Darken red >>> darken(['#FF0000', '#00FF00'], factor=0.3) # Darken multiple colors >>> darken('viridis', factor=0.3) # Darken entire colormap """ if mode not in ['hls', 'hsl', 'hsv']: raise ValueError("mode must be 'hls' or 'hsv'") factor = np.clip(factor, 0, 1) try: colors = None category = None if _is_cmap(c): if isinstance(c, str): c = colormaps[c] colors = get_colors(c, exclude_extreme=False) if not _is_qualitative(c): colors = get_colors(c, 256, exclude_extreme=False) category = "cmap" elif _is_color(c): colors = [c] category = _get_color_type(c) elif _is_arraylike(c): colors = c category = _get_color_type(c[0]) except: colors = None category = None if colors is None: return None if len(colors) > 1: colors = [darken(color, factor, keep_alpha, mode) for color in colors] if category == 'cmap': name = c.name + "_dark" if _is_qualitative(c): colors = ListedColormap(colors, name=name) else: colors = LinearSegmentedColormap.from_list(name, colors) else: try: rgb = to_rgb(colors[0], keep_alpha=True) alpha = rgb[3] if len(rgb) == 4 else None if mode == 'hsv': hsv = np.array(to_hsv(rgb)) hsv[2] = np.clip(hsv[2] - factor, 0, 1) rgb = mc.hsv_to_rgb(hsv[:3]) elif mode in ['hls', 'hsl']: hls = np.array(to_hls(rgb)) hls[1] = np.clip(hls[1] - factor, 0, 1) rgb = colorsys.hls_to_rgb(*hls[:3]) rgb = rgb + (alpha,) if alpha is not None else rgb rgb = tuple([float(np.round(x, 4)) for x in rgb]) # Format output colors = _format_output_color(rgb, category) except: colors = None return colors
[docs] def saturate( c: Union[Colormap, str, List[str], Tuple[float, ...], List[Tuple[float, ...]]], factor: float = 0.1, keep_alpha: bool = False, mode: str = 'hls' ) -> Union[str, List[str], Tuple[float, ...], List[Tuple[float, ...]], Colormap, ListedColormap, LinearSegmentedColormap, None]: """ Saturates color(s) or a colormap by increasing saturation. This function takes colors or a colormap and returns more saturated versions by increasing their saturation in HLS color space. Parameters ---------- c : list of str or tuple, str or tuple, or matplotlib.colors.Colormap Input colors to display. Can be: - A colormap name or instance - A single color str or rgb tuple - A list of colors str or rgb tuples factor : float, default=0.2 Increment in saturation between 0 and 1: keep_alpha : bool, default=False If True, keep the alpha channel mode : str, default='hls' If 'hls' or 'hsl', use HLS color space to saturate the colors If 'hsv', use HSV color space to saturate the colors Returns ------- list of str or tuple, str or tuple, or matplotlib.colors.Colormap Saturated version of the input color or colors. Returns same type as input. Examples -------- >>> saturate('#FF0000', factor=0.5) # Saturate red >>> saturate(['#FF0000', '#00FF00'], factor=0.3) # Saturate multiple colors >>> saturate('viridis', factor=0.3) # Saturate entire colormap """ if mode not in ['hls', 'hsl', 'hsv']: raise ValueError("mode must be 'hls' or 'hsv'") factor = np.clip(factor, 0, 1) try: colors = None category = None if _is_cmap(c): if isinstance(c, str): c = colormaps[c] colors = get_colors(c, exclude_extreme=False) if not _is_qualitative(c): colors = get_colors(c, 256, exclude_extreme=False) category = "cmap" elif _is_color(c): colors = [c] category = _get_color_type(c) elif _is_arraylike(c): colors = c category = _get_color_type(c[0]) except: colors = None category = None if colors is None: return None if len(colors) > 1: colors = [saturate(color, factor, keep_alpha, mode) for color in colors] if category == 'cmap': name = c.name + "_saturated" if _is_qualitative(c): colors = ListedColormap(colors, name=name) else: colors = LinearSegmentedColormap.from_list(name, colors) else: try: rgb = to_rgb(colors[0], keep_alpha=True) alpha = rgb[3] if len(rgb) == 4 else None if mode == 'hsv': hsv = np.array(to_hsv(rgb)) hsv[1] = np.clip(hsv[1] + factor, 0, 1) rgb = mc.hsv_to_rgb(hsv[:3]) elif mode in ['hls', 'hsl']: hls = np.array(to_hls(rgb)) hls[2] = np.clip(hls[2] + factor, 0, 1) rgb = colorsys.hls_to_rgb(*hls[:3]) rgb = rgb + (alpha,) if alpha is not None else rgb rgb = tuple([float(np.round(x, 4)) for x in rgb]) # Format output colors = _format_output_color(rgb, category) except: colors = None return colors
[docs] def desaturate( c: Union[Colormap, str, List[str], Tuple[float, ...], List[Tuple[float, ...]]], factor: float = 0.1, keep_alpha: bool = False, mode: str = 'hls' ) -> Union[str, List[str], Tuple[float, ...], List[Tuple[float, ...]], Colormap, ListedColormap, LinearSegmentedColormap, None]: """ Desaturates color(s) or a colormap by decreasing saturation. This function takes colors or a colormap and returns less saturated versions by decreasing their saturation in HLS color space. Parameters ---------- c : list of str or tuple, str or tuple, or matplotlib.colors.Colormap Input colors to display. Can be: - A colormap name or instance - A single color str or rgb tuple - A list of colors str or rgb tuples factor : float, default=0.2 Decrement in saturation between 0 and 1: keep_alpha : bool, default=False If True, keep the alpha channel mode : str, default='hls' If 'hls' or 'hsl', use HLS color space to desaturate the colors If 'hsv', use HSV color space to desaturate the colors Returns ------- list of str or tuple, str or tuple, or matplotlib.colors.Colormap Desaturated version of the input color or colors. Returns same type as input. Examples -------- >>> desaturate('#FF0000', factor=0.5) # Desaturate red >>> desaturate(['#FF0000', '#00FF00'], factor=0.3) # Desaturate multiple colors >>> desaturate('viridis', factor=0.3) # Desaturate entire colormap """ if mode not in ['hls', 'hsl', 'hsv']: raise ValueError("mode must be 'hls' or 'hsv'") factor = np.clip(factor, 0, 1) try: colors = None category = None if _is_cmap(c): if isinstance(c, str): c = colormaps[c] colors = get_colors(c, exclude_extreme=False) if not _is_qualitative(c): colors = get_colors(c, 256, exclude_extreme=False) category = "cmap" elif _is_color(c): colors = [c] category = _get_color_type(c) elif _is_arraylike(c): colors = c category = _get_color_type(c[0]) except: colors = None category = None if colors is None: return None if len(colors) > 1: colors = [desaturate(color, factor, keep_alpha, mode) for color in colors] if category == 'cmap': name = c.name + "_desaturated" if _is_qualitative(c): colors = ListedColormap(colors, name=name) else: colors = LinearSegmentedColormap.from_list(name, colors) else: try: rgb = to_rgb(colors[0], keep_alpha=True) alpha = rgb[3] if len(rgb) == 4 else None if mode == 'hsv': hsv = np.array(to_hsv(rgb)) hsv[1] = np.clip(hsv[1] - factor, 0, 1) rgb = mc.hsv_to_rgb(hsv[:3]) elif mode in ['hls', 'hsl']: hls = np.array(to_hls(rgb)) hls[2] = np.clip(hls[2] - factor, 0, 1) rgb = colorsys.hls_to_rgb(*hls[:3]) rgb = rgb + (alpha,) if alpha is not None else rgb rgb = tuple([float(np.round(x, 4)) for x in rgb]) # Format output colors = _format_output_color(rgb, category) except: colors = None return colors
def _format_output_color(rgb, category): """ Formats output color based on category. Parameters ---------- rgb : tuple or list An RGB or RGBA color tuple with values in the range [0, 1]. Returns ------- str A CSS-like formatted string, e.g., "rgb(255, 0, 0)" or "rgba(255, 0, 0, 1.0)". """ output = None if category in ['hex', 'hexa', 'name']: output = to_hex(rgb, keep_alpha=True) elif category in ['rgb|hls|hsl|hsv|oklch', 'rgba']: output = rgb elif category in ['rgb255', 'rgba255']: output = rgb_to_rgb255(rgb, keep_alpha=True) elif category in ['rgb255 formatted', 'rgba255 formatted']: output = _to_formatted_rgb(rgb) elif category == 'hls formatted': output = _to_formatted_hls(rgb) elif category == 'hsl formatted': output = _to_formatted_hsl(rgb) elif category == 'hsv formatted': output = _to_formatted_hsv(rgb) elif category == 'oklch formatted': output = _to_formatted_oklch(rgb) return output import types __all__ = [name for name, thing in globals().items() if not (name.startswith("_") or isinstance(thing, types.ModuleType))] del types