Source code for mpas_analysis.shared.plot.colormap

# This software is open source software available under the BSD-3 license.
#
# Copyright (c) 2022 Triad National Security, LLC. All rights reserved.
# Copyright (c) 2022 Lawrence Livermore National Security, LLC. All rights
# reserved.
# Copyright (c) 2022 UT-Battelle, LLC. All rights reserved.
#
# Additional copyright and license information can be found in the LICENSE file
# distributed with this code, or at
# https://raw.githubusercontent.com/MPAS-Dev/MPAS-Analysis/main/LICENSE
"""
Utilities for handling color maps and color bars
"""
# Authors
# -------
# Xylar Asay-Davis, Milena Veneziani, Luke Van Roekel, Greg Streletz

import matplotlib.pyplot as plt
import matplotlib.colors as cols
import numpy as np
from matplotlib.colors import LinearSegmentedColormap
import matplotlib
from xml.etree import ElementTree
import configparser
import cmocean
import pkg_resources


[docs] def setup_colormap(config, configSectionName, suffix=''): """ Set up a colormap from the registry Parameters ---------- config : instance of ConfigParser the configuration, containing a [plot] section with options that control plotting configSectionName : str name of config section suffix: str, optional suffix of colormap related options Returns ------- colormapDict : dict A dictionary of colormap information. 'colormap' specifies the name of the new colormap 'norm' is a matplotlib norm object used to normalize the colormap 'levels' is an array of contour levels or ``None`` if not using indexed color map 'ticks' is an array of values where ticks should be placed 'contours' is an array of contour values to plot or ``None`` if none have been specified 'lineWidth' is the width of contour lines or ``None`` if not specified 'lineColor' is the color of contour lines or ``None`` if not specified """ # Authors # ------- # Xylar Asay-Davis, Milena Veneziani, Greg Streletz register_custom_colormaps() option = 'colormapType{}'.format(suffix) if config.has_option(configSectionName, option): colormapType = config.get(configSectionName, option) if colormapType == 'indexed': (colormap, norm, levels, ticks) = _setup_indexed_colormap( config, configSectionName, suffix=suffix) elif colormapType == 'continuous': (colormap, norm, ticks) = _setup_colormap_and_norm( config, configSectionName, suffix=suffix) levels = None else: raise ValueError(f'config section {configSectionName} option ' f'{option} is not "indexed" or "continuous"') else: colormap = None norm = None levels = None ticks = None option = 'contourLevels{}'.format(suffix) if config.has_option(configSectionName, option): contours = config.getexpression(configSectionName, option, use_numpyfunc=True) if isinstance(contours, str) and contours == 'none': contours = None else: contours = None option = 'contourThickness{}'.format(suffix) if config.has_option(configSectionName, option): lineWidth = config.getfloat(configSectionName, option) else: lineWidth = None option = 'contourColor{}'.format(suffix) if config.has_option(configSectionName, option): lineColor = config.get(configSectionName, option) else: lineColor = None option = 'arrowsOnContour{}'.format(suffix) if config.has_option(configSectionName, option): arrows = config.getboolean(configSectionName, option) else: arrows = None return {'colormap': colormap, 'norm': norm, 'levels': levels, 'ticks': ticks, 'contours': contours, 'lineWidth': lineWidth, 'lineColor': lineColor, 'arrows': arrows}
def register_custom_colormaps(): name = 'ferret' backgroundColor = (0.9, 0.9, 0.9) red = np.array([[0, 0.6], [0.15, 1], [0.35, 1], [0.65, 0], [0.8, 0], [1, 0.75]]) green = np.array([[0, 0], [0.1, 0], [0.35, 1], [1, 0]]) blue = np.array([[0, 0], [0.5, 0], [0.9, 0.9], [1, 0.9]]) colorCount = 21 colorList = np.ones((colorCount, 4), float) colorList[:, 0] = np.interp(np.linspace(0, 1, colorCount), red[:, 0], red[:, 1]) colorList[:, 1] = np.interp(np.linspace(0, 1, colorCount), green[:, 0], green[:, 1]) colorList[:, 2] = np.interp(np.linspace(0, 1, colorCount), blue[:, 0], blue[:, 1]) colorList = colorList[::-1, :] colorMap = cols.LinearSegmentedColormap.from_list( name, colorList, N=255) colorMap.set_bad(backgroundColor) _register_colormap_and_reverse(name, colorMap) name = 'erdc_iceFire_H' colorArray = np.array([ [-1, 4.05432e-07, 0, 5.90122e-06], [-0.87451, 0, 0.120401, 0.302675], [-0.74902, 0, 0.216583, 0.524574], [-0.623529, 0.0552475, 0.345025, 0.6595], [-0.498039, 0.128047, 0.492588, 0.720288], [-0.372549, 0.188955, 0.641309, 0.792092], [-0.247059, 0.327673, 0.784935, 0.873434], [-0.121569, 0.60824, 0.892164, 0.935547], [0.00392157, 0.881371, 0.912178, 0.818099], [0.129412, 0.951407, 0.835621, 0.449279], [0.254902, 0.904481, 0.690489, 0], [0.380392, 0.85407, 0.510864, 0], [0.505882, 0.777093, 0.33018, 0.00088199], [0.631373, 0.672862, 0.139087, 0.00269398], [0.756863, 0.508815, 0, 0], [0.882353, 0.299417, 0.000366289, 0.000547829], [1, 0.0157519, 0.00332021, 4.55569e-08]], float) colorCount = 255 colorList = np.ones((colorCount, 4), float) x = colorArray[:, 0] for cIndex in range(3): colorList[:, cIndex] = np.interp( np.linspace(-1., 1., colorCount), x, colorArray[:, cIndex + 1]) colorMap = cols.LinearSegmentedColormap.from_list( name, colorList, N=255) _register_colormap_and_reverse(name, colorMap) name = 'erdc_iceFire_L' colorArray = np.array([ [-1, 0.870485, 0.913768, 0.832905], [-0.87451, 0.586919, 0.887865, 0.934003], [-0.74902, 0.31583, 0.776442, 0.867858], [-0.623529, 0.18302, 0.632034, 0.787722], [-0.498039, 0.117909, 0.484134, 0.713825], [-0.372549, 0.0507239, 0.335979, 0.654741], [-0.247059, 0, 0.209874, 0.511832], [-0.121569, 0, 0.114689, 0.28935], [0.00392157, 0.0157519, 0.00332021, 4.55569e-08], [0.129412, 0.312914, 0, 0], [0.254902, 0.520865, 0, 0], [0.380392, 0.680105, 0.15255, 0.0025996], [0.505882, 0.785109, 0.339479, 0.000797922], [0.631373, 0.857354, 0.522494, 0], [0.756863, 0.910974, 0.699774, 0], [0.882353, 0.951921, 0.842817, 0.478545], [1, 0.881371, 0.912178, 0.818099]], float) colorCount = 255 colorList = np.ones((colorCount, 4), float) x = colorArray[:, 0] for cIndex in range(3): colorList[:, cIndex] = np.interp( np.linspace(-1., 1., colorCount), x, colorArray[:, cIndex + 1]) colorMap = cols.LinearSegmentedColormap.from_list( name, colorList, N=255) _register_colormap_and_reverse(name, colorMap) name = 'BuOr' colors1 = plt.cm.PuOr(np.linspace(0., 1, 256)) colors2 = plt.cm.RdBu(np.linspace(0, 1, 256)) # combine them and build a new colormap, just the orange from the first # and the blue from the second colorList = np.vstack((colors1[0:128, :], colors2[128:256, :])) # reverse the order colorList = colorList[::-1, :] colorMap = cols.LinearSegmentedColormap.from_list(name, colorList) _register_colormap_and_reverse(name, colorMap) name = 'Maximenko' colorArray = np.array([ [-1, 0., 0.45882352941, 0.76470588235], [-0.666667, 0., 0.70196078431, 0.90588235294], [-0.333333, 0.3294117647, 0.87058823529, 1.], [0., 0.76470588235, 0.94509803921, 0.98039215686], [0.333333, 1., 1., 0.], [0.666667, 1., 0.29411764705, 0.], [1, 1., 0., 0.]], float) colorCount = 255 colorList = np.ones((colorCount, 4), float) x = colorArray[:, 0] for cIndex in range(3): colorList[:, cIndex] = np.interp( np.linspace(-1., 1., colorCount), x, colorArray[:, cIndex + 1]) colorMap = cols.LinearSegmentedColormap.from_list( name, colorList, N=255) _register_colormap_and_reverse(name, colorMap) # add the cmocean color maps map_names = list(cmocean.cm.cmapnames) # don't bother with gray (already exists, I think) map_names.pop(map_names.index('gray')) for map_name in map_names: _register_colormap_and_reverse(map_name, getattr(cmocean.cm, map_name)) # add ScientificColourMaps7 from # http://www.fabiocrameri.ch/colourmaps.php # https://doi.org/10.5281/zenodo.5501399 for map_name in ['acton', 'bam', 'bamako', 'bamO', 'batlow', 'batlowK', 'batlowW', 'berlin', 'bilbao', 'broc', 'brocO', 'buda', 'bukavu', 'cork', 'corkO', 'davos', 'devon', 'fes', 'grayC', 'hawaii', 'imola', 'lajolla', 'lapaz', 'lisbon', 'nuuk', 'oleron', 'oslo', 'roma', 'romaO', 'tofino', 'tokyo', 'turku', 'vanimo', 'vik', 'vikO']: xml_file = f'ScientificColourMaps7/{map_name}/{map_name}_PARAVIEW.xml' xml_file = pkg_resources.resource_filename(__name__, xml_file) _read_xml_colormap(xml_file, map_name) # add SciVisColor colormaps from # https://sciviscolor.org/home/colormaps/ for map_name in ['3wave-yellow-grey-blue', '3Wbgy5', '4wave-grey-red-green-mgreen', '5wave-yellow-brown-blue', 'blue-1', 'blue-3', 'blue-6', 'blue-8', 'blue-orange-div', 'brown-2', 'brown-5', 'brown-8', 'green-1', 'green-4', 'green-7', 'green-8', 'orange-5', 'orange-6', 'orange-green-blue-gray', 'purple-7', 'purple-8', 'red-1', 'red-3', 'red-4', 'yellow-1', 'yellow-7']: xml_file = f'SciVisColorColormaps/{map_name}.xml' xml_file = pkg_resources.resource_filename(__name__, xml_file) _read_xml_colormap(xml_file, map_name) name = 'white_cmo_deep' # modify cmo.deep to start at white colors2 = plt.cm.get_cmap('cmo.deep')(np.linspace(0, 1, 224)) colorCount = 32 colors1 = np.ones((colorCount, 4), float) x = np.linspace(0., 1., colorCount+1)[0:-1] white = [1., 1., 1., 1.] for cIndex in range(4): colors1[:, cIndex] = np.interp(x, [0., 1.], [white[cIndex], colors2[0, cIndex]]) colors = np.vstack((colors1, colors2)) # generating a smoothly-varying LinearSegmentedColormap cmap = LinearSegmentedColormap.from_list(name, colors) _register_colormap_and_reverse(name, cmap) def _setup_colormap_and_norm(config, configSectionName, suffix=''): """ Set up a colormap from the registry Parameters ---------- config : instance of ConfigParser the configuration, containing a [plot] section with options that control plotting configSectionName : str name of config section suffix: str, optional suffix of colormap related options Returns ------- colormap : srt new colormap norm : matplotlib.colors.Normalize the norm used to normalize the colormap ticks : array of float the tick marks on the colormap """ # Authors # ------- # Xylar Asay-Davis register_custom_colormaps() colormap = plt.get_cmap(config.get(configSectionName, 'colormapName{}'.format(suffix))) normType = config.get(configSectionName, 'normType{}'.format(suffix)) kwargs = config.getexpression(configSectionName, 'normArgs{}'.format(suffix)) if normType == 'symLog': norm = cols.SymLogNorm(**kwargs) elif normType == 'log': norm = cols.LogNorm(**kwargs) elif normType == 'linear': norm = cols.Normalize(**kwargs) else: raise ValueError('Unsupported norm type {} in section {}'.format( normType, configSectionName)) try: ticks = config.getexpression( configSectionName, 'colorbarTicks{}'.format(suffix), use_numpyfunc=True) except configparser.NoOptionError: ticks = None return colormap, norm, ticks def _setup_indexed_colormap(config, configSectionName, suffix=''): """ Set up a colormap from the registry Parameters ---------- config : instance of ConfigParser the configuration, containing a [plot] section with options that control plotting configSectionName : str name of config section suffix: str, optional suffix of colormap related options Returns ------- colormap : srt new colormap norm : matplotlib.colors.Normalize the norm used to normalize the colormap ticks : array of float the tick marks on the colormap """ # Authors # ------- # Xylar Asay-Davis, Milena Veneziani, Greg Streletz colormap = plt.get_cmap(config.get(configSectionName, 'colormapName{}'.format(suffix))) indices = config.getexpression(configSectionName, 'colormapIndices{}'.format(suffix), use_numpyfunc=True) try: levels = config.getexpression( configSectionName, 'colorbarLevels{}'.format(suffix), use_numpyfunc=True) except configparser.NoOptionError: levels = None if levels is not None: # set under/over values based on the first/last indices in the colormap underColor = colormap(indices[0]) overColor = colormap(indices[-1]) if len(levels) + 1 == len(indices): # we have 2 extra values for the under/over so make the colormap # without these values indices = indices[1:-1] elif len(levels) - 1 != len(indices): # indices list must be either one element shorter # or one element longer than colorbarLevels list raise ValueError('length mismatch between indices and ' 'colorbarLevels') colormap = cols.ListedColormap(colormap(indices), 'colormapName{}'.format(suffix)) colormap.set_under(underColor) colormap.set_over(overColor) norm = cols.BoundaryNorm(levels, colormap.N) try: ticks = config.getexpression( configSectionName, 'colorbarTicks{}'.format(suffix), use_numpyfunc=True) except configparser.NoOptionError: ticks = levels return colormap, norm, levels, ticks def _read_xml_colormap(xmlFile, map_name): """Read in an XML colormap""" xml = ElementTree.parse(xmlFile) root = xml.getroot() colormap = root.findall('ColorMap') if len(colormap) > 0: colormap = colormap[0] colorDict = {'red': [], 'green': [], 'blue': []} for point in colormap.findall('Point'): x = float(point.get('x')) color = [float(point.get('r')), float(point.get('g')), float(point.get('b'))] colorDict['red'].append((x, color[0], color[0])) colorDict['green'].append((x, color[1], color[1])) colorDict['blue'].append((x, color[2], color[2])) cmap = LinearSegmentedColormap(map_name, colorDict, 256) _register_colormap_and_reverse(map_name, cmap) def _register_colormap_and_reverse(map_name, cmap): if map_name not in matplotlib.colormaps: matplotlib.colormaps.register(cmap, name=map_name) matplotlib.colormaps.register(cmap.reversed(), name=f'{map_name}_r') def _plot_color_gradients(): """from https://matplotlib.org/tutorials/colors/colormaps.html""" cmap_list = [m for m in plt.colormaps() if not m.endswith("_r")] gradient = np.linspace(0, 1, 256) gradient = np.vstack((gradient, gradient)) nrows = len(cmap_list) fig, axes = plt.subplots(figsize=(7.2, 0.25 * nrows), nrows=nrows) fig.subplots_adjust(top=0.99, bottom=0.01, left=0.35, right=0.99) for ax, name in zip(axes, cmap_list): ax.imshow(gradient, aspect='auto', cmap=plt.get_cmap(name)) pos = list(ax.get_position().bounds) x_text = pos[0] - 0.01 y_text = pos[1] + pos[3] / 2. fig.text(x_text, y_text, name, va='center', ha='right', fontsize=10) # Turn off *all* ticks & spines, not just the ones with colormaps. for ax in axes: ax.set_axis_off() plt.savefig('colormaps.png', dpi=100)