Source code for pycolorbar.settings.colormap_registry

# -----------------------------------------------------------------------------.
# MIT License

# Copyright (c) 2024 pycolorbar developers
#
# This file is part of pycolorbar.

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

# -----------------------------------------------------------------------------.
"""Define the register of univiariate colormaps."""

import os
import tempfile

import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

from pycolorbar.settings.categories import (
    check_category_list,
    get_aux_category,
    get_matplotlib_cmaps,
)
from pycolorbar.settings.colormap_io import read_cmap_dict, write_cmap_dict
from pycolorbar.univariate import adapt_cmap
from pycolorbar.utils.yaml import list_yaml_files

# Matplotlib Registry Initialization:
# - In the __init__ : from matplotlib.cm import _colormaps as colormaps
# - Instead of singleton pattern, uses globals().updates() for module-level instance
#   --> https://github.com/matplotlib/matplotlib/blob/v3.8.2/lib/matplotlib/cm.py#L59
#   --> https://github.com/matplotlib/matplotlib/blob/v3.8.2/lib/matplotlib/cm.py#L236
#
#   _colormaps = ColormapRegistry()
#   globals().update(_colormaps)


[docs] def flatten_list(nested_list): """Flatten a nested list into a single-level list.""" if isinstance(nested_list, list) and len(nested_list) == 0: return nested_list # If list is already flat, return as is to avoid flattening to chars if isinstance(nested_list, list) and not isinstance(nested_list[0], list): return nested_list return [item for sublist in nested_list for item in sublist] if isinstance(nested_list, list) else [nested_list]
[docs] class ColormapRegistry: """ A singleton class to manage colormap registrations. This class provides methods to register colormaps, remove them, retrieve colormap file paths, and add new colormaps to a temporary directory for further processing. Attributes ---------- _instance : ColormapRegistry The singleton instance of the `ColormapRegistry`. registry : dict The dictionary holding the registered colormap names and their corresponding configuration YAML file paths. tmp_dir : str The path of a temporary directory where colormap YAML files are stored when specifying a colormap on-the-fly with `add_cmap_dict(cmap_dict)`. """ _instance = None def __new__(cls): """Create a new instance of the `ColormapRegistry`.""" if cls._instance is None: cls._instance = super().__new__(cls) # cls._instance = super(ColormapRegistry, cls).__new__(cls) cls._instance.registry = {} # Create temporary path cls._instance.tmp_dir = None return cls._instance
[docs] @classmethod def get_instance(cls): """Return the singleton instance of the `ColormapRegistry`.""" if cls._instance is None: cls() # this will call __new__ return cls._instance
[docs] def reset(self): """Clears the entire colormap registry.""" self.registry.clear()
@property def names(self): """List the names of all registered colormaps.""" return sorted(self.registry) def __contains__(self, item): """Test registration of colormap in the registry.""" return item in self.names def _check_if_cmap_in_use(self, name, force, verbose): if name in self.registry: if force and verbose: print(f"Warning: Overwriting existing colormap '{name}'") if not force: raise ValueError(f"A colormap named '{name}' already exists. To allow overwriting, set 'force=True'.")
[docs] def register(self, filepath: str, verbose: bool = True, force: bool = True): """ Register a colormap by its name and file path. Parameters ---------- filepath : str The file path where the colormap's YAML file is located. The name of the colormap correspond to the name of the YAML file ! verbose : bool, optional If `True`, the method will print a warning when overwriting an existing colormap. The default is `True`. force : bool, optional If `True`, it allow to overwrites an existing colormap. The default is `True`. If `False`, it raise an error if attempting to overwrite an existing colormap. Notes ----- If a colormap with the same name already exists, it will be overwritten, and a warning will be printed. The validity of the colormap's YAML file is not validated ! """ # Check file exists if not os.path.isfile(filepath): raise ValueError(f"The colormap configuration YAML file {filepath} does not exist.") # Define colormap name name = os.path.splitext(os.path.basename(filepath))[0] # Check if the name is already used self._check_if_cmap_in_use(name=name, force=force, verbose=verbose) # Register self.registry[name] = filepath
[docs] def add_cmap_dict(self, cmap_dict: dict, name: str, verbose: bool = True, force=True): """ Add a colormap to the registry by providing a colormap dictionary and the colormap name. A temporary file YAML configuration file is created in `ColormapRegistry.tmp_dir`. Parameters ---------- cmap_dict : dict The colormap dictionary containing the colormap's configuration. name : str The name of the colormap. verbose : bool, optional If `True`, the method will print a warning when overwriting an existing colormap. The default is `True`. force : bool, optional If `True`, it allow to overwrites an existing colormap. The default is `True`. If `False`, it raise an error if attempting to overwrite an existing colormap. Notes ----- If a colormap with the same name already exists, it will be overwritten, and a warning will be printed. The YAML file for the colormap is stored in a temporary directory. """ # Create a temporary directory if not yet initiated if self.tmp_dir is None: self.tmp_dir = tempfile.mkdtemp(prefix="pycolorbar_cmaps_") # Check if the name is already used self._check_if_cmap_in_use(name=name, force=force, verbose=verbose) # Define filepath filename = f"{name}.yaml" filepath = os.path.join(self.tmp_dir, filename) # Write cmap_dict (and validate) write_cmap_dict(cmap_dict, filepath=filepath, force=True, validate=True, encode=True) # Update registry self.registry[name] = filepath
[docs] def unregister(self, name: str): """ Remove a colormap from the registry. Parameters ---------- name : str The name of the colormap to remove. Raises ------ ValueError If the colormap with the specified name is not registered. """ if name in self.registry: _ = self.registry.pop(name) else: raise ValueError(f"The colormap {name} is not registered in pycolorbar.")
[docs] def get_cmap_filepath(self, name: str): """ Retrieve the colormap's YAML configuration file path. Parameters ---------- name : str The name of the colormap. Returns ------- str The colormap's YAML configuration file path. Raises ------ ValueError If the colormap with the specified name is not registered. """ # Remove _r suffix if name.endswith("_r"): name = name[:-2] # Retrieve filepath if name not in self.registry: raise ValueError(f"The {name} colormap is not registered in pycolorbar.") return self.registry[name]
[docs] def get_cmap_dict(self, name: str): """ Retrieve the validated colormap dictionary of a registered colormap. Parameters ---------- name : str The name of the colormap. Returns ------- dict The validated colormap dictionary. Raises ------ ValueError If the colormap configuration is invalid or cannot be read. """ filepath = self.get_cmap_filepath(name) return read_cmap_dict(filepath, validate=True, decode=True)
[docs] def get_cmap(self, name: str): """ Retrieve the colormap. Parameters ---------- name : str The name of the colormap. Returns ------- matplotlib.colors.Colormap The matplotlib colormap. Raises ------ ValueError If the colormap configuration is invalid or cannot be read. """ from pycolorbar.settings.colormap_utility import create_cmap cmap_dict = self.get_cmap_dict(name) cmap = create_cmap(name=name, cmap_dict=cmap_dict) if name.endswith("_r"): cmap = cmap.reversed(name) return cmap
[docs] def validate(self, name: str | None = None): """ Validate the registered colormaps. If a specific name is provided, only that colormap is validated. Parameters ---------- name : str, optional The name of a specific colormap to validate. If `None`, all registered colormaps are validated. Raises ------ ValueError If any of the validated colormaps have invalid configurations. Notes ----- Invalid colormap configurations are reported. """ if isinstance(name, str): if name not in self.names: raise ValueError(f"{name} is not a registered colormap.") names = [name] else: names = self.names # Validate colormaps wrong_names = [] for name in names: try: _ = self.get_cmap_dict(name) except Exception as e: wrong_names.append(name) print(f"{name} has an invalid configuration: {e}") print("") if wrong_names: raise ValueError(f"The {wrong_names} colormaps have invalid configurations.")
[docs] def to_yaml(self, name, filepath, force=False): """Write the colormap configuration to a YAML file.""" cmap_dict = self.get_cmap_dict(name) write_cmap_dict(cmap_dict=cmap_dict, filepath=filepath, force=force)
def _get_category_subset(self, category): """List subset of colormaps matching the specified category.""" category = check_category_list(category) names = [] for name in self.names: cmap_dict = self.get_cmap_dict(name) aux_category = get_aux_category(cmap_dict) # list of upper case strings if np.all(np.isin(category, aux_category)): # intersection ! names.append(name) return names
[docs] def available(self, category=None, include_reversed=False): """List the name of available colormaps for specific categories.""" names = self.names if category is None else self._get_category_subset(category=category) if include_reversed: names = [name + "_r" for name in names] + names return sorted(names)
[docs] def show_colormap(self, name): """Display a colormap.""" from pycolorbar.univariate import plot_colormap cmap = self.get_cmap(name) plot_colormap(cmap)
[docs] def show_colormaps(self, category=None, include_reversed=False, subplot_size=None): """Display available colormaps.""" from pycolorbar.univariate import plot_colormaps # Retrieve available colormaps (of given categories) names = self.available(category=category, include_reversed=include_reversed) if len(names) == 0: raise ValueError("No colormaps are yet registered in the pycolorbar ColormapRegistry.") # If only 1 colormap registered, plot it with the other method if len(names) == 1: self.show_colormap(name=names[0]) return # Else, retrieve colormaps to display cmaps = [self.get_cmap(name) for name in sorted(names)] # Display colormaps plot_colormaps(cmaps, subplot_size=subplot_size)
[docs] def register_colormaps(directory: str, name: str | None = None, verbose: bool = True, force: bool = True): """ Register all colormap YAML files present in the specified directory (if name=None). This function assumes that all YAML files present in the directory are valid pycolorbar colormaps configuration files. Parameters ---------- directory : str The directory where colormap YAML files are located. name : str, optional The specific name of a colormap to register. If `None`, all colormaps in the directory are registered. force : bool, optional If `True`, it allow to overwrites existing colormaps. The default is `True`. If `False`, it raise an error if attempting to overwrite an existing colormap. verbose : bool, optional If `True`, the method will print a warning when overwriting existing colormaps. The default is `True`. """ colormaps = ColormapRegistry.get_instance() # List the colormap YAML files to register if name is not None: # noqa SIM108 filepaths = [os.path.join(directory, f"{name}.yaml")] else: # List all YAML files in the directory filepaths = list_yaml_files(directory) # Add colormaps to the ColormapRegistry for filepath in filepaths: colormaps.register(filepath, verbose=verbose, force=force)
[docs] def register_colormap(filepath: str, verbose: bool = True, force: bool = True): """ Register a single colormap YAML file. Parameters ---------- filepath : str The file path where the colormap's YAML file is located. The name of the colormap correspond to the name of the YAML file ! force : bool, optional If `True`, it allow to overwrites an existing colormap. The default is `True`. If `False`, it raise an error if attempting to overwrite an existing colormap. verbose : bool, optional If `True`, the method will print a warning when overwriting an existing colormap. The default is `True`. Raises ------ ValueError If the specified colormap YAML file is not available in the directory or if trying to register an already registered colormap and `force=False`. """ colormaps = ColormapRegistry.get_instance() colormaps.register(filepath, verbose=verbose, force=force)
[docs] def get_cmap_dict(name): """ Retrieve the validated colormap dictionary of a registered colormap. Parameters ---------- name : str The name of the colormap. Returns ------- dict The validated colormap dictionary. Raises ------ Exception If the colormap configuration is invalid or cannot be read. """ colormaps = ColormapRegistry.get_instance() return colormaps.get_cmap_dict(name)
[docs] def get_cmap( name: str | None = None, n: int | None = None, interval: tuple | None = None, alpha: float | None = None, bias: float | None = 1, ): """ Get a colormap instance. Parameters ---------- name : str The name of a colormap known to pycolorbar or Matplotlib. If the name ends with the suffix `_r`, the colormap is reversed. n : int, optional If not `None` (the default), the colormap will be resampled to have n entries in the lookup table. If the name ends with the suffix `_r`, the resampling is done after reversing the colormap. interval : tuple of float, optional A tuple (start, end) with values between 0 and 1, indicating the fraction of the colormap to use. Defaults to using the full colormap (0, 1). alpha : float, optional A transparency value to apply to the colors, where 0 is fully transparent and 1 is fully opaque. If `alpha` is None (the default), the original transparency of the colors is preserved. bias : float, optional A factor that skews the distribution of colors in the colormap. A `bias` of 1 (default) results in no bias. Values less than 1 space the colors more widely at the high end of the color map. Values greater than 1 space the colors more widely at the lower end of the colormap. Returns ------- matplotlib.colors.Colormap A new colormap that reflects the specified interval, n, alpha and bias values. """ colormaps = ColormapRegistry.get_instance() # Use default matplotlib colormap if name is None: cmap = plt.get_cmap(name=name) # If already a colormap elif isinstance(name, mpl.colors.Colormap): cmap = name # Else retrieve registered else: # Retrieve registered colormaps names pycolorbar_registered_names = colormaps.names + [s + "_r" for s in colormaps.names] mpl_registered_names = plt.colormaps() # Get pycolorbar colormap if name in pycolorbar_registered_names: cmap = colormaps.get_cmap(name) # this reverse if necessary # Or registered matplotlib colormap elif name in mpl_registered_names: cmap = plt.get_cmap(name=name) # Unavailable colormap else: raise ValueError( f"{name} is not registered in pycolorbar and matplotlib !\n " f"Valid matplotlib colormap are {mpl_registered_names}.\n " f"Valid pycolorbar colormap are {pycolorbar_registered_names}.", ) # Adapt colormap if asked cmap = adapt_cmap(cmap, interval=interval, n=n, alpha=alpha, bias=bias) return cmap
[docs] def available_colormaps(category=None, include_reversed=False): """ Return a list with the name of registered colormaps. Parameters ---------- category : str or list, optional The name(s) of an optional category to subset the list of registered colormaps. In the colormap YAML file, the `auxiliary/category` field lists the relevant categories of the colormap. Common colormap categories are `'diverging'`, `'cyclic'`, `'sequential'`, `'categorical'`, `'qualitative'`, `'perceptual'`. If `None` (the default), returns all available colormaps. include_reversed : bool, optional Whether to include also the name of the reversed colormap suffixed by `_r`. The default is `False`. Returns ------- names : str List of registered colormaps. """ colormaps = ColormapRegistry.get_instance() category = check_category_list(category) names = colormaps.available(category=category, include_reversed=include_reversed) # Add matplotlib colormaps # - Only if categories is None or the specified category is a colormap category. names += get_matplotlib_cmaps(category=category, include_reversed=include_reversed) return sorted(np.unique(names))
[docs] def check_colormap_archive(): """Check the pycolorbar colormap archive.""" import pycolorbar # Reset registry pycolorbar.colormaps.reset() # Register the pycolorbar default colormaps colormap_dir = pycolorbar.etc_directory pycolorbar.register_colormaps(os.path.join(colormap_dir, "colormaps"), force=False) # Validate the colormaps pycolorbar.colormaps.validate() # validate all colormaps in the registry