Source code for typhon.plots.colors.common

"""Utility functions related to plotting."""
import csv
import os
import re
from functools import lru_cache
from warnings import warn

import matplotlib.pyplot as plt
from matplotlib import colors
from matplotlib.colors import LinearSegmentedColormap
import numpy as np

from typhon.utils import deprecated

__all__ = [
    'cmap2rgba',
    'colors2cmap',
    'cmap2txt',
    'cmap2cpt',
    'cmap2act',
    'cmap2c3g',
    'cmap2ggr',
    'cmap_from_act',
    'cmap_from_txt',
    'get_material_design',
]


[docs]def cmap2rgba(cmap=None, N=None, interpolate=True): """Convert a colormap into a list of RGBA values. Parameters: cmap (str): Name of a registered colormap. N (int): Number of RGBA-values to return. If ``None`` use the number of colors defined in the colormap. interpolate (bool): Toggle the interpolation of values in the colormap. If ``False``, only values from the colormap are used. This may lead to the re-use of a color, if the colormap provides less colors than requested. If ``True``, a lookup table is used to interpolate colors (default is ``True``). Returns: ndarray: RGBA-values. Examples: >>> cmap2rgba('viridis', 5) array([[ 0.267004, 0.004874, 0.329415, 1. ], [ 0.229739, 0.322361, 0.545706, 1. ], [ 0.127568, 0.566949, 0.550556, 1. ], [ 0.369214, 0.788888, 0.382914, 1. ], [ 0.993248, 0.906157, 0.143936, 1. ]]) """ cmap = plt.get_cmap(cmap) if N is None: N = cmap.N nlut = N if interpolate else None if interpolate and isinstance(cmap, colors.ListedColormap): # `ListedColormap` does not support lookup table interpolation. cmap = colors.LinearSegmentedColormap.from_list('', cmap.colors) return cmap(np.linspace(0, 1, N)) return plt.get_cmap(cmap.name, lut=nlut)(np.linspace(0, 1, N))
def _to_hex(c): """Convert arbitray color specification to hex string.""" ctype = type(c) # Convert rgb to hex. if ctype is tuple or ctype is np.ndarray or ctype is list: return colors.rgb2hex(c) if ctype is str: # If color is already hex, simply return it. regex = re.compile('^#[A-Fa-f0-9]{6}$') if regex.match(c): return c # Convert named color to hex. return colors.cnames[c] raise Exception("Can't handle color of type: {}".format(ctype))
[docs]def colors2cmap(*args, name=None): """Create a colormap from a list of given colors. Parameters: *args: Arbitrary number of colors (Named color, HEX or RGB). name (str): Name with which the colormap is registered. Returns: LinearSegmentedColormap. Examples: >>> colors2cmap('darkorange', 'white', 'darkgreen', name='test') """ if len(args) < 2: raise Exception("Give at least two colors.") cmap_data = [_to_hex(c) for c in args] cmap = colors.LinearSegmentedColormap.from_list(name, cmap_data) plt.register_cmap(name, cmap) return cmap
[docs]def cmap2txt(cmap, filename=None, N=None, comments='%'): """Export colormap to txt file. Parameters: cmap (str): Colormap name. filename (str): Optional filename. Default: cmap + '.txt' N (int): Number of colors. comments (str): Character to start comments with. """ colors = cmap2rgba(cmap, N) header = 'Colormap "{}"'.format(cmap) if filename is None: filename = cmap + '.txt' np.savetxt(filename, colors[:, :3], fmt='%.4f', header=header, comments=comments)
[docs]def cmap2cpt(cmap, filename=None, N=None): """Export colormap to cpt file. Parameters: cmap (str): Colormap name. filename (str): Optional filename. Default: cmap + '.cpt' N (int): Number of colors. """ colors = cmap2rgba(cmap, N) header = ('# GMT palette "{}"\n' '# COLOR_MODEL = RGB\n'.format(cmap)) left = '{:>3d} {:>3d} {:>3d} {:>3d} '.format right = '{:>3d} {:>3d} {:>3d} {:>3d}\n'.format if filename is None: filename = cmap + '.cpt' with open(filename, 'w') as f: f.write(header) # For each level specify a ... for n in range(len(colors)): rgb = [int(c * 255) for c in colors[n, :3]] # ... start color ... f.write(left(n, *rgb)) # ... and end color. f.write(right(n + 1, *rgb))
[docs]def cmap2act(cmap, filename=None, N=None): """Export colormap to Adobe Color Table file. Parameters: cmap (str): Colormap name. filename (str): Optional filename. Default: cmap + '.cpt' N (int): Number of colors. """ if filename is None: filename = cmap + '.act' # If the number of color levels to export is not set... if N is None: # ... use the number of colors defined in the colormap. N = plt.get_cmap(cmap).N if N > 256: N = 256 warn('Maximum number of colors is 256.') colors = cmap2rgba(cmap, N)[:, :3] rgb = np.zeros(256 * 3 + 2) rgb[:colors.size] = (colors.flatten() * 255).astype(np.uint8) rgb[768:770] = np.uint8(N // 2**8), np.uint8(N % 2**8) rgb.astype(np.uint8).tofile(filename)
[docs]def cmap2c3g(cmap, filename=None, N=None): """Export colormap ass CSS3 gradient. Parameters: cmap (str): Colormap name. filename (str): Optional filename. Default: cmap + '.cpt' N (int): Number of colors. """ if filename is None: filename = cmap + '.c3g' colors = cmap2rgba(cmap, N) header = ( '/*' ' CSS3 Gradient "{}"\n' '*/\n\n' 'linear-gradient(\n' ' 0deg,\n' ).format(cmap) color_spec = ' rgb({:>3d},{:>3d},{:>3d}) {:>8.3%}'.format with open(filename, 'w') as f: f.write(header) ncolors = len(colors) for n in range(ncolors): r, g, b = [int(c * 255) for c in colors[n, :3]] f.write(color_spec(r, g, b, n / (ncolors - 1))) if n < ncolors - 1: f.write(',\n') f.write('\n );')
[docs]def cmap2ggr(cmap, filename=None, N=None): """Export colormap as GIMP gradient. Parameters: cmap (str): Colormap name. filename (str): Optional filename. Default: cmap + '.cpt' N (int): Number of colors. """ if filename is None: filename = cmap + '.ggr' colors = cmap2rgba(cmap, N) header = ('GIMP Gradient\n' 'Name: {}\n' '{}\n').format(cmap, len(colors) - 1) line = ('{:.6f} {:.6f} {:.6f} ' # start, middle, stop '{:.6f} {:.6f} {:.6f} {:.6f} ' # RGBA '{:.6f} {:.6f} {:.6f} {:.6f} ' # RGBA next level '0 0\n').format def idx(x): return x / (len(colors) - 1) with open(filename, 'w') as f: f.write(header) for n in range(len(colors) - 1): rgb = colors[n, :] rgb_next = colors[n + 1, :] f.write(line(idx(n), idx(n + 0.5), idx(n + 1), *rgb, *rgb_next))
[docs]def cmap_from_act(file, name=None): """Import colormap from Adobe Color Table file. Parameters: file (str): Path to act file. name (str): Colormap name. Defaults to filename without extension. Returns: LinearSegmentedColormap. """ # Extract colormap name from filename. if name is None: name = os.path.splitext(os.path.basename(file))[0] # Read binary file and determine number of colors rgb = np.fromfile(file, dtype=np.uint8) if rgb.shape[0] >= 770: ncolors = rgb[768] * 2**8 + rgb[769] else: ncolors = 256 colors = rgb[:ncolors*3].reshape(ncolors, 3) / 255 # Create and register colormap... cmap = LinearSegmentedColormap.from_list(name, colors, N=ncolors) plt.register_cmap(cmap=cmap) # Register colormap. # ... and the reversed colormap. cmap_r = LinearSegmentedColormap.from_list( name + '_r', np.flipud(colors), N=ncolors) plt.register_cmap(cmap=cmap_r) return cmap
[docs]def cmap_from_txt(file, name=None, N=-1, comments='%'): """Import colormap from txt file. Reads colormap data (RGB/RGBA) from an ASCII file. Values have to be given in [0, 1] range. Parameters: file (str): Path to txt file. name (str): Colormap name. Defaults to filename without extension. N (int): Number of colors. ``-1`` means all colors (i.e., the complete file). comments (str): Character to start comments with. Returns: LinearSegmentedColormap. """ # Extract colormap name from filename. if name is None: name = os.path.splitext(os.path.basename(file))[0] # Read binary file and determine number of colors rgb = np.genfromtxt(file, comments=comments) if N == -1: N = np.shape(rgb)[0] if np.min(rgb) < 0 or np.max(rgb) > 1: raise Exception('RGB value out of range: [0, 1].') # Create and register colormap... cmap = LinearSegmentedColormap.from_list(name, rgb, N=N) plt.register_cmap(cmap=cmap) # ... and the reversed colormap. cmap_r = LinearSegmentedColormap.from_list( name + '_r', np.flipud(rgb), N=N) plt.register_cmap(cmap=cmap_r) return cmap
[docs]@lru_cache(16) def get_material_design(name, shade=None): """Return material design colors. Parameters: name (str): Color name (e.g. 'red'). shade (str): Color shade (e.g. '500'). If ``None`` all defined shades are returned. Returns: str or list[str]: Hex RGB value or list of hex RGB values. References: https://material.io/design/color/the-color-system.html Raises: ValueError: If the specified ``name`` or ``shade`` is not defined. Examples: >>> get_material_design('red', shade='500') '#F44336' >>> get_material_design('red') ['#FFEBEE', '#FFCDD2', '#EF9A9A', '#E57373', '#EF5350', '#F44336', '#E53935', '#D32F2F', '#C62828', '#B71C1C', '#FF8A80', '#FF5252', '#FF1744', '#D50000'] """ material_source = os.path.join(os.path.dirname(__file__), 'material.csv') with open(material_source, newline='') as csvfile: reader = csv.DictReader(csvfile, delimiter=',') available_colors = [] for row in reader: available_colors.append(row['name']) if available_colors[-1] == name: available_shades = [k for k, v in row.items() if v and k != 'name'] if shade is None: return [c for c in list(row.values())[1:len(available_shades) + 1]] if shade in available_shades: return row[shade] else: raise ValueError( f'Shade "{shade}" not defined for color "{name}". ' f'Available shades are:\n{available_shades}' ) raise ValueError( f'Color "{name}" not defined. ' f'Available colors are:\n{available_colors}.' )