You are viewing the latest unreleased documentation 3.9.0.dev37. You can switch to a stable version.

Source code for iris.palette

# Copyright Iris contributors
#
# This file is part of Iris and is released under the BSD license.
# See LICENSE in the root of the repository for full licensing details.
"""Color map pallettes management.

Load, configure and register color map palettes and initialise
color map meta-data mappings.
"""

from functools import wraps
import os
import os.path
import re

import cf_units
from matplotlib import colormaps as mpl_colormaps
import matplotlib.cm as mpl_cm
import matplotlib.colors as mpl_colors
import numpy as np

import iris.config
import iris.cube

# Symmetric normalization function pivot points by SI unit.
PIVOT_BY_UNIT = {cf_units.Unit("K"): 273.15}

# Color map names by palette file metadata field value.
CMAP_BREWER = set()
_CMAP_BY_SCHEME = None
_CMAP_BY_KEYWORD = None
_CMAP_BY_STD_NAME = None

_MISSING_KWARG_CMAP = "missing kwarg cmap"
_MISSING_KWARG_NORM = "missing kwarg norm"


[docs]def is_brewer(cmap): """Determine whether the color map is a Cynthia Brewer color map. Parameters ---------- cmap : The color map instance. Returns ------- bool """ result = False if cmap is not None: result = cmap.name in CMAP_BREWER return result
def _default_cmap_norm(args, kwargs): """Inject default cmap and norm behaviour into the keyword arguments. This function injects default cmap and norm behaviour into the keyword arguments, based on the cube referenced within the positional arguments. """ cube = None # Find the single cube reference within the positional arguments. for arg in args: if isinstance(arg, iris.cube.Cube): cube = arg break # Find the keyword arguments of interest. colors = kwargs.get("colors", None) # cmap = None to disable default behaviour. cmap = kwargs.get("cmap", _MISSING_KWARG_CMAP) # norm = None to disable default behaviour. norm = kwargs.get("norm", _MISSING_KWARG_NORM) # Note that "colors" and "cmap" keywords are mutually exclusive. if colors is None and cube is not None: std_name = cube.standard_name.lower() if cube.standard_name else "" # Perform default "cmap" keyword behaviour. if cmap == _MISSING_KWARG_CMAP: # Check for an exact match against standard name. cmaps = _CMAP_BY_STD_NAME.get(std_name, set()) if len(cmaps) == 0: # Check for a fuzzy match against a keyword. for keyword in _CMAP_BY_KEYWORD.keys(): if keyword in std_name: cmaps.update(_CMAP_BY_KEYWORD[keyword]) # Add default color map to keyword arguments. if len(cmaps): cmap = sorted(cmaps, reverse=True)[0] kwargs["cmap"] = mpl_cm.get_cmap(cmap) # Perform default "norm" keyword behaviour. if norm == _MISSING_KWARG_NORM: if "anomaly" in std_name: # Determine the pivot point. pivot = PIVOT_BY_UNIT.get(cube.units, 0) norm = SymmetricNormalize(pivot) kwargs["norm"] = norm return args, kwargs
[docs]def cmap_norm(cube): """Determine the default. Determine the default :class:`matplotlib.colors.LinearSegmentedColormap` and :class:`iris.palette.SymmetricNormalize` instances associated with the cube. Parameters ---------- cube : :class:`iris.cube.Cube` Source cube to generate default palette from. Returns ------- tuple Tuple of :class:`matplotlib.colors.LinearSegmentedColormap` and :class:`iris.palette.SymmetricNormalize`. Notes ----- This function maintains laziness when called; it does not realise data. See more at :doc:`/userguide/real_and_lazy_data`. """ args, kwargs = _default_cmap_norm((cube,), {}) return kwargs.get("cmap"), kwargs.get("norm")
[docs]def auto_palette(func): """Auto palette decorator wrapper function to control the default behaviour. Decorator wrapper function to control the default behaviour of the matplotlib cmap and norm keyword arguments. Parameters ---------- func : callable Callable function to be wrapped by the decorator. Returns ------- Closure wrapper function. """ @wraps(func) def wrapper_func(*args, **kwargs): """Closure wrapper function to provide default keyword argument behaviour.""" # Update the keyword arguments with defaults. args, kwargs = _default_cmap_norm(args, kwargs) # Call the wrapped function and return its result. return func(*args, **kwargs) # Return the closure wrapper function. return wrapper_func
[docs]class SymmetricNormalize(mpl_colors.Normalize): """Provides a symmetric normalization class around a given pivot point.""" def __init__(self, pivot, *args, **kwargs): self.pivot = pivot self._vmin = None self._vmax = None mpl_colors.Normalize.__init__(self, *args, **kwargs) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self.pivot) def _update(self, val, update_min=True, update_max=True): # Update both _vmin and _vmax from given value. val_diff = np.abs(val - self.pivot) vmin_diff = np.abs(self._vmin - self.pivot) if self._vmin else 0.0 vmax_diff = np.abs(self._vmax - self.pivot) if self._vmax else 0.0 diff = max(val_diff, vmin_diff, vmax_diff) if update_min: self._vmin = self.pivot - diff if update_max: self._vmax = self.pivot + diff @property def vmin(self): return getattr(self, "_vmin") @vmin.setter def vmin(self, val): if val is None: self._vmin = None elif self._vmax is None: # Don't set _vmax, it'll stop matplotlib from giving us one. self._update(val, update_max=False) else: # Set both _vmin and _vmax from value self._update(val) @property def vmax(self): return getattr(self, "_vmax") @vmax.setter def vmax(self, val): if val is None: self._vmax = None elif self._vmin is None: # Don't set _vmin, it'll stop matplotlib from giving us one. self._update(val, update_min=False) else: # Set both _vmin and _vmax from value self._update(val)
def _load_palette(): """Load palette. Load, configure and register color map palettes and initialise color map metadata mappings. """ # Reference these module level namespace variables. global CMAP_BREWER, _CMAP_BY_SCHEME, _CMAP_BY_KEYWORD, _CMAP_BY_STD_NAME _CMAP_BY_SCHEME = {} _CMAP_BY_KEYWORD = {} _CMAP_BY_STD_NAME = {} filenames = [] # Identify all .txt color map palette files. for root, dirs, files in os.walk(iris.config.PALETTE_PATH): # Prune any .svn directory from the tree walk. if ".svn" in dirs: del dirs[dirs.index(".svn")] # Identify any target .txt color map palette files. filenames.extend( [ os.path.join(root, filename) for filename in files if os.path.splitext(filename)[1] == ".txt" ] ) for filename in filenames: # Default color map name based on the file base-name (case-SENSITIVE). cmap_name = os.path.splitext(os.path.basename(filename))[0] cmap_scheme = None cmap_keywords = [] cmap_std_names = [] cmap_type = None # Perform default color map interpolation for quantization # levels per primary color. interpolate_flag = True # Read the file header. with open(filename) as file_handle: header = filter( lambda line: re.match(r"^\s*#.*:\s+.*$", line), file_handle.readlines(), ) # Extract the file header metadata. for line in header: line = line.replace("#", "", 1).split(":") head = line[0].strip().lower() body = line[1].strip() if head == "name": # Case-SENSITIVE. cmap_name = "brewer_{}".format(body) if head == "scheme": # Case-insensitive. cmap_scheme = body.lower() if head == "keyword": # Case-insensitive. keywords = [part.strip().lower() for part in body.split(",")] cmap_keywords.extend(keywords) if head == "std_name": # Case-insensitive. std_names = [part.strip().lower() for part in body.split(",")] cmap_std_names.extend(std_names) if head == "interpolate": # Case-insensitive. interpolate_flag = body.lower() != "off" if head == "type": # Case-insensitive. cmap_type = body.lower() # Integrity check for meta-data 'type' field. assert cmap_type is not None, ( 'Missing meta-data "type" keyword for color map file, "%s"' % filename ) assert cmap_type == "rgb", 'Invalid type [%s] for color map file "%s"' % ( cmap_type, filename, ) # Update the color map look-up dictionaries. CMAP_BREWER.add(cmap_name) if cmap_scheme is not None: scheme_group = _CMAP_BY_SCHEME.setdefault(cmap_scheme, set()) scheme_group.add(cmap_name) for keyword in cmap_keywords: keyword_group = _CMAP_BY_KEYWORD.setdefault(keyword, set()) keyword_group.add(cmap_name) for std_name in cmap_std_names: std_name_group = _CMAP_BY_STD_NAME.setdefault(std_name, set()) std_name_group.add(cmap_name) # Load palette data and create the associated color map. cmap_data = np.loadtxt(filename) # Ensure to restrict the number of RGB quantization levels to # prevent color map interpolation. if interpolate_flag: # Perform default color map interpolation for quantization # levels per primary color. cmap = mpl_colors.LinearSegmentedColormap.from_list(cmap_name, cmap_data) else: # Restrict quantization levels per primary color (turn-off # interpolation). # Typically used for Brewer color maps. cmap = mpl_colors.LinearSegmentedColormap.from_list( cmap_name, cmap_data, N=len(cmap_data) ) # Register the color map for use. mpl_colormaps.register(cmap) # Ensure to load the color map palettes. _load_palette()