Source code for pycolorbar.univariate.cmap_viz

import math

import matplotlib.pyplot as plt
import numpy as np

from pycolorbar.univariate.cmap import get_cmap_colors, get_cmap_lab, get_cmap_lightness
from pycolorbar.univariate.cmap_cyclic import plot_circular_colormap


def _plot_colormaps_subplots(cmaps, plot_func, cols=None, subplot_size=None, subplot_kw=None, dpi=200, **plot_kwargs):
    """Plot a list of colormaps."""
    # Accept only a single colormap
    if not isinstance(cmaps, list):
        cmaps = [cmaps]

    # If a single colormap, plot with plot_colormap
    if len(cmaps) == 1:
        ax = plot_func(cmaps[0], **plot_kwargs)
        return ax.figure

    # Define subplot_size
    if subplot_size is None:
        subplot_size = (2, 0.5)

    # Define number of subplots
    n = len(cmaps)

    # Initialize subplot_kw
    # --> Required polar project for circular colormaps
    if subplot_kw is None:
        subplot_kw = {}

    # Define a layout most similar to a square
    if cols is None:
        cols = math.ceil(math.sqrt(n))
        cols = min(cols, 6)

    # Define number of rows required
    rows = int(np.ceil(n / cols))

    # Define figure width and height
    fig_width = cols * subplot_size[0]
    fig_height = rows * subplot_size[1]

    # Initialize figure
    fig, axes = plt.subplots(rows, cols, figsize=(fig_width, fig_height), dpi=dpi, subplot_kw=subplot_kw)
    fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.1, wspace=0.1)

    # Flatten axes for easy iteration
    axes = axes.ravel()

    # Loop through colormaps and axes
    for cmap, ax in zip(cmaps, axes[:n], strict=False):
        _ = plot_func(cmap=cmap, ax=ax, **plot_kwargs)

    # Turn off any remaining axes
    for ax in axes[n:]:
        ax.axis("off")
    return fig


####-------------------------------------------------------------------------------------------------------------------.
#### Univariate rectangular colormap


def _plot_colormap(cmap, ax):
    # mpl.colorbar.ColorbarBase(ax, cmap=cmap, orientation="horizontal")
    # ax.set_title(cmap.name, fontsize=10, weight="bold")

    # Create dummy image for colormap
    im = np.outer(np.ones(10), np.arange(100))
    ax.imshow(im, cmap=cmap)
    ax.set_title(cmap.name, fontsize=10, weight="bold")
    ax.axis("off")  # Set axis off
    return ax


[docs] def plot_colormap(cmap, dpi=200, ax=None, **plot_kwargs): """Plot a single colormap.""" if ax is None: fig, ax = plt.subplots(figsize=(4, 0.4), dpi=dpi) ax = _plot_colormap(cmap=cmap, ax=ax, **plot_kwargs) plt.show() return fig
[docs] def plot_colormaps(cmaps, cols=None, subplot_size=None, dpi=200, **plot_kwargs): """Plot a list of colormaps.""" # Define subplot_size if subplot_size is None: subplot_size = (2, 0.5) # Plot colormaps _ = _plot_colormaps_subplots( cmaps=cmaps, plot_func=_plot_colormap, cols=cols, subplot_size=subplot_size, dpi=dpi, **plot_kwargs, ) plt.show()
####-------------------------------------------------------------------------------------------------------------------. #### Univariate circular colormap
[docs] def plot_circular_colormaps( cmaps, # Subplots options cols=None, subplot_size=None, dpi=200, # Options for sine ramp of Kovesi n_cycles=0, # 50 amplitude=np.pi / 5, power=4, **plot_kwargs, ): """Plot a list of colormaps.""" # Define project subplot_kw = {"projection": "polar"} # Define subplot_size if subplot_size is None: subplot_size = (2, 2) # Plot colormaps fig = _plot_colormaps_subplots( cmaps=cmaps, plot_func=plot_circular_colormap, cols=cols, subplot_size=subplot_size, subplot_kw=subplot_kw, dpi=dpi, # Options for sine ramp of Kovesi n_cycles=n_cycles, amplitude=amplitude, power=power, **plot_kwargs, ) fig.tight_layout() plt.show()
####-------------------------------------------------------------------------------------------------------------------. #### Diagnostics
[docs] def plot_lightness(cmap, ax=None, difference=False, labelsize=6, ticksize=6, s=2, **plot_kwargs): """ Plot the lightness values of a colormap. Parameters ---------- cmap : matplotlib.colors.Colormap The colormap instance (e.g., LinearSegmentedColormap or ListedColormap) whose lightness will be plotted. ax : matplotlib.axes.Axes, optional The axes on which to plot the lightness. If None, a new figure and axes will be created. **plot_kwargs : keyword arguments Additional keyword arguments passed to `ax.scatter` (e.g., marker size, edge colors). Returns ------- ax : matplotlib.axes.Axes The axes with the lightness plot. """ if ax is None: fig, ax = plt.subplots(figsize=(3, 0.8), dpi=200) # noqa: RUF059 colors = get_cmap_colors(cmap) lightness = get_cmap_lightness(cmap) title = "Lightness" if difference: colors = colors[:-1] lightness = np.diff(lightness) title = "ΔL" x = np.linspace(0.0, 1.0, len(colors)) ax.scatter(x, lightness, c=colors, s=s, **plot_kwargs) ax.set_ylabel(title, fontsize=labelsize) ax.set_xlim(0, 1) if not difference: ax.set_ylim(0, 100) ax.xaxis.set_visible(False) ax.tick_params(axis="y", labelsize=ticksize) ax.figure.tight_layout() return ax
[docs] def plot_rgb_components(cmap, ax=None, labelsize=6, ticksize=6, s=2, **plot_kwargs): """ Plot the RGB components of a colormap. Parameters ---------- cmap : matplotlib.colors.Colormap The colormap instance (e.g., LinearSegmentedColormap or ListedColormap) whose RGB components will be plotted. ax : matplotlib.axes.Axes, optional The axes on which to plot the RGB components. If None, a new figure and axes will be created. labelsize : int, optional Font size for the labels. Default is 6. ticksize : int, optional Font size for the tick labels. Default is 6. s : int, optional Marker size for the scatter plot. Default is 2. **plot_kwargs : keyword arguments Additional keyword arguments passed to `ax.scatter` (e.g., marker size, edge colors). Returns ------- ax : matplotlib.axes.Axes The axes with the RGB component plots. """ if ax is None: fig, ax = plt.subplots(figsize=(3, 0.8), dpi=200) # noqa: RUF059 # Get RGB colors from colormap colors = get_cmap_colors(cmap) # Extract R, G, B components red = colors[:, 0] green = colors[:, 1] blue = colors[:, 2] # Generate x values for the plot x = np.linspace(0.0, 1.0, len(colors)) # Plot each component with the corresponding color ax.plot(x, red, color="red", label="Red", s=s, **plot_kwargs) ax.plot(x, green, color="green", label="Green", s=s, **plot_kwargs) ax.plot(x, blue, color="blue", label="Blue", s=s, **plot_kwargs) # Set y-axis label and legend ax.set_ylabel("RGB", fontsize=labelsize) # ax.legend(fontsize=labelsize, loc='upper right') # Set y-axis limits (RGB values range from 0 to 1) ax.set_ylim(0, 1) ax.set_xlim(0, 1) # Turn off x-axis ax.xaxis.set_visible(False) # Set y-axis ticks with the specified size ax.tick_params(axis="y", labelsize=ticksize) # Adjust layout ax.figure.tight_layout() return ax
[docs] def plot_lab_components(cmap, ax=None, add_legend=True, labelsize=6, ticksize=6, s=2, **plot_kwargs): """Plot the LAB components of a colormap. Lightness (L*) is displayed on the left y-axis and A* and B* components on the right y-axis. Parameters ---------- cmap : matplotlib.colors.Colormap The colormap instance (e.g., LinearSegmentedColormap or ListedColormap) whose RGB components will be plotted. ax : matplotlib.axes.Axes, optional The axes on which to plot the LAB components. If None, a new figure and axes will be created. labelsize : int, optional Font size for the labels. Default is 6. ticksize : int, optional Font size for the tick labels. Default is 6. s : int, optional Marker size for the scatter plot. Default is 2. **plot_kwargs : keyword arguments Additional keyword arguments passed to `ax.scatter` (e.g., marker size, edge colors). Returns ------- ax : matplotlib.axes.Axes The axes with the RGB component plots. """ if ax is None: fig, ax = plt.subplots(figsize=(3, 0.8), dpi=200) # noqa: RUF059 # Get RGB colors from colormap lab = get_cmap_lab(cmap) # Extract L*, A*, and B* components L = lab[:, 0] # Lightness A = lab[:, 1] # A* (green-red axis) B = lab[:, 2] # B* (blue-yellow axis) # Generate x values for the plot x = np.linspace(0.0, 1.0, len(L)) # Plot the Lightness (L*) on the primary y-axis (left) ax.plot(x, L, color="black", label="Lightness (L*)", s=s, **plot_kwargs) ax.set_ylabel("L", fontsize=labelsize, color="black") ax.set_ylim(0, 100) # Lightness typically ranges from 0 to 100 # Set y-axis ticks and label size for Lightness ax.tick_params(axis="y", labelsize=ticksize, colors="black") # Create a secondary y-axis for A* and B* components ax_right = ax.twinx() ax_right.plot(x, A, color="green", label="A", s=s, **plot_kwargs) ax_right.plot(x, B, color="blue", label="B", s=s, **plot_kwargs) # Set y-axis limits for A* and B* (typical range for A* and B* is -128 to 128) ax_right.set_ylabel("A,B", fontsize=labelsize) ax_right.set_ylim(-128, 128) # Set y-axis ticks and label size for A* and B* ax_right.tick_params(axis="y", labelsize=ticksize) # Add a legend for the A* and B* components if add_legend: ax_right.legend(loc="lower right", fontsize=5, ncol=2) # Turn off x-axis labels and set axis limits ax.xaxis.set_visible(False) ax.set_xlim(0, 1) # Adjust layout ax.figure.tight_layout() return ax
[docs] def delta_e_cie2000(lab1, lab2): """ Calculate the Delta E (CIEDE2000) color difference between two LAB colors. Parameters ---------- lab1 : array-like, shape (3,) LAB color 1 in the format [L*, a*, b*]. lab2 : array-like, shape (3,) LAB color 2 in the format [L*, a*, b*]. Returns ------- delta_e : float The Delta E (CIEDE2000) color difference between the two LAB colors. Notes ----- This implementation follows the CIEDE2000 standard and is adapted from formulas described in Sharma et al. (2005). """ # Constants kL = kC = kH = 1 # scaling factors L1, a1, b1 = lab1 L2, a2, b2 = lab2 # Mean values L_ = (L1 + L2) / 2.0 C1 = np.sqrt(a1**2 + b1**2) C2 = np.sqrt(a2**2 + b2**2) C_ = (C1 + C2) / 2.0 G = 0.5 * (1 - np.sqrt(C_**7 / (C_**7 + 25**7))) a1_ = (1 + G) * a1 a2_ = (1 + G) * a2 C1_ = np.sqrt(a1_**2 + b1**2) C2_ = np.sqrt(a2_**2 + b2**2) h1_ = np.degrees(np.arctan2(b1, a1_)) % 360 h2_ = np.degrees(np.arctan2(b2, a2_)) % 360 dL_ = L2 - L1 dC_ = C2_ - C1_ dh_ = h2_ - h1_ if C1_ * C2_ == 0: dh_ = 0 elif abs(dh_) > 180: dh_ -= np.sign(dh_) * 360 dH_ = 2 * np.sqrt(C1_ * C2_) * np.sin(np.radians(dh_ / 2)) L_50_sq = (L_ - 50) ** 2 SL = 1 + (0.015 * L_50_sq) / np.sqrt(20 + L_50_sq) SC = 1 + 0.045 * C_ T = ( 1 - 0.17 * np.cos(np.radians(h1_ - 30)) + 0.24 * np.cos(np.radians(2 * h1_)) + 0.32 * np.cos(np.radians(3 * h1_ + 6)) - 0.20 * np.cos(np.radians(4 * h1_ - 63)) ) SH = 1 + 0.015 * C_ * T dTheta = 30 * np.exp(-(((h1_ - 275) / 25) ** 2)) RC = 2 * np.sqrt(C_**7 / (C_**7 + 25**7)) RT = -RC * np.sin(np.radians(2 * dTheta)) dE = np.sqrt( (dL_ / (kL * SL)) ** 2 + (dC_ / (kC * SC)) ** 2 + (dH_ / (kH * SH)) ** 2 + RT * (dC_ / (kC * SC)) * (dH_ / (kH * SH)), ) return dE
[docs] def plot_deltae(cmap, ax=None, accurate=True, labelsize=6, ticksize=6, s=2, **plot_kwargs): """Plot the Delta E (CIE76) values of a colormap. The Delta E represents the color difference between consecutive colors in the LAB color space. Parameters ---------- cmap : matplotlib.colors.Colormap The colormap instance (e.g., LinearSegmentedColormap or ListedColormap) whose color differences will be plotted. ax : matplotlib.axes.Axes, optional The axes on which to plot the Delta E values. If None, a new figure and axes will be created. accurate: bool If True, compute the Delta E (CIEDE2000). IF False, compute the Delta E (CIE76) which corresponds to the Euclidean distance in the CIELAB color space. labelsize : int, optional Font size for the labels. Default is 6. ticksize : int, optional Font size for the tick labels. Default is 6. s : int, optional Marker size for the scatter plot. Default is 2. **plot_kwargs : keyword arguments Additional keyword arguments passed to `ax.plot` (e.g., line style, marker). Returns ------- ax : matplotlib.axes.Axes The axes with the Delta E plot. Notes ----- - Delta E (ΔE) is a measure of color difference in LAB color space. - This function uses the CIE76 formula to compute ΔE between consecutive colors in the colormap. - Typical ΔE values: - 0-1: Imperceptible difference. - 1-2: Perceptible only to trained eyes. - 2-10: Perceptible to the human eye. - 10+: Large difference. Example ------- >>> cmap = plt.get_cmap("viridis") >>> plot_deltae(cmap) """ if ax is None: fig, ax = plt.subplots(figsize=(3, 0.8), dpi=200) # noqa: RUF059 # Get RGB colors from colormap lab = get_cmap_lab(cmap) # Compute deltaE if accurate: delta_e = np.array([delta_e_cie2000(lab[i], lab[i + 1]) for i in range(len(lab) - 1)]) else: delta_e = np.linalg.norm(np.diff(lab, axis=0), axis=1) # Generate x values for the plot x = np.linspace(0.0, 1.0, len(delta_e)) # Plot the Lightness (L*) on the primary y-axis (left) ax.plot(x, delta_e, color="black", label="Lightness (L*)", s=s, **plot_kwargs) ax.set_ylabel("ΔE", fontsize=labelsize, color="black") ax.set_ylim(0, None) # Set y-axis ticks and label size for Lightness ax.tick_params(axis="y", labelsize=ticksize, colors="black") # Turn off x-axis labels and set axis limits ax.xaxis.set_visible(False) ax.set_xlim(0, 1) # Adjust layout ax.figure.tight_layout() return ax
[docs] def plot_viscm_diagnostic(cmap): """Evaluate goodness of colormap using perceptual deltas.""" try: from viscm import viscm except ImportError: raise ImportError( "The 'viscm' package is required but not found. " "Please install it using the following command: " "conda install -c conda-forge viscm", ) from None viscm(cmap) fig = plt.gcf() fig.set_size_inches(22, 10) plt.show()