"""
Utils
=====
Utility functions for registering and manipulating colormaps in various ways.
"""
# %% IMPORTS
# Built-in imports
import sys
from collections import OrderedDict
from glob import glob
from importlib.util import find_spec
from os import path
from textwrap import dedent
from typing import TYPE_CHECKING, Callable, Optional, Union
import matplotlib as mpl
import numpy as np
# Package imports
from colorspacious import cspace_converter
from matplotlib.colors import (
Colormap,
LinearSegmentedColormap,
ListedColormap as LC,
to_hex,
to_rgb,
)
# CMasher imports
from cmasher import cm as cmrcm
from ._known_cmap_types import _CMASHER_BUILTIN_MAP_TYPES
if TYPE_CHECKING:
from matplotlib.artist import Artist
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
_HAS_VISCM = find_spec("viscm") is not None
# All declaration
__all__ = [
"combine_cmaps",
"create_cmap_mod",
"create_cmap_overview",
"get_bibtex",
"get_cmap_list",
"get_cmap_type",
"get_sub_cmap",
"import_cmaps",
"register_cmap",
"set_cmap_legend_entry",
"take_cmap_colors",
"view_cmap",
]
# %% GLOBALS
# Obtain the colorspace converter for showing cmaps in gray-scale
cspace_convert = cspace_converter("sRGB1", "CAM02-UCS")
# Type aliases
CMAP = Union[str, Colormap]
RED: "TypeAlias" = float
GREEN: "TypeAlias" = float
BLUE: "TypeAlias" = float
RGB = list[tuple[RED, GREEN, BLUE]]
# %% HELPER FUNCTIONS
# Define function for obtaining the sorting order for lightness ranking
def _get_cmap_lightness_rank(cmap: CMAP) -> tuple[int, int, float, float, float, str]:
"""
Returns a tuple of objects used for sorting the provided `cmap` based
on its lightness profile.
Parameters
----------
cmap : str or :obj:`~matplotlib.colors.Colormap` object
The registered name of the colormap in :mod:`matplotlib.cm` or its
corresponding :obj:`~matplotlib.colors.Colormap` object.
Returns
-------
L_slope : int
The slope type of lightness profile of `cmap`.
L_type : int
The range type of lightness profile of `cmap`.
This is only used for sequential colormaps.
L_start : float
The starting lightness value of `cmap`.
For diverging/cyclic colormaps, this is the central lightness value.
L_rng : float
The lightness range of `cmap`.
L_rmse : float
The RMSE of the lightness profile of `cmap`.
For diverging/cyclic colormaps, this is the max RMSE of either half.
name : str
The name of `cmap`.
For qualitative and miscellaneous colormaps, this is the only value
that is used.
"""
# Obtain the colormap
if isinstance(cmap, str):
cmap = mpl.colormaps[cmap]
cm_type = get_cmap_type(cmap)
# Determine lightness profile stats for sequential/diverging/cyclic
if cm_type in ("sequential", "diverging", "cyclic"):
# Get RGB values for colormap
rgb = cmap(np.arange(cmap.N))[:, :3]
# Get lightness values of colormap
lab = cspace_converter("sRGB1", "CAM02-UCS")(rgb)
L = lab[:, 0]
# If cyclic colormap, add first L at the end
if cm_type == "cyclic":
L = np.r_[L, [L[0]]]
# Determine number of values that will be in deltas
N_deltas = len(L) - 1
# Determine the deltas of the lightness profile
deltas = np.diff(L)
derivs = N_deltas * deltas
# Set lightness profile type to 0
L_type = 0
# Determine the RMSE of the lightness profile of a sequential colormap
if cm_type == "sequential":
# Take RMSE of entire lightness profile
L_rmse = np.around(np.std(derivs), 0)
# Calculate starting lightness value
L_start = np.around(L[0], 0)
# Determine type of lightness profile
L_type += (not np.allclose(rgb[0], [0, 0, 0])) * 2
L_type += np.allclose(rgb[0], [0, 0, 0]) == np.allclose(rgb[-1], [1, 1, 1])
# Diverging/cyclic colormaps
else:
# Determine the center of the colormap
central_i = [int(np.ceil(N_deltas / 2)), int(np.floor(N_deltas / 2))]
# Calculate RMSE of both halves
L_rmse = np.max(
[
np.around(np.std(derivs[: central_i[0]]), 0),
np.around(np.std(derivs[central_i[1] :]), 0),
]
)
# Calculate central lightness value
L_start = np.around(np.average(L[central_i]), 0)
# Determine lightness range
L_rng = np.around(np.max(L) - np.min(L), 0)
# Determine if cmap goes from dark to light or the opposite
L_slope = (L_start > L[-1]) * 2 - 1
# For qualitative/misc colormaps, set all lightness values to zero
else:
L_slope = L_type = L_start = L_rng = L_rmse = 0
# Return lightness contributions to the rank
return (L_slope, L_type, L_start, L_rng, L_rmse, cmap.name)
# Define function for obtaining the sorting order for perceptual ranking
def _get_cmap_perceptual_rank(
cmap: CMAP,
) -> tuple[int, int, float, float, float, float, str]:
"""
In addition to returning the lightness rank as given by
:func:`~_get_cmap_lightness_rank`, also returns the length of the
perceptual profile, also known as the perceptual range, of the provided
`cmap`.
Parameters
----------
cmap : str or :obj:`~matplotlib.colors.Colormap` object
The registered name of the colormap in :mod:`matplotlib.cm` or its
corresponding :obj:`~matplotlib.colors.Colormap` object.
Returns
-------
*L_rank : objects
The values returned by :func:`~_get_cmap_lightness_rank`, except for
the name of the colormap.
P_rng : float
The perceptual range of `cmap`.
name : str
The name of `cmap`.
For qualitative and miscellaneous colormaps, this is the only value
that is used.
"""
# Obtain the colormap
if isinstance(cmap, str):
cmap = mpl.colormaps[cmap]
cm_type = get_cmap_type(cmap)
# Determine perceptual range for sequential/diverging/cyclic
if cm_type in ("sequential", "diverging", "cyclic"):
# Get RGB values for colormap
rgb = cmap(np.arange(cmap.N))[:, :3]
# Get lab values of colormap
lab = cspace_converter("sRGB1", "CAM02-UCS")(rgb)
# If cyclic colormap, add first lab at the end
if cm_type == "cyclic":
lab = np.r_[lab, [lab[0]]]
# Determine the deltas of the lightness profile
deltas = np.sqrt(np.sum(np.diff(lab, axis=0) ** 2, axis=-1))
# Determine perceptual range
P_rng = np.around(np.sum(deltas), 0)
# For qualitative/misc colormaps, set all values to zero
else:
P_rng = 0
# Return perceptual contributions to the rank
return (*_get_cmap_lightness_rank(cmap)[:-1], P_rng, cmap.name)
# %% FUNCTIONS
# This function combines multiple colormaps at given nodes
[docs]
def combine_cmaps(
*cmaps: Union[Colormap, str],
nodes: Optional[Union[list[float], np.ndarray]] = None,
n_rgb_levels: int = 256,
combined_cmap_name: str = "combined_cmap",
) -> LinearSegmentedColormap:
"""Create a composite matplotlib colormap by combining multiple colormaps.
Parameters
----------
*cmaps: Colormap or colormap name (str) to be combined.
nodes: list or numpy array of nodes (float). Defaults: equal divisions.
The blending points between colormaps, in the range [0, 1].
n_rgb_levels: int. Defaults: 256.
Number of RGB levels for each colormap segment.
combined_cmap_name: str. Defaults: "combined_cmap".
name of the combined Colormap.
Returns
-------
Colormap: The composite colormap.
Raises
------
TypeError: If the list contains mixed datatypes or invalid
colormap names.
ValueError: If the cmaps contain only one single colormap,
or if the number of nodes is not one less than the number
of colormaps, or if the nodes do not contain incrementing values
between 0.0 and 1.0.
Note
----
The colormaps are combined from low value to high value end.
References
----------
- https://stackoverflow.com/questions/31051488/combining-two-matplotlib-colormaps/31052741#31052741
Examples
--------
Using predefined colormap names::
>>> custom_cmap_1 = combine_cmaps(
["ocean", "prism", "coolwarm"], nodes=[0.2, 0.75]
)
Using Colormap objects::
>>> cmap_0 = plt.get_cmap("Blues")
>>> cmap_1 = plt.get_cmap("Oranges")
>>> cmap_2 = plt.get_cmap("Greens")
>>> custom_cmap_2 = combine_cmaps([cmap_0, cmap_1, cmap_2])
"""
# Check colormap datatype and convert to list[Colormap]
if len(cmaps) <= 1:
raise ValueError("Expected at least two colormaps to combine.")
for cm in cmaps:
if not isinstance(cm, (Colormap, str)):
raise TypeError(f"Unsupported colormap type: {type(cm)}.")
_cmaps: list[Colormap] = [
cm if isinstance(cm, Colormap) else mpl.colormaps[cm] for cm in cmaps
]
# Generate default nodes for equal separation
if nodes is None:
nodes_arr = np.linspace(0, 1, len(_cmaps) + 1)
elif isinstance(nodes, (list, np.ndarray)):
nodes_arr = np.concatenate([[0.0], nodes, [1.0]])
else:
raise TypeError(f"Unsupported nodes type: {type(nodes)}, expect list of float.")
# Check nodes length
if len(nodes_arr) != len(_cmaps) + 1:
raise ValueError(
"Number of nodes should be one less than the number of colormaps."
)
# Check node values
if any((nodes_arr < 0) | (nodes_arr > 1)) or any(np.diff(nodes_arr) <= 0):
raise ValueError(
"Nodes should only contain increasing values between 0.0 and 1.0."
)
# Generate composite colormap
combined_cmap_segments = []
for i, cmap in enumerate(_cmaps):
start_position = nodes_arr[i]
end_position = nodes_arr[i + 1]
# Calculate the length of the segment
segment_length = int(n_rgb_levels * (end_position - start_position))
# Append the segment to the combined colormap segments
combined_cmap_segments.append(cmap(np.linspace(0, 1, segment_length)))
# Combine the segments (from bottom to top)
return LinearSegmentedColormap.from_list(
combined_cmap_name, np.vstack(combined_cmap_segments)
)
# This function creates a standalone module of a CMasher colormap
[docs]
def create_cmap_mod(
cmap: str, *, save_dir: str = ".", _copy_name: Optional[str] = None
) -> str:
"""
Creates a standalone Python module of the provided *CMasher* `cmap` and
saves it in the given `save_dir` as '<`cmap`>.py'.
A standalone colormap module can be used to quickly share a colormap with
someone without adding the *CMasher* dependency.
Importing the created module allows the colormap to be used in the same way
as usual through *MPL* (including the 'cmr.' prefix).
Parameters
----------
cmap : str
The name of the *CMasher* colormap a standalone Python module must be
made for. An added 'cmr.' prefix will be ignored.
Optional
--------
save_dir : str. Default: '.'
The path to the directory where the module must be saved.
By default, the current directory is used.
Returns
-------
cmap_path : str
The path to the Python file containing the colormap module.
Example
-------
Creating a standalone Python module of the 'rainforest' colormap::
>>> create_cmap_mod('rainforest')
One can now import the 'rainforest' colormap in any script by moving the
created 'rainforest.py' file to the proper working directory and importing
it with ``import rainforest``.
Note
----
Unlike other *CMasher* utility functions, `cmap` solely accepts names of
colormaps that are registered in *CMasher* (:mod:`cmasher.cm`).
"""
# Get absolute value to provided save_dir
save_dir = path.abspath(save_dir)
# Remove any 'cmr.' prefix from provided cmap
name = cmap.removeprefix("cmr.")
# Obtain the CMasher colormap associated with the provided cmap
if (_cmap := cmrcm.cmap_d.get(name, None)) is None:
raise ValueError(f"{name!r} is not a valid cmasher colormap name")
cm_type = get_cmap_type(cmap)
# Obtain the RGB tuples of provided cmap
rgb = np.array(_cmap.colors)
# Convert RGB values to string
array_str = np.array2string(
rgb,
max_line_width=79,
prefix="cm_data = ",
separator=", ",
threshold=rgb.size,
formatter={"float": lambda x: "%.8f" % (x)},
)
# Create Python module template and add obtained RGB data to it
cm_py_file = dedent(
"""
# %% IMPORTS
# Package imports
import matplotlib as mpl
from matplotlib.colors import ListedColormap
# All declaration
__all__ = ["cmap"]
# Author declaration
__author__ = "Ellert van der Velden (@1313e)"
# Package declaration
__package__ = "cmasher"
# %% GLOBALS AND DEFINITIONS
# Type of this colormap
cm_type = '{0}'
# RGB-values of this colormap
cm_data = {1}
# Create ListedColormap object for this colormap
cmap = ListedColormap(cm_data, name='cmr.{2}', N={3})
cmap_r = cmap.reversed()
# Register (reversed) cmap in MPL
mpl.colormaps.register(cmap=cmap)
mpl.colormaps.register(cmap=cmap_r)
"""
)
# If this colormap is cyclic, add code to register shifted version as well
if cm_type == "cyclic":
cm_py_file += dedent(
"""
# Shift the entire colormap by half of its length
cm_data_s = list(cm_data[{4}:])
cm_data_s.extend(cm_data[:{4}])
# Create ListedColormap object for this shifted version
cmap_s = ListedColormap(cm_data_s, name='cmr.{2}_s', N={3})
cmap_s_r = cmap_s.reversed()
# Register shifted versions in MPL as well
mpl.colormaps.register(cmap=cmap_s)
mpl.colormaps.register(cmap=cmap_s_r)
"""
)
# Format py-file string
cm_py_file = cm_py_file.format(
cm_type, array_str, _copy_name or name, len(rgb), len(rgb) // 2
)
# Obtain the path to the module
cmap_path = path.join(save_dir, f"{_copy_name or name}.py")
# Create Python module
with open(cmap_path, "w") as f:
f.write(cm_py_file[1:])
# Return cmap_path
return cmap_path
# This function creates an overview plot of all colormaps specified
[docs]
def create_cmap_overview(
cmaps: Union[list[CMAP], dict[str, list[Colormap]], None] = None,
*,
savefig: Optional[str] = None,
use_types: bool = True,
sort: Optional[Union[str, Callable]] = "alphabetical",
show_grayscale: bool = True,
show_info: bool = False,
plot_profile: Union[bool, float] = False,
dark_mode: bool = False,
title: Optional[str] = "Colormap Overview",
wscale: float = 1,
hscale: float = 1,
) -> None:
"""
Creates an overview plot containing all colormaps defined in the provided
`cmaps`.
Optional
--------
cmaps : list of {str; :obj:`~matplotlib.colors.Colormap` objects}, dict \
of lists or None. Default: None
A list of all colormaps that must be included in the overview plot.
If dict of lists, the keys define categories for the colormaps.
If *None*, all colormaps defined in *CMasher* are used instead.
savefig : str or None. Default: None
If not *None*, the path where the overview plot must be saved to.
Else, the plot will simply be shown.
use_types : bool. Default: True
Whether all colormaps in `cmaps` should be categorized into their
colormap types (sequential; diverging; cyclic; qualitative; misc).
If `cmaps` is a dict, this value is ignored.
sort : {'alphabetical'/'name'; 'lightness'; 'perceptual'}, function or \
None. Default: 'alphabetical'
String or function indicating how the colormaps should be sorted in the
overview.
If 'alphabetical', the colormaps are sorted alphabetically on their
name.
If 'lightness', the colormaps are sorted based on their lightness
profile, which is given by :func:`~_get_cmap_lightness_rank`.
If 'perceptual', the colormaps sorted based on their perceptual range
in addition to their lightness profile, which is given by
:func:`~_get_cmap_perceptual_rank`. Note that this is only meaningful
if all `cmaps` are perceptually uniform sequential.
If function, a function definition that takes a
:obj:`~matplotlib.colors.Colormap` object and returns the sorted
position of that colormap.
If *None*, the colormaps retain the order they were given in.
show_grayscale : bool. Default: True
Whether to show the grayscale versions of the given `cmaps` in the
overview.
show_info : bool. Default: False
Whether the statistics information of all sequential, diverging and
cyclic colormaps should be shown under their names. This is a series of
numbers representing, in order, the starting (sequential) or central
(diverging/cyclic) lightness value; the final/outer lightness value;
and the perceptual range of the colormap.
plot_profile : bool or float. Default: False
Whether the lightness profiles of all non-qualitative colormaps should
be plotted. If not *False*, the lightness profile of a colormap is
plotted on top of its gray-scale version and `plot_profile` is used for
setting the alpha (opacity) value.
If `plot_profile` is *True*, it will be set to `0.25`.
If `show_grayscale` is *False*, this value is ignored.
dark_mode : bool. Default: False
Whether the colormap overview should be created using mostly dark
colors.
title : str or None. Default: "Colormap Overview"
String to be used as the title of the colormap overview.
If empty or *None*, no title will be used.
wscale, hscale : float. Default: (1, 1)
Floats that determine with what factor the colormap subplot dimensions
in the overview should be scaled with.
The default values uses the default dimensions for the subplots (which
are determined by other input arguments).
Notes
-----
The colormaps in `cmaps` can either be provided as their registered name in
:mod:`matplotlib.cm`, or their corresponding
:obj:`~matplotlib.colors.Colormap` object.
Any provided reversed colormaps (colormaps that end their name with '_r')
are ignored if their normal versions were provided as well.
When `sort` is 'lightness' or 'perceptual', qualitative and miscellaneous
colormaps are solely sorted on their names, as the lightness/perceptual
profile of these colormaps is meaningless.
If `plot_profile` is not set to *False*, the lightness profiles are plotted
on top of the gray-scale colormap versions, where the y-axis ranges from 0%
lightness to 100% lightness.
The lightness profile transitions between black and white at 50% lightness.
"""
import matplotlib.pyplot as plt
# If cmaps is None, use cmap_d.values
if cmaps is None:
cmaps = list(cmrcm.cmap_d.values())
# If sort is a string, obtain proper function
if isinstance(sort, str):
# Convert sort to lowercase
sort = sort.lower()
# Check what string was provided and obtain sorting function
if sort in ("alphabetical", "name"):
def sort_key(x):
return x.name
elif sort == "lightness":
sort_key = _get_cmap_lightness_rank
elif sort == "perceptual":
sort_key = _get_cmap_perceptual_rank
else:
raise ValueError(
"Input argument 'sort' has invalid string value " "%r!" % (sort)
)
# Create empty list of cmaps
cmaps_list: list[Union[Colormap, tuple[str, bool]]] = []
# Define empty dict of colormaps
cmaps_dict: OrderedDict[str, list[Colormap]] = OrderedDict()
# If cmaps is a dict, it has cm_types defined
if isinstance(cmaps, dict):
# Set use_types to True
use_types = True
# Save provided cmaps as something else
input_cmaps = cmaps
# Loop over all cm_types
for cm_type, maps in input_cmaps.items():
# Add empty list of colormaps to cmaps_dict with this cm_type
cmaps_dict[cm_type] = []
# Loop over all cmaps and add their Colormap objects
for cmap in maps:
if isinstance(cmap, str):
cmaps_dict[cm_type].append(mpl.colormaps[cmap])
else:
cmaps_dict[cm_type].append(cmap)
# Else, it is a list with no cm_types
else:
# If cm_types are requested
if use_types:
# Define empty dict with the base cm_types
cm_types = ["sequential", "diverging", "cyclic", "qualitative", "misc"]
cmaps_dict.update({cm_type: [] for cm_type in cm_types})
# Loop over all cmaps and add their Colormap objects
for cm in cmaps:
cm_type = get_cmap_type(cm)
if isinstance(cm, str):
cmaps_dict[cm_type].append(mpl.colormaps[cm])
else:
cmaps_dict[cm_type].append(cm)
else:
# Loop over all cmaps and add their Colormap objects
for cm in cmaps:
if isinstance(cm, str):
cmaps_list.append(mpl.colormaps[cm])
else:
cmaps_list.append(cm)
# If use_types is True, a dict is currently used
if use_types:
# Convert entire cmaps_dict into a list again
for key, value in cmaps_dict.items():
# If this cm_type has at least 1 colormap, sort and add them
if value:
# Obtain the names of all colormaps
names = [x.name for x in value]
# Remove all reversed colormaps that also have their original
off_dex = len(names) - 1
for i, name in enumerate(reversed(names)):
if name.endswith("_r") and name[:-2] in names:
value.pop(off_dex - i)
# Sort the colormaps if requested
if sort is not None:
value.sort(key=sort_key)
# Add to list
cmaps_list.append((key, False))
cmaps_list.extend(value)
# Else, a list is used
else:
# Obtain the names of all colormaps
names = [x.name for x in cmaps_list if isinstance(x, Colormap)]
# Remove all reversed colormaps that also have their original
off_dex = len(names) - 1
for i, name in enumerate(reversed(names)):
if name.endswith("_r") and name[:-2] in names:
cmaps_list.pop(off_dex - i)
# Sort the colormaps if requested
if sort is not None:
cmaps_list.sort(key=sort_key)
# Add title to cmaps_list if requested
if title:
cmaps_list.insert(0, (title, True))
# Check value of show_grayscale
if show_grayscale:
# If True, the overview will have two columns
ncols = 2
else:
# If False, the overview will have one column
ncols = 1
wscale *= 0.5
# Determine text/element positions
wscale = 0.2 + 0.8 * wscale
left_pos = 0.2 / wscale
spacing = 0.01 / wscale
title_pos = left_pos + (1 - spacing - left_pos) / 2
# If plot_profile is True, set it to its default value
if plot_profile is True:
plot_profile = 0.25
# Check if dark mode is requested
if dark_mode:
# If so, use dark gray for the background and light gray for the text
edge_color = "#24292E"
face_color = "#24292E"
text_color = "#9DA5B4"
else:
# If not, use white for the background and black for the text
edge_color = "#FFFFFF"
face_color = "#FFFFFF"
text_color = "#000000"
# Create figure instance
height = (0.4 * len(cmaps_list) + 0.1) * hscale
fig, axs = plt.subplots(
figsize=(6.4 * wscale, height),
nrows=len(cmaps_list),
ncols=ncols,
edgecolor=edge_color,
facecolor=face_color,
)
# Adjust subplot positioning
fig.subplots_adjust(
top=(1 - 0.05 / height),
bottom=0.05 / height,
left=left_pos,
right=1.0 - spacing,
wspace=0.05,
)
# If cmaps_list only has a single element, make sure axs is a list
if len(cmaps_list) == 1:
axs = [axs]
# Loop over all cmaps defined in cmaps list
for ax, _cm in zip(axs, cmaps_list):
# Obtain axes objects and turn them off
if show_grayscale:
# Obtain Axes objects
ax0, ax1 = ax
# Turn axes off
ax0.set_axis_off()
ax1.set_axis_off()
else:
# Obtain Axes object
ax0 = ax
# Turn axis off
ax0.set_axis_off()
# Obtain position bbox of ax0
pos0 = ax0.get_position()
# If cmap is a tuple, it defines a title or cm_type
if isinstance(_cm, tuple):
# If it is a title
if _cm[1]:
# Write the title as text in the correct position
fig.text(
title_pos,
pos0.y0 + pos0.height / 2,
_cm[0],
va="center",
ha="center",
fontsize=18,
c=text_color,
)
# If it is a cm_type
else:
# Write the cm_type as text in the correct position
fig.text(
title_pos,
pos0.y0,
_cm[0],
va="bottom",
ha="center",
fontsize=14,
c=text_color,
)
# Else, this is a colormap
elif isinstance(_cm, Colormap):
# Obtain the colormap type
cm_type = get_cmap_type(_cm)
# Get array of all values for which a colormap value is requested
x = np.arange(_cm.N)
# Get RGB values for colormap
rgb = _cm(x)[:, :3]
# Add colormap subplot
ax0.imshow(rgb[np.newaxis, ...], aspect="auto")
# Add gray-scale colormap subplot if requested
if show_grayscale:
# Get lightness values of colormap
lab = cspace_convert(rgb)
L = lab[:, 0]
# Normalize lightness values
L /= 99.99871678
# Get RGB values for lightness values using neutral
rgb_L = cmrcm.neutral(L)[:, :3]
# Add gray-scale colormap subplot
ax1.imshow(rgb_L[np.newaxis, ...], aspect="auto")
# Check if the lightness profile was requested
if plot_profile and (cm_type != "qualitative"):
# Determine the points that need to be plotted
plot_L = -(L - 0.5)
points = np.stack([x, plot_L], axis=1)
# Determine the colors that each point must have
# Use black for L >= 0.5 and white for L <= 0.5.
colors = np.zeros_like(plot_L, dtype=int)
colors[plot_L >= 0] = 1
# Split points up into segments with the same color
s_idx = np.nonzero(np.diff(colors))[0] + 1
segments = np.split(points, s_idx)
# Loop over all pairs of adjacent segments
for i, (seg1, seg2) in enumerate(zip(segments[:-1], segments[1:])):
# Determine the point in the center of these segments
central_point = (seg1[-1] + seg2[0]) / 2
# Add this point to the ends of these segments
# This ensures that color changes in between segments
segments[i] = np.r_[segments[i], [central_point]]
segments[i + 1] = np.r_[[central_point], segments[i + 1]]
from matplotlib.collections import LineCollection
# Create an MPL LineCollection object with these segments
lc = LineCollection(
segments,
cmap=cmrcm.neutral,
alpha=plot_profile,
)
lc.set_linewidth(1)
# Determine the colors of each segment
s_colors = [colors[0]]
s_colors.extend(colors[s_idx])
# Set the values of the line-collection to be these colors
lc.set_array(np.array(s_colors))
# Add line-collection to this subplot
ax1.add_collection(lc)
# Determine positions of colormap name
x_text = pos0.x0 - spacing
y_text = pos0.y0 + pos0.height / 2
# Check if lightness information was requested for valid cm_type
if show_info and cm_type in ("sequential", "diverging", "cyclic"):
# If so, obtain lightness/perceptual profile information
rank = _get_cmap_perceptual_rank(_cm)[0:6]
# Write name of colormap in the correct position
fig.text(
x_text,
y_text,
_cm.name,
va="bottom",
ha="right",
fontsize=10,
c=text_color,
)
# Write lightness profile information in the correct position
fig.text(
x_text,
y_text,
f"({rank[2]:.3g}, {rank[2]-rank[0]*rank[3]:.3g}, {rank[5]:.3g})",
va="top",
ha="right",
fontsize=10,
c=text_color,
)
else:
# If not, just write the name of the colormap
fig.text(
x_text,
y_text,
_cm.name,
va="center",
ha="right",
fontsize=10,
c=text_color,
)
else:
raise RuntimeError
# If savefig is not None, save the figure
if savefig is not None:
dpi = 100 if (path.splitext(savefig)[1] == ".svg") else 250
plt.savefig(savefig, dpi=dpi, facecolor=face_color, edgecolor=edge_color)
plt.close(fig)
# Else, simply show it
else:
plt.show()
# Define function that prints a string with the BibTeX entry to CMasher's paper
[docs]
def get_bibtex() -> None:
"""
Prints a string that gives the BibTeX entry for citing the *CMasher* paper
(Van der Velden 2020, JOSS, 5, 2004).
"""
# Create string with BibTeX entry
bibtex = dedent(
r"""
@ARTICLE{2020JOSS....5.2004V,
author = {{van der Velden}, Ellert},
title = "{CMasher: Scientific colormaps for making accessible,
informative and 'cmashing' plots}",
journal = {The Journal of Open Source Software},
keywords = {Python, science, colormaps, data visualization,
plotting, Electrical Engineering and Systems Science - Image
and Video Processing, Physics - Data Analysis, Statistics and
Probability},
year = 2020,
month = feb,
volume = {5},
number = {46},
eid = {2004},
pages = {2004},
doi = {10.21105/joss.02004},
archivePrefix = {arXiv},
eprint = {2003.01069},
primaryClass = {eess.IV},
adsurl = {https://ui.adsabs.harvard.edu/abs/2020JOSS....5.2004V},
adsnote = {Provided by the SAO/NASA Astrophysics Data System}
}
"""
)
# Print the string
print(bibtex.strip())
# This function returns a list of all colormaps available in CMasher
[docs]
def get_cmap_list(cmap_type: str = "all") -> list[str]:
"""
Returns a list with the names of all colormaps available in *CMasher* of
the given `cmap_type`.
Note that *CMasher* colormaps registered in *MPL* have an added 'cmr.'
prefix.
Optional
--------
cmap_type : {'a'/'all'; 's'/'seq'/'sequential'; 'd'/'div'/'diverging'; \
'c'/'cyc'/'cyclic'}. Default: 'all'
The colormap type that should be in the returned list.
Returns
-------
cmap_list : list of str
List containing the names of all colormaps available in *CMasher*.
"""
# Convert cmap_type to lowercase
cmap_type = cmap_type.lower()
# Obtain proper list
if cmap_type in ("a", "all"):
cmaps = list(cmrcm.cmap_d)
elif cmap_type in ("s", "seq", "sequential"):
cmaps = list(cmrcm.cmap_cd["sequential"])
elif cmap_type in ("d", "div", "diverging"):
cmaps = list(cmrcm.cmap_cd["diverging"])
elif cmap_type in ("c", "cyc", "cyclic"):
cmaps = list(cmrcm.cmap_cd["cyclic"])
# Return cmaps
return cmaps
# This function determines the colormap type of a given colormap
[docs]
def get_cmap_type(cmap: CMAP) -> str:
"""
Checks what the colormap type (sequential; diverging; cyclic; qualitative;
misc) of the provided `cmap` is and returns it.
Parameters
----------
cmap : str or :obj:`~matplotlib.colors.Colormap` object
The registered name of the colormap in :mod:`matplotlib.cm` or its
corresponding :obj:`~matplotlib.colors.Colormap` object.
Returns
-------
cm_type : {'sequential'; 'diverging'; 'cyclic'; 'qualitative'; 'misc'}
A string stating which of the defined colormap types the provided
`cmap` has.
"""
# Obtain the colormap
if isinstance(cmap, str):
if cmap in _CMASHER_BUILTIN_MAP_TYPES:
# fast track for known results
return _CMASHER_BUILTIN_MAP_TYPES[cmap]
cmap = mpl.colormaps[cmap]
# Get RGB values for colormap
rgb = cmap(np.arange(cmap.N))[:, :3]
# Get lightness values of colormap
lab = cspace_converter("sRGB1", "CAM02-UCS")(rgb)
L = lab[:, 0]
diff_L = np.diff(L)
# Obtain central values of lightness
N = cmap.N - 1
central_i = [int(np.floor(N / 2)), int(np.ceil(N / 2))]
diff_L0 = np.diff(L[: central_i[0] + 1])
diff_L1 = np.diff(L[central_i[1] :])
# Obtain perceptual differences of last two and first two values
lab_red = lab[[-2, -1, 0, 1]]
deltas = np.sqrt(np.sum(np.diff(lab_red, axis=0) ** 2, axis=-1))
# Check the statistics of cmap and determine the colormap type
# QUALITATIVE
# If the colormap has less than 40 values, assume it is qualitative
if cmap.N < 40:
return "qualitative"
# MISC 1
# If the colormap has only a single lightness, it is misc
elif np.allclose(diff_L, 0):
return "misc"
# SEQUENTIAL
# If the lightness values always increase or decrease, it is sequential
elif np.isclose(np.abs(np.sum(diff_L)), np.sum(np.abs(diff_L))):
return "sequential"
# DIVERGING
# If the lightness values have a central extreme and sequential sides
# Then it is diverging
elif np.isclose(np.abs(np.sum(diff_L0)), np.sum(np.abs(diff_L0))) and np.isclose(
np.abs(np.sum(diff_L1)), np.sum(np.abs(diff_L1))
):
# If the perceptual difference between the last and first value is
# comparable to the other perceptual differences, it is cyclic
if np.all(np.abs(np.diff(deltas)) < deltas[::2]) and np.diff(deltas[::2]):
return "cyclic"
# Otherwise, it is a normal diverging colormap
else:
return "diverging"
# MISC 2
# If none of the criteria above apply, it is misc
else:
return "misc"
# Function create a colormap using a subset of the colors in an existing one
[docs]
def get_sub_cmap(
cmap: CMAP, start: float, stop: float, *, N: Optional[int] = None
) -> LC:
"""
Creates a :obj:`~matplotlib.cm.ListedColormap` object using the colors in
the range `[start, stop]` of the provided `cmap` and returns it.
This function can be used to create a colormap that only uses a portion of
an existing colormap.
If `N` is not set to *None*, this function creates a qualitative colormap
from `cmap` instead.
Parameters
----------
cmap : str or :obj:`~matplotlib.colors.Colormap` object
The registered name of the colormap in :mod:`matplotlib.cm` or its
corresponding :obj:`~matplotlib.colors.Colormap` object.
start, stop : float
The normalized range of the colors in `cmap` that must be in the
sub-colormap.
Optional
--------
N : int or None. Default: None
The number of color segments to take from the provided `cmap` within
the range given by the provided `start` and `stop`.
If *None*, take all colors in `cmap` within this range.
Returns
-------
sub_cmap : :obj:`~matplotlib.colors.ListedColormap`
The created colormap that uses a subset of the colors in `cmap`.
If `N` is not *None*, this will be a qualitative colormap.
Example
-------
Creating a colormap using the first 80% of the 'rainforest' colormap::
>>> get_sub_cmap('cmr.rainforest', 0, 0.8)
Creating a qualitative colormap containing five colors from the middle 60%
of the 'lilac' colormap:
>>> get_sub_cmap('cmr.lilac', 0.2, 0.8, N=5)
Notes
-----
As it can create artifacts, this function does not interpolate between the
colors in `cmap` to fill up the space. Therefore, using values for `start`
and `stop` that are too close to each other, may result in a colormap that
contains too few different colors to be smooth.
It is recommended to use at least 128 different colors in a colormap for
optimal results (*CMasher* colormaps have 256 or 511/510 different colors,
for sequential or diverging/cyclic colormaps respectively).
One can check the number of colors in a colormap with
:attr:`matplotlib.colors.Colormap.N`.
Any colormaps created using this function are not registered in either
*CMasher* or *MPL*.
"""
if isinstance(cmap, str):
# Obtain the colormap
cmap = mpl.colormaps[cmap]
# Check value of N to determine suffix for the name
suffix = "_sub" if N is None else "_qual"
# Obtain colors
colors = take_cmap_colors(cmap, N, cmap_range=(start, stop))
# Create new colormap
sub_cmap = LC(colors, cmap.name + suffix, N=len(colors))
# Return sub_cmap
return sub_cmap
# Function to import all custom colormaps in a file or directory
[docs]
def import_cmaps(cmap_path: str, *, _skip_registration: bool = False) -> None:
"""
Reads in custom colormaps from a provided file or directory `cmap_path`;
transforms them into :obj:`~matplotlib.colors.ListedColormap` objects; and
makes them available in the :mod:`cmasher.cm` module, in addition to
registering them in the :mod:`matplotlib.cm` module.
Both the imported colormap and its reversed version will be registered.
If a provided colormap is a 'cyclic' colormap, its shifted version will
also be registered with the `_s` suffix.
Parameters
----------
cmap_path : str
Relative or absolute path to a custom colormap file; or directory that
contains custom colormap files. A colormap file can be a *NumPy* binary
file ('.npy'); a *viscm* source file ('.jscm'); or any text file.
If the file is not a JSCM-file, it must contain the normalized; 8-bit;
or hexadecimal string RGB values that define the colormap.
Notes
-----
All colormap files must have names starting with the 'cm\\_' prefix. The
resulting colormaps will have the name of their file without the prefix and
extension.
In *MPL*, the colormaps will have the added 'cmr.' prefix to avoid name
clashes.
Example
-------
Importing a colormap named 'test' can be done by saving its normalized RGB
values in a file called 'cm_test.txt' and executing
>>> import_cmaps('/path/to/dir/cm_test.txt')
The 'test' colormap is now available in *CMasher* and *MPL* using
>>> cmr.cm.test # CMasher
>>> plt.get_cmap('cmr.test') # MPL
"""
# Obtain path to file or directory with colormaps
cmap_path = path.abspath(cmap_path)
# Check if provided file or directory exists
if not path.exists(cmap_path):
raise OSError(
"Input argument 'cmap_path' is a non-existing path (%r)!" % (cmap_path)
)
# Check if cmap_path is a file or directory and act accordingly
if path.isfile(cmap_path):
# If file, split cmap_path up into dir and file components
cmap_dir, cmap_file = path.split(cmap_path)
# Check if its name starts with 'cm_' and raise error if not
if not cmap_file.startswith("cm_"):
raise OSError(
"Input argument 'cmap_path' does not lead to a file "
"with the 'cm_' prefix (%r)!" % (cmap_path)
)
# Set cm_files to be the sole read-in file
cm_files = [cmap_file]
else:
# If directory, obtain the names of all colormap files in cmap_path
cmap_dir = cmap_path
cm_files = list(map(path.basename, glob("%s/cm_*" % (cmap_dir))))
cm_files.sort()
def sort_key(name):
# prioritize binary files over text files because binary loads faster
_, ext = path.splitext(name)
if ext == ".npy":
return 0
if ext == ".txt":
return 1
return 10
cm_files.sort(key=sort_key)
del sort_key
if any(file.endswith(".jscm") for file in cm_files) and not _HAS_VISCM:
raise ValueError("The 'viscm' package is required to read '.jscm' files!")
# Read in all the defined colormaps, transform and register them
seen: set[str] = set()
for cm_file in cm_files:
# Split basename and extension
base_str, ext_str = path.splitext(cm_file)
if base_str in seen:
continue
else:
seen.add(base_str)
cm_name = base_str[3:]
# Obtain absolute path to colormap data file
cm_file_path = path.join(cmap_dir, cm_file)
# Process colormap files
try:
# If file is a NumPy binary file
if ext_str == ".npy":
rgb = np.load(cm_file_path)
# If file is viscm source file
elif ext_str == ".jscm":
import viscm
# Load colormap
cmap = viscm.gui.Colormap(None, None, None)
cmap.load(cm_file_path)
# Create editor and obtain RGB values
v = viscm.viscm_editor(
uniform_space=cmap.uniform_space,
cmtype=cmap.cmtype,
method=cmap.method,
**cmap.params,
)
rgb, _ = v.cmap_model.get_sRGB()
# If file is anything else
else:
rgb = np.genfromtxt(
cm_file_path, dtype=None, comments="//", encoding=None
) # type: ignore [call-overload]
if not _skip_registration:
# Register colormap
register_cmap(cm_name, rgb)
# Check if provided cmap is a cyclic colormap
# If so, obtain its shifted (reversed) versions as well
if get_cmap_type("cmr." + cm_name) == "cyclic":
# Determine the central value index of the colormap
idx = len(rgb) // 2
# Shift the entire colormap by this index
rgb_s = np.r_[rgb[idx:], rgb[:idx]]
if not _skip_registration:
# Register this colormap as well
register_cmap(cm_name + "_s", rgb_s)
# If any error is raised, reraise it
except Exception as error:
raise ValueError(
f"Provided colormap {cm_name} is invalid! ({error})"
) from error
# Function to register a custom colormap in MPL and CMasher
[docs]
def register_cmap(name: str, data: RGB) -> None:
"""
Creates a :obj:`~matplotlib.colors.ListedColormap` object using the
provided `name` and `data`, and registers the colormap in the
:mod:`cmasher.cm` and :mod:`matplotlib.cm` modules.
A reversed version of the colormap will be registered as well.
Parameters
----------
name : str
The name that this colormap must have.
data : 2D array_like of {float; int} with shape `(N, 3)` or 1D array_like \
of str with shape `(N, )`
An array containing the RGB values of all segments in the colormap.
If float, the array contains normalized RGB values.
If int, the array contains 8-bit RGB values.
If str, the array contains hexadecimal string RGB values.
Note
----
In *MPL*, the colormap will have the added 'cmr.' prefix to avoid name
clashes.
"""
# Convert provided data to a NumPy array
cm_data_arr = np.array(data)
# Check the type of the data
if issubclass(cm_data_arr.dtype.type, str):
# If the values are strings, make sure they start with a '#'
if cm_data_arr.ndim == 0:
cm_data = [cm_data_arr.item()]
else:
cm_data = ["#" + x if not x.startswith("#") else x for x in cm_data_arr]
try:
# Convert all values to floats
colorlist = [to_rgb(_) for _ in cm_data]
except ValueError:
raise ValueError(
f"Input data isn't valid hexadecimal RGB values: {data=}"
) from None
else:
# Make sure that cm_data is 2D
cm_data_arr = np.atleast_2d(cm_data_arr)
# If the values are integers, divide them by 255
if issubclass(cm_data_arr.dtype.type, np.integer):
cm_data_arr = cm_data_arr / 255
# Convert cm_data to a list
colorlist = cm_data_arr.tolist()
# Transform colorlist into a Colormap
cmap_N = len(colorlist)
cmap_mpl = LC(colorlist, "cmr." + name, N=cmap_N)
cmap_cmr = LC(colorlist, name, N=cmap_N)
cmap_mpl_r = cmap_mpl.reversed()
cmap_cmr_r = cmap_cmr.reversed()
# Determine the cm_type of the colormap
if name in _CMASHER_BUILTIN_MAP_TYPES:
cm_type = _CMASHER_BUILTIN_MAP_TYPES[name]
else:
cm_type = get_cmap_type(cmap_mpl)
# Test that the colormaps can be called
cmap_mpl(1)
cmap_mpl_r(1)
# Add cmap to matplotlib's cmap list
mpl.colormaps.register(cmap=cmap_mpl)
setattr(cmrcm, cmap_cmr.name, cmap_cmr)
cmrcm.__all__.append(cmap_cmr.name)
cmrcm.cmap_d[cmap_cmr.name] = cmap_cmr
cmrcm.cmap_cd[cm_type][cmap_cmr.name] = cmap_cmr
# Add reversed cmap to matplotlib's cmap list
mpl.colormaps.register(cmap=cmap_mpl_r)
setattr(cmrcm, cmap_cmr_r.name, cmap_cmr_r)
cmrcm.__all__.append(cmap_cmr_r.name)
cmrcm.cmap_d[cmap_cmr_r.name] = cmap_cmr_r
cmrcm.cmap_cd[cm_type][cmap_cmr_r.name] = cmap_cmr_r
# Function to set the legend label of an artist that uses a colormap
[docs]
def set_cmap_legend_entry(artist: "Artist", label: str) -> None:
"""
Sets the label of the provided `artist` to `label`, and creates a legend
entry using a miniature version of the colormap of `artist` as the legend
icon.
This function can be used to add legend entries for *MPL* artists that use
a colormap, like those made with :func:`~matplotlib.pyplot.hexbin`;
:func:`~matplotlib.pyplot.hist2d`; :func:`~matplotlib.pyplot.scatter`; or
any :mod:`~matplotlib.pyplot` function that takes `cmap` as an input
argument.
Keep in mind that using this function will override any legend entry that
already exists for `artist`.
Parameters
----------
artist : :obj:`~matplotlib.artist.Artist` object
Any artist object that has the `cmap` attribute, for which a legend
entry must be made using its colormap as the icon.
label : str
The string that must be set as the label of `artist`.
"""
from matplotlib.legend import Legend
# Obtain the colormap of the provided artist
cmap = getattr(artist, "cmap", None)
# If cmap is None, raise error
if cmap is None:
raise ValueError("Input argument 'artist' does not have attribute 'cmap'!")
# Set the label of this artist
artist.set_label(label)
# Add the HandlerColorPolyCollection to the default handler map for artist
from ._handlercolorpolycollection import _HandlerColorPolyCollection
Legend.get_default_handler_map()[artist] = _HandlerColorPolyCollection() # type: ignore [index]
# Function to take N equally spaced colors from a colormap
[docs]
def take_cmap_colors(
cmap: CMAP,
N: Optional[int],
*,
cmap_range: tuple[float, float] = (0, 1),
return_fmt: str = "float",
) -> RGB:
"""
Takes `N` equally spaced colors from the provided colormap `cmap` and
returns them.
Parameters
----------
cmap : str or :obj:`~matplotlib.colors.Colormap` object
The registered name of the colormap in :mod:`matplotlib.cm` or its
corresponding :obj:`~matplotlib.colors.Colormap` object.
N : int or None
The number of colors to take from the provided `cmap` within the given
`cmap_range`.
If *None*, take all colors in `cmap` within this range.
Optional
--------
cmap_range : tuple of float. Default: (0, 1)
The normalized value range in the colormap from which colors should be
taken.
By default, colors are taken from the entire colormap.
return_fmt : {'float'/'norm'; 'int'/'8bit'; 'str'/'hex'}. Default: 'float'
The format of the requested colors.
If 'float'/'norm', the colors are returned as normalized RGB tuples.
If 'int'/'8bit', the colors are returned as 8-bit RGB tuples.
If 'str'/'hex', the colors are returned using their hexadecimal string
representations.
Returns
-------
colors : list of {tuple; str}
The colors that were taken from the provided `cmap`.
Examples
--------
Taking five equally spaced colors from the 'rainforest' colormap::
>>> take_cmap_colors('cmr.rainforest', 5)
[(0.0, 0.0, 0.0),
(0.226123592, 0.124584033, 0.562997277),
(0.0548210513, 0.515835251, 0.45667819),
(0.709615979, 0.722863985, 0.0834727592),
(1.0, 1.0, 1.0)]
Requesting their 8-bit RGB values instead::
>>> take_cmap_colors('cmr.rainforest', 5, return_fmt='int')
[(0, 0, 0),
(58, 32, 144),
(14, 132, 116),
(181, 184, 21),
(255, 255, 255)]
Requesting HEX-code values instead::
>>> take_cmap_colors('cmr.rainforest', 5, return_fmt='hex')
['#000000', '#3A2090', '#0E8474', '#B5B815', '#FFFFFF']
Requesting colors in a specific range::
>>> take_cmap_colors('cmr.rainforest', 5, cmap_range=(0.2, 0.8),
return_fmt='hex')
['#3E0374', '#10528A', '#0E8474', '#5CAD3C', '#D6BF4A']
Note
----
Using this function on a perceptually uniform sequential colormap, like
those in *CMasher*, allows one to pick a number of line colors that are
different but still sequential. This is useful when plotting a set of lines
that describe the same property, but have a different initial state.
"""
# Convert provided fmt to lowercase
return_fmt = return_fmt.lower()
# Obtain the colormap
if isinstance(cmap, str):
cmap = mpl.colormaps[cmap]
# Check if provided cmap_range is valid
if not ((0 <= cmap_range[0] <= 1) and (0 <= cmap_range[1] <= 1)):
raise ValueError(
"Input argument 'cmap_range' does not contain normalized values!"
)
# Extract and convert start and stop to their integer indices (inclusive)
start = int(np.floor(cmap_range[0] * cmap.N))
stop = int(np.ceil(cmap_range[1] * cmap.N)) - 1
# Pick colors
if N is None:
index = np.arange(start, stop + 1, dtype=int)
else:
index = np.array(np.rint(np.linspace(start, stop, num=N)), dtype=int)
colors = cmap(index)
# Convert colors to proper format
if return_fmt in ("float", "norm", "int", "8bit"):
colors = np.apply_along_axis(to_rgb, 1, colors) # type: ignore [arg-type]
if return_fmt in ("int", "8bit"):
colors = np.array(np.rint(colors * 255), dtype=int)
colors = list(map(tuple, colors))
else:
colors = [to_hex(x).upper() for x in colors]
# Return colors
return colors
# Function to view what a colormap looks like
[docs]
def view_cmap(
cmap: CMAP,
*,
savefig: Optional[str] = None,
show_test: bool = False,
show_grayscale: bool = False,
) -> None:
"""
Shows a simple plot of the provided `cmap`.
Parameters
----------
cmap : str or :obj:`~matplotlib.colors.Colormap` object
The registered name of the colormap in :mod:`matplotlib.cm` or its
corresponding :obj:`~matplotlib.colors.Colormap` object.
Optional
--------
savefig : str or None. Default: None
If not *None*, the path where the plot must be saved to.
Else, the plot will simply be shown.
show_test : bool. Default: False
If *True*, show a colormap test in the plot instead.
show_grayscale : bool. Default: False
If *True*, also show the grayscale version of `cmap`.
"""
import matplotlib.pyplot as plt
if isinstance(cmap, str):
# Obtain cmap
cmap = mpl.colormaps[cmap]
# Check if show_grayscale is True
if show_grayscale:
# If so, create a colormap of cmap in grayscale
rgb = cmap(np.arange(cmap.N))[:, :3]
L = cspace_convert(rgb)[:, 0]
L /= 99.99871678
rgb_L = cmrcm.neutral(L)[:, :3]
cmap_L = LC(rgb_L)
# Set that there are two plots to create
nplots = 2
else:
# Else, there is only one plot
nplots = 1
# Create figure
fig, ax = plt.subplots(ncols=nplots, figsize=(12.8 * nplots, 3.2))
# Check if show_test is True
if show_test:
# If so, use a colormap test data file
data = np.load(path.join(path.dirname(__file__), "data/colormaptest.npy"))
else:
# If not, just plot the colormap
data = [np.linspace(0, 1, cmap.N)]
# If show_grayscale is True, show both plots instead of just one
if show_grayscale:
ax[0].imshow(data, cmap=cmap, aspect="auto")
ax[0].set_axis_off()
ax[1].imshow(data, cmap=cmap_L, aspect="auto")
ax[1].set_axis_off()
else:
ax.imshow(data, cmap=cmap, aspect="auto")
ax.set_axis_off()
# Use tight layout
plt.tight_layout(pad=0, h_pad=0, w_pad=0)
# If savefig is not None, save the figure
if savefig is not None:
plt.savefig(savefig, dpi=100, bbox_inches="tight", pad_inches=0)
plt.close(fig)
# Else, simply show it
else:
plt.show()
# %% IMPORT SCRIPT
# Import all colormaps defined in './colormaps'
import_cmaps(path.join(path.dirname(__file__), "colormaps"))