r"""Utility functions that are used across modules for colors.
Public utility functions should be added to documentation in the '/docs/_templates/overrides/tropycal.utils.rst' file."""
import numpy as np
import matplotlib.colors as mcolors
import matplotlib as mlib
import warnings
from .generic_utils import *
# ===========================================================================================================
# Public utilities
# These are used internally and have use externally. Add these to documentation.
# ===========================================================================================================
[docs]def get_colors_sshws(wind_speed):
r"""
Retrieve the default colors for the Saffir-Simpson Hurricane Wind Scale (SSHWS).
Parameters
----------
wind_speed : int or list
Sustained wind speed in knots.
Returns
-------
str
Hex string for the corresponding color.
"""
# If category string passed, convert to wind
if isinstance(wind_speed, str):
wind_speed = category_label_to_wind(wind_speed)
# Return default SSHWS category color scale
if wind_speed < 5:
return '#FFFFFF'
elif wind_speed < 34:
return '#8FC2F2' # '#7DB7ED'
elif wind_speed < 64:
return '#3185D3'
elif wind_speed < 83:
return '#FFFF00'
elif wind_speed < 96:
return '#FF9E00'
elif wind_speed < 113:
return '#DD0000'
elif wind_speed < 137:
return '#FF00FC'
else:
return '#8B0088'
def get_colors_sshws_recon(wind_speed):
r"""
Retrieve modified colors for the Saffir-Simpson Hurricane Wind Scale (SSHWS).
Parameters
----------
wind_speed : int or list
Sustained wind speed in knots.
Returns
-------
str
Hex string for the corresponding color.
Notes
-----
These are the same colors as the default SSHWS colors, but include a special 50-64 knot category.
"""
# If category string passed, convert to wind
if isinstance(wind_speed, str):
wind_speed = category_label_to_wind(wind_speed)
# Return default SSHWS category color scale
if wind_speed < 5:
return '#FFFFFF'
elif wind_speed < 34:
return '#8FC2F2'
elif wind_speed < 50:
return '#3185D3'
elif wind_speed < 64:
return '#05539b'
elif wind_speed < 83:
return '#FFFF00'
elif wind_speed < 96:
return '#FF9E00'
elif wind_speed < 113:
return '#DD0000'
elif wind_speed < 137:
return '#FF00FC'
else:
return '#8B0088'
def make_colormap(colors, whiten=0):
z = np.array(sorted(colors.keys()))
n = len(z)
z1 = min(z)
zn = max(z)
x0 = (z - z1) / (zn - z1)
CC = mcolors.ColorConverter()
R = []
G = []
B = []
for i in range(n):
Ci = colors[z[i]]
if type(Ci) == str:
RGB = CC.to_rgb(Ci)
else:
RGB = Ci
R.append(RGB[0] + (1-RGB[0])*whiten)
G.append(RGB[1] + (1-RGB[1])*whiten)
B.append(RGB[2] + (1-RGB[2])*whiten)
cmap_dict = {}
cmap_dict['red'] = [(x0[i], R[i], R[i]) for i in range(len(R))]
cmap_dict['green'] = [(x0[i], G[i], G[i]) for i in range(len(G))]
cmap_dict['blue'] = [(x0[i], B[i], B[i]) for i in range(len(B))]
mymap = mcolors.LinearSegmentedColormap('mymap', cmap_dict)
return mymap
[docs]def get_colors_ef(colormap='default'):
r"""
Retrieve a list of colors for the Enhanced Fujita (EF) tornado scale.
Parameters
----------
colormap : str or list
Matplotlib colormap to use. Default is 'default', which uses Tropycal's default colors for the EF scale. If a list, this list must have 6 colors in order from EF0 to EF5.
Returns
-------
list or cmap
If used a matplotlib colormap, a cmap is returned, otherwise a list of colors is returned.
"""
# Matplotlib colormap
if isinstance(colormap, str) and colormap != 'default':
try:
cmap = mlib.cm.get_cmap(colormap)
norm = mlib.colors.Normalize(vmin=0, vmax=5)
colors = cmap(norm([0, 1, 2, 3, 4, 5]))
except:
# colors = [colormap]*6
raise ValueError('Colormap not found.')
# User-passed list of colors
elif isinstance(colormap, list):
if len(colormap) == 6:
colors = colormap
else:
raise ValueError(
'Must pass a list of 6 colors to correspond from EF0 to EF5.')
# Otherwise, return default colors
else:
colors = ['lightsalmon', 'tomato', 'red',
'firebrick', 'darkred', 'purple']
# Return list of colors, or colormap
return colors
[docs]def get_colors_pph(plot_type, colormap, levels=None):
r"""
Retrieve a list of colors for Practically Perfect Hindcast (PPH) for tornadoes.
Parameters
----------
plot_type : str
Plot type for PPH. Can be "daily" for single-day PPH, or "total" for multi-day PPH.
colormap : str or list
Matplotlib colormap to use. Default is 'spc', which uses Tropycal's default colors for the EF scale. If a list, this list must have 6 colors in order from EF0 to EF5.
levels : list
List of contour levels. Default is SPC intervals.
Returns
-------
colors : list or cmap
If used a matplotlib colormap, a cmap is returned, otherwise a list of colors is returned.
levels : list
List of contour levels.
"""
# Insert default levels if none specified
if levels is None:
levels = [2, 5, 10, 15, 30, 45, 60, 100]
# Default SPC colormap
if colormap == 'spc':
# Daily plot type
if plot_type == 'daily':
levels = [2, 5, 10, 15, 30, 45, 60, 100]
colors = ['#008B00', '#8B4726', '#FFC800',
'#FF0000', '#FF00FF', '#912CEE', '#104E8B']
# Multi-day plot type
else:
warnings.warn('SPC colors only allowed for daily PPH.\n' +
'Defaulting to plasma colormap.')
colormap = 'plasma'
# User-defined colormap
if colormap != 'spc':
# Matplotlib colormap
if isinstance(colormap, str):
cmap = mlib.cm.get_cmap(colormap)
norm = mlib.colors.Normalize(vmin=0, vmax=len(levels)-2)
colors = cmap(norm(np.arange(len(levels))))
# User defined list of colors
elif isinstance(colormap, list):
colors = colormap
# If a cmap is passed
else:
norm = mlib.colors.Normalize(vmin=0, vmax=len(levels)-2)
colors = colormap(norm(np.arange(len(levels))))
# Return colormap and values
return colors, levels
# ===========================================================================================================
# Private utilities
# These are primarily intended to be used internally. Do not add these to documentation.
# ===========================================================================================================
def rgb_tuple_to_str(rgb_tuple):
r"""
Convert an RGB tuple to hex string.
Parameters
----------
rgb_tuple : tuple
Tuple ordered in (r,g,b) containing integers from 0 to 255.
Returns
-------
str
Hex string for RGB value.
"""
r, g, b = rgb_tuple
r = int(r)
g = int(g)
b = int(b)
return '#%02x%02x%02x' % (r, g, b)
def get_cmap_levels(varname, colormap, levels, linear=False):
r"""
Retrieve a list of colors for Practically Perfect Hindcast (PPH) for tornadoes.
Parameters
----------
varname : str
if 'vmax', 'sfmr', or 'fl_to_sfc', then SSHWS category colors can be used.
colormap : str or list
Matplotlib colormap to use. Default is 'category', which uses Tropycal's default colors for the SSHWS scale. If a list, a colormap is generated from this list.
levels : list
List of contour levels. Default is SPC intervals.
linear : bool
If colormap is 'category', determine whether to generate a colorbar using linear category increments (True) or wind increments (False).
Returns
-------
colors : cmap
Matplotlib colormap object.
levels : list
List of contour levels.
"""
# Default SSHWS colormap
if colormap in ['category', 'category_recon']:
# Ensure variable contains some element of surface wind
if varname in ['vmax', 'sfmr', 'wspd', 'fl_to_sfc']:
# Generate contour levels
levels = [category_to_wind(c) for c in range(-1, 6)]+[200]
# Linear category increments
if linear:
colors = [mcolors.to_rgba(get_colors_sshws(lev))
for c, lev in enumerate(levels[:-1]) for _ in range(levels[c+1]-levels[c])]
# Linear wind increments
else:
if colormap == 'category':
levels = [category_to_wind(c) for c in range(-1, 6)]+[200]
colors = [get_colors_sshws(lev) for lev in levels[:-1]]
else:
levels = [category_to_wind(
c) for c in range(-1, 1)]+[50]+[category_to_wind(c) for c in range(1, 6)]+[200]
colors = [get_colors_sshws_recon(
lev) for lev in levels[:-1]]
cmap = mcolors.ListedColormap(colors)
# Otherwise, default to plasma colormap
else:
warnings.warn(
'Saffir Simpson category colors are not allowed for this variable.')
colormap = 'plasma'
# Other colormap options
if colormap not in ['category', 'category_recon']:
# Matplotlib colormap name
if isinstance(colormap, str):
cmap = mlib.cm.get_cmap(colormap)
# User defined list of colors
elif isinstance(colormap, list):
cmap = mcolors.ListedColormap(colormap)
# Dictionary
elif isinstance(colormap, dict):
cmap = make_colormap(colormap)
# ListedColormap
elif isinstance(colormap, mlib.colors.ListedColormap):
cmap = colormap
# Default to plasma
else:
cmap = mlib.cm.get_cmap('plasma')
# Normalize colors relative to levels
norm = mlib.colors.Normalize(vmin=0, vmax=len(levels)-1)
# If more than 2 levels were passed, use those for the contour levels
if len(levels) > 2:
colors = cmap(norm(np.arange(len(levels)-1)))
cmap = mcolors.ListedColormap(colors)
# Otherwise, create a list of colors based on levels
else:
colors = cmap(norm(np.linspace(0, 1, 256)))
cmap = mcolors.LinearSegmentedColormap.from_list(
'my_colormap', colors)
y0 = min(levels)
y1 = max(levels)
dy = (y1-y0)/8
scalemag = int(np.log(dy)/np.log(10))
dy_scaled = dy*10**-scalemag
dc = min([1, 2, 5, 10], key=lambda x: abs(x-dy_scaled))
dc = dc*10**scalemag
c0 = np.ceil(y0/dc)*dc
c1 = np.floor(y1/dc)*dc
levels = np.arange(c0, c1+dc, dc)
if scalemag > 0:
levels = levels.astype(int)
# Return colormap and levels
return cmap, levels