Source code for excolor.cmaptools

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

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

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mc
from matplotlib.colors import ListedColormap, LinearSegmentedColormap, Colormap
from matplotlib.axes import Axes
from .colortypes import to_hex, to_rgb
from .colortools import show_colors, darken
from .utils import get_colors, _is_qualitative, _is_divergent, _is_cyclic
from typing import Union, Tuple, List, Callable, Optional

import warnings
warnings.filterwarnings("ignore")


def _get_cmap_list() -> List[str]:
    """
    Gets a list of all registered colormaps in matplotlib.

    Returns
    -------
    cmaps : list of str
        List of colormap names
    """
    cmaps: List[str] = []
    cmaps_r = [cmap for cmap in plt.colormaps() if cmap.endswith('_r')]
    for cmap in plt.colormaps():
        if not cmap in cmaps_r:
            cmaps.append(cmap)
            cmap_r = f'{cmap}_r'
            if cmap_r in cmaps_r:
                cmaps.append(cmap_r)
    return cmaps


def _get_cmap_categories(cmap: Union[str, Colormap]) -> List[str]:
    """
    Gets the kind of a colormap.

    This function determines the kind of a colormap based on its properties.

    Parameters
    ----------
    cmap : str or matplotlib.colors.Colormap
        Colormap name or instance to determine the kind of

    Returns
    -------
    kind : list of str
        List of kinds of the colormap
    """
    cmap = plt.get_cmap(cmap)
    categories: List[str] = []
    if _is_qualitative(cmap):
        categories.append('Qualitative')
    else:
        categories.append('Continuous')
    if _is_divergent(cmap):
        categories.append('Divergent')
    if _is_cyclic(cmap):
        categories.append('Cyclic')
    return categories


def _register_cmap(cmap: Colormap) -> None:
    """
    Registers a matplotlib colormap in the current session.

    This function attempts to register a colormap with matplotlib's colormap registry.
    If the colormap is already registered or if registration fails for any reason,
    the function will silently continue.

    Parameters
    ----------
    cmap : matplotlib.colors.Colormap
        Colormap to register

    Returns
    -------
    None

    Examples
    --------
    >>> custom_cmap = LinearSegmentedColormap.from_list('custom', ['red', 'white', 'blue'])
    >>> _register_cmap(custom_cmap)
    >>> plt.get_cmap('custom')  # Now available
    """
    try:
        plt.colormaps.register(cmap)
    except:
        pass


def _add_extended_colormaps() -> None:
    """
    Extends the list of registered colormaps.

    This function adds a variety of custom colormaps to matplotlib's registry,
    including:
    - Sequential versions (warm and cold)
    - Color combinations (BrBu, BrGn, OrBu, OrGn, PiBu)
    - Gruvbox themes (light, normal, dark)
    - Art deco and cyberpunk inspired colormaps
    - Aquamarine variations (light, normal, dark)

    Each colormap is registered in both its original and reversed form.

    Returns
    -------
    None

    Examples
    --------
    >>> _add_extended_colormaps()
    >>> plt.get_cmap('gruvbox')  # Now available
    >>> plt.get_cmap('cyberpunk')  # Now available
    """
    # Add continuous colormaps
    color_dict = {
        "warm": plt.get_cmap("coolwarm")(np.linspace(0.5, 1, 128)),
        "cold": plt.get_cmap("coolwarm_r")(np.linspace(0.5, 1, 128)),
    }
    for name, colors in color_dict.items():
        try:    
            cmap = LinearSegmentedColormap.from_list(name, colors)
            _register_cmap(cmap)
            cmap = LinearSegmentedColormap.from_list(name + "_r", colors[::-1])
            _register_cmap(cmap)
        except:
            pass
    # Add qualitative colormaps
    color_dict = {
        "BrBu": ["#9B2227", "#BA3F04", "#CA6705", "#EE9B04", "#EAD7A4", "#93D3BD", "#4BB3A9", "#039396", "#027984", "#015F72"],
        "BrGn": ["#9B2227", "#BA3F04", "#CA6705", "#EE9B04", "#EAD7A4", "#CAB67B", "#A99945", "#897C0F", "#736E12", "#5E6014"],
        "OrBu": ["#B97401", "#DC8D01", "#FAC316", "#F8E584", "#F8FFC9", "#638094", "#4C6E83", "#3E596D", "#374053"],
        "OrGn": ["#B97401", "#DC8D01", "#FAC316", "#F8E584", "#F1FFC1", "#7DA4A4", "#628E8E", "#467171", "#284C4C"],
        "PiBu": ["#7D433D", "#A45040", "#C45D47", "#DD6850", "#CAA59F", "#4C8A9A", "#427283", "#385B6C", "#2E4355"],
        "rtd": ["#206390", "#2980BA", "#419AD5", "#6BB0DE", "#94C6E8", "#FFFFFF", "#EEFFCC", "#DDFF99", "#CCFF66", "#BBFF33"],
        "artdeco": ["#9F1B10", "#D88533", "#E8B055", "#D9B97B", "#B6BEAA", "#768C86", "#365861", "#204755", "#0A3649"],
        "cyberpunk": ["#55D6F5", "#5C9BE8", "#6260DC", "#522FAA", "#42007A", "#4F057A", "#5D097C", "#A917BE", "#F225FF"],
        "synthwave": ["#5C9BE8", "#5368C4", "#4B35A0", "#42007A", "#550584", "#680B8E", "#7B1098", "#A75466", "#D19536", "#FBD606"],
        "gruvbox_light": ["#E34039", "#F17626", "#F8B533", "#C4C221", "#87B189", "#57A6A9", "#C284A0"],
        "gruvbox": ["#CC241D", "#D65D0E", "#D79921", "#98971A", "#689D6A", "#458588", "#B16286"],
        "cobalt": ["#FB94FF", "#FA841A", "#FFC619", "#3AD900", "#0088FF"],
        "noctis": ["#9F1C17", "#E66533", "#FFC180", "#49E9A6", "#14A5FF"],
        "monokai": ["#F92672", "#FD971D", "#E69F66", "#E6DB74", "#A6E22E", "#66D9EF", "#AE81FF"],
        "oceanic": ["#7239D2", "#5F52CC", "#4C6CC5", "#3985BF", "#269FB9", "#13B9B2", "#00D2AC"],
    }
    for name, colors in color_dict.items():
        try:
            cmap = ListedColormap(colors, name=name)
            _register_cmap(cmap)
            cmap = ListedColormap(colors[::-1], name=name + "_r")
            _register_cmap(cmap)
        except:
           pass
    return


[docs] def show_colorbar(cmap: Union[str, Colormap], ax: Optional[plt.Axes] = None) -> None: """ Displays a colormap as a colorbar. This function creates a visualization of a colormap as a horizontal colorbar with labeled ticks. The colorbar shows the full range of the colormap from 0 to 1, with major ticks at 0, 0.5, and 1. Parameters ---------- cmap : str or matplotlib.colors.Colormap Colormap name or instance to display ax : matplotlib.axes.Axes, optional Axes to plot on. If None, a new figure is created. Returns ------- None Displays the colorbar visualization using matplotlib. Examples -------- >>> show_cbar('viridis') # Display viridis colormap as colorbar >>> show_cbar(plt.cm.viridis) # Same as above, using colormap instance """ cmap = plt.get_cmap(cmap) gradient = np.linspace(0, 1, 256) gradient = np.vstack((gradient, gradient)) new_figure = ax is None if new_figure: plt.figure(figsize=(12,2), facecolor="#00000000") else: ax.set_facecolor("#00000000") plt.title(f'{cmap.name} colorbar', fontsize=20, color="grey", pad=16) plt.imshow(gradient, aspect="auto", cmap=cmap) plt.xticks([0, 127, 255], ["0", "0.5", "1"], fontsize=16, color="grey") plt.yticks([]) for e in ["top", "bottom", "right", "left"]: plt.gca().spines[e].set_color("#00000000") plt.tight_layout() if new_figure: plt.show() plt.close() return
[docs] def show_cmap(cmap: Union[str, Colormap], verbose: bool = True) -> None: """ Displays a colormap's colorbar, sample colors, and background color. This function displays a colormap's colorbar, sample colors, and background color. If verbose is True, it also prints information about the colormap's properties. Parameters ---------- cmap : str or matplotlib.colors.Colormap Colormap name or instance to display verbose : bool, default=True If True, print information about the colormap. Returns ------- None Displays the colormap and its properties using matplotlib. Examples -------- >>> show_cmap('viridis') # Display with default sampling >>> gradient = np.logspace(0, -2, 10) # Custom logarithmic sampling >>> show_cmap('viridis', gradient) """ cmap = plt.get_cmap(cmap) if verbose: print(cmap.name) print() categories = _get_cmap_categories(cmap) for category in ['Qualitative', 'Continuous', 'Divergent', 'Cyclic']: if category in categories: print(f'{category}: True') else: print(f'{category}: False') show_colorbar(cmap) colors = get_colors(cmap, exclude_extreme=False) colors = [to_hex(c) for c in colors] print(f'{len(colors)} sample colors:') show_colors(colors, title=f'{cmap.name} sample colors') # type: ignore plt.show() plt.close() if verbose: bg_color = get_bgcolor(cmap) print(f'Background color: {bg_color}') show_colors([bg_color], title=f'{cmap.name} background', size=(2, 1)) plt.show() plt.close() return
[docs] def list_cmaps(category: str = 'All', display: bool = False) -> None: """ Lists and shows all registered colormaps. This function prints a list of all available colormaps in matplotlib, along with their properties (e.g., Qualitative, Continuous, Divergent, Cyclic). If a category other than 'All' is provided, the colormaps are displayed as a grid. Parameters ---------- category : str, default='All' 'All' : list all colormaps 'Qualitative' : list only qualitative colormaps 'Continuous' : list only continuous colormaps 'Divergent' : list only divergent colormaps 'Cyclic' : list only cyclic colormaps display : bool, default=False If True, display the colormaps as a grid. Returns ------- None Examples -------- >>> list_cmaps() # List all colormaps >>> list_cmaps("Qualitative") # List and display each colormap """ # Get sorted list of colormaps cmaps = _get_cmap_list() # Output cmap info for each colormap for cmap in cmaps: categories = _get_cmap_categories(cmap) if category != 'All': if category not in categories: continue categories = [f'{k:10}' for k in categories] prtstr = f'{cmap:20}' + (' ').join(categories) print(prtstr) if display: show_colorbar(cmap) return
[docs] def list_qualitative_cmaps(display: bool = False) -> None: """ Displays all qualitative colormaps in a grid layout. This function creates a grid of all qualitative colormaps available in matplotlib. The grid is displayed using matplotlib. Parameters ---------- display : bool, default=False If True, display the colormaps as a grid. Returns ------- None Displays the grid of qualitative colormaps using matplotlib Examples -------- >>> show_qualitative_cmaps() # Display all qualitative colormaps """ list_cmaps(category='Qualitative', display=display) return
[docs] def list_continuous_cmaps(display: bool = False) -> None: """ Displays all continuous colormaps in a grid layout. This function creates a grid of all continuous colormaps available in matplotlib. The grid is displayed using matplotlib. Parameters ---------- display : bool, default=False If True, display the colormaps as a grid. Returns ------- None Displays the grid of continuous colormaps using matplotlib Examples -------- >>> show_continuous_cmaps() # Display all continuous colormaps """ list_cmaps(category='Continuous', display=display) return
[docs] def list_divergent_cmaps(display: bool = False) -> None: """ Displays all divergent colormaps in a grid layout. This function creates a grid of all divergent colormaps available in matplotlib. The grid is displayed using matplotlib. Parameters ---------- display : bool, default=False If True, display the colormaps as a grid. Returns ------- None Displays the grid of divergent colormaps using matplotlib Examples -------- >>> show_divergent_cmaps() # Display all divergent colormaps """ list_cmaps(category='Divergent', display=display) return
[docs] def list_cyclic_cmaps(display: bool = False) -> None: """ Displays all cyclic colormaps in a grid layout. This function creates a grid of all cyclic colormaps available in matplotlib. The grid is displayed using matplotlib. Parameters ---------- display : bool, default=False If True, display the colormaps as a grid. Returns ------- None Displays the grid of cyclic colormaps using matplotlib Examples -------- >>> show_cyclic_cmaps() # Display all cyclic colormaps """ list_cmaps(category='Cyclic', display=display) return
[docs] def logscale_cmap(cmap: Union[str, Colormap], norders: int = 3) -> Colormap: """ Creates a logarithmic colormap by extending the input colormap with interpolated colors. This function takes a colormap and creates a new colormap with colors interpolated on a logarithmic scale. This is useful for visualizing data with a large dynamic range. Parameters ---------- cmap : str or matplotlib.colors.Colormap Input colormap name or instance norders : int, default=3 Number of orders of magnitude to span in the logarithmic scale. The resulting colormap will have 2 * norders colors. Returns ------- matplotlib.colors.Colormap A new colormap with colors interpolated on a logarithmic scale. The name of the new colormap will be 'log_' prefixed to the original name. Examples -------- >>> cmap = logscale_cmap('viridis', norders=2) >>> plt.imshow(data, cmap=cmap) """ cmap = plt.get_cmap(cmap) name = f"log_{cmap.name}" gradient = np.logspace(0, -norders, 2 * norders) colors = cmap(gradient) cmod = LinearSegmentedColormap.from_list(name, colors) return cmod
[docs] def get_bgcolor(cmap: Union[str, Colormap]) -> str: """ Gets background color for a given colormap. Parameters ---------- cmap : str or matplotlib.colors.Colormap Colormap name or instance. If a string is provided, it will be converted to a Colormap object. Returns ------- color : str Background color in hex format. The color is determined by: - Predefined background colors for known colormaps - The darkest color from the colormap, darkened by 80% for unknown colormaps Examples -------- >>> get_bgcolor('viridis') '#0E0717' >>> get_bgcolor(plt.cm.viridis) '#0E0717' """ cmap = plt.get_cmap(cmap) bg_color_dict = { "gruvbox": "#1D2021", "gruvbox_dark": "#1D2021", "gruvbox_light": "#FBF1C7", "cobalt": "#122738", "noctis": "#03181B", "monokai": "#272822", } if cmap.name in bg_color_dict: color = bg_color_dict[cmap.name] else: # Get darkest color and darken it by 0.3 colors = get_colors(cmap) h, s, v = np.array([mc.rgb_to_hsv(mc.to_rgb(c)) for c in colors]).T darkest_color = colors[np.argmin(v)] color: str = darken(darkest_color, 0.3) # type: ignore return color
""" Aliases for functions """
[docs] def show_cmaps(category: str = 'All'): """Alias for show_colormaps(display=True).""" return show_colormaps(category=category, display=True)
[docs] def show_qualitative_cmaps(category: str = 'All'): """Alias for list_qualitative_cmaps(display=True).""" return list_qualitative_cmaps(display=True)
[docs] def show_continuous_cmaps(category: str = 'All'): """Alias for list_continuous_cmaps(display=True).""" return list_continuous_cmaps(display=True)
[docs] def show_divergent_cmaps(category: str = 'All'): """Alias for list_divergent_cmaps(display=True).""" return list_divergent_cmaps(display=True)
[docs] def show_cyclic_cmaps(category: str = 'All'): """Alias for list_cyclic_cmaps(display=True).""" return list_cyclic_cmaps(display=True)
# Inherit docstring from show_colormaps show_cmaps.__doc__ = list_cmaps.__doc__ show_qualitative_cmaps.__doc__ = list_qualitative_cmaps.__doc__ show_continuous_cmaps.__doc__ = list_continuous_cmaps.__doc__ show_divergent_cmaps.__doc__ = list_divergent_cmaps.__doc__ show_cyclic_cmaps.__doc__ = list_cyclic_cmaps.__doc__ show_colormap: Callable[..., None] = show_cmap show_qualitative_colormaps: Callable[..., None] = show_qualitative_cmaps show_continuous_colormaps: Callable[..., None] = show_continuous_cmaps show_divergent_colormaps: Callable[..., None] = show_divergent_cmaps show_cyclic_colormaps: Callable[..., None] = show_cyclic_cmaps list_colormaps: Callable[..., None] = list_cmaps list_qualitative_colormaps: Callable[..., None] = list_qualitative_cmaps list_continuous_colormaps: Callable[..., None] = list_continuous_cmaps list_divergent_colormaps: Callable[..., None] = list_divergent_cmaps list_cyclic_colormaps: Callable[..., None] = list_cyclic_cmaps import types __all__ = [name for name, thing in globals().items() if not (name.startswith("_") or isinstance(thing, types.ModuleType))] del types