"""Helper functions for colors and colormaps."""
import numpy as np
from ..import_manager import _import_optional_dependency
def _float2int(colors):
rgb_int = (np.array(colors) * 255).astype(int)
return [tuple(c) for c in rgb_int]
def _get_valid_color(gdf, ignore_collisions=True):
"""
Return a color object if the default cmap is appropriate for the gdf.
Used in map_conversion plots.
"""
color = Color()
if ignore_collisions:
return color
else:
try:
color.match_colors_with_roughness(np.unique(gdf["z0"]))
return color
except Exception as e:
raise Exception(
"{} {}\n{}".format(
"The default colormap and norm of the class is not appropriate.",
e,
"Please specify a colormap to plot.",
)
)
[docs]
class Color:
"""Class to create and store color dictionnaries and maps.
Parameters
----------
levels: list of float, optional. Default: None
List of values marking different color bins, both min and max level should be
included. If levels is None, a default list of 15 levels from 0 to 1, plus a
level of 100 is included.
colors: list of colors (rgb-tuple or html-str), optional. Default: None
If tuple, should have 3 values, if string should have leading '#'. List should
be one less than the number of levels. If None, a default list of 15 colors that
represent the levels as roughness lengths is provided.
"""
[docs]
def __init__(self, levels=None, colors=None):
if levels is None and colors is None:
levels = [
0,
0.0002,
0.0003,
0.001,
0.005,
0.008,
0.01,
0.03,
0.05,
0.1,
0.2,
0.4,
0.5,
0.8,
1,
100,
]
colors = [
(0, 0, 255),
(255, 244, 137),
(255, 255, 255),
(197, 143, 112),
(156, 255, 151),
(167, 167, 167),
(108, 193, 75),
(213, 228, 75),
(161, 191, 11),
(114, 133, 17),
(16, 182, 19),
(255, 175, 1),
(255, 60, 21),
(5, 151, 0),
(0, 95, 9),
]
elif levels is None or colors is None:
raise Exception(
"{}{}{}".format(
"Either level or color has not been specified. ",
"To use default levels and colors do not define any of them. ",
"To use custom levels and colors please specify both of them. ",
)
)
self._levels = levels
self._colors = colors
self.cmap = None
self.norm = None
self.update(self._levels, self._colors)
@classmethod
def _from_lc(cls, landcover_table):
colors = [properties["color"] for properties in landcover_table.values()]
levels = [properties["z0"] for properties in landcover_table.values()]
levels, index = np.unique(levels, return_inverse=True)
colors = reformat_colors(np.array(colors)[index])
levels = np.concatenate((levels, [100]))
return cls(levels, colors)
[docs]
def get_color_list(self, html=False, index=None):
"""Return a formatted list of colors.
Parameters
----------
html: bool, optional. Default: False
If True, the colors will be in html format.
index: list, optional. Default: None
If not None, return only the colors at the specified indices.
"""
if index is None:
index = list(range(0, len(self._colors)))
if html:
return np.array(["%02X%02X%02X" % color for color in self._colors])[index]
else:
return [tuple(c) for c in np.array(self._colors)[index]]
[docs]
def add_color(self, lower_level, color):
"""
Add a color to the set.
Parameters
----------
color : tuple or string
0-255 RGB color or HTML format.
lower_bound: float
"""
i = np.searchsorted(self._levels, lower_level)
color = reformat_colors([color])[0]
self._levels = np.insert(self._levels, i, lower_level)
self._colors = np.insert(self._colors, i, color, axis=0)
[docs]
def update(self, levels, colors):
"""Update the ranges and colors of the object."""
if len(levels) != len(colors) + 1:
raise ValueError("The number of levels should be the number of colors + 1")
self._levels = levels
self._colors = reformat_colors(colors)
self._update_cmap()
def _update_cmap(self):
"""Return a colormap and norm using the object colors and levels."""
mplc = _import_optional_dependency("matplotlib.colors")
colors = reformat_colors(self._colors, output_type=float)
self.cmap, self.norm = mplc.from_levels_and_colors(
self._levels, colors, extend="neither"
)
[docs]
def match_colors_with_roughness(self, z0, html=False, ignore_collisions=True):
"""
Return the list of colors corresponding to the list of roughness values.
Parameters
----------
z0 : list of roughness values
html: bool, optional. Default: False
If True, the colors will be in html format.
ignore_collisions: bool, optional. Default: True
if True, will raise an error if several roughness map to the same
color.
"""
index = np.searchsorted(self._levels, z0, side="right") - 1
colors = self.get_color_list(html=html, index=index)
if not ignore_collisions:
self._check_for_duplicates(index, z0)
return colors
def _check_for_duplicates(self, index, z0, retrieve=False):
"""Raise an error if a given color is assigned to several roughness values."""
duplicate_indices = np.argwhere(np.bincount(index) > 1).flatten()
if len(duplicate_indices) > 0:
duplicate_roughness = [
np.sort(np.array(z0)[np.argwhere(index == i).flatten()])
for i in duplicate_indices
]
duplicate_messages = [
"roughness values {} assigned to color {}".format(
", ".join(duplicate_roughness[i].astype(str)),
self._colors[j],
)
for i, j in enumerate(duplicate_indices)
]
error_message = "{}\n{}".format(
"Several roughness values assigned to the same color:",
"\n".join(duplicate_messages),
)
if retrieve:
error_message += "\nYou can add level/color pairs using the method add_color or retrieve the landcover as is from the object if you wish."
raise Exception(error_message)
def _df_to_dict(df):
"""Convert a df landcover table to a dictionary."""
return {i: {key: df.loc[i, key] for key in df.columns} for i in df.index}