import json
from pathlib import Path
from typing import Dict, Iterable, Tuple, Union
from _plotly_utils.colors import validate_colors, validate_scale_values
from plotly.colors import find_intermediate_color, hex_to_rgb, label_rgb
from ridgeplot._utils import LazyMapping, normalise_min_max
# A colorscale is an iterable (usually a list or tuple) of tuples of two
# elements:
# - the first element (a _scale value_) is a float bounded to the
# interval [0, 1]
# - the second element (a _color_) is a string representation of a
# color parsable by Plotly
#
# For instance, The Viridis colorscale would be defined as
# >>> get_colorscale("viridis")
# ... ((0.0, 'rgb(68, 1, 84)'),
# ... (0.1111111111111111, 'rgb(72, 40, 120)'),
# ... (0.2222222222222222, 'rgb(62, 73, 137)'),
# ... (0.3333333333333333, 'rgb(49, 104, 142)'),
# ... (0.4444444444444444, 'rgb(38, 130, 142)'),
# ... (0.5555555555555556, 'rgb(31, 158, 137)'),
# ... (0.6666666666666666, 'rgb(53, 183, 121)'),
# ... (0.7777777777777777, 'rgb(110, 206, 88)'),
# ... (0.8888888888888888, 'rgb(181, 222, 43)'),
# ... (1.0, 'rgb(253, 231, 37)'))
ColorScaleType = Iterable[Tuple[float, str]]
ColorScaleMappingType = Dict[str, ColorScaleType]
_PATH_TO_COLORS_JSON = Path(__file__).parent.joinpath("colors.json")
def _colormap_loader() -> ColorScaleMappingType:
colors: dict = json.loads(_PATH_TO_COLORS_JSON.read_text())
for name, colorscale in colors.items():
colors[name] = tuple(tuple(entry) for entry in colorscale)
return colors
_COLORSCALE_MAPPING = LazyMapping(loader=_colormap_loader)
def validate_colorscale(colorscale: ColorScaleType) -> None:
"""Validate the structure, scale values, and colors of colorscale.
Adapted from :py:func:`_plotly_utils.colors.validate_colorscale`.
"""
scale, colors = zip(*colorscale)
validate_scale_values(scale=scale)
validate_colors(colors=colors)
def _any_to_rgb(color: Union[str, tuple]) -> str:
if not isinstance(color, (str, tuple)):
raise TypeError(f"Expected str or tuple for color, got {type(color)} instead.")
if isinstance(color, tuple):
rgb = label_rgb(color)
elif color.startswith("#"):
rgb = label_rgb(hex_to_rgb(color))
elif color.startswith("rgb("):
rgb = str(color)
else:
raise ValueError(
f"color should be a tuple or a str representation of a hex or rgb color, got {color!r} instead."
)
validate_colors(rgb)
return rgb
[docs]def get_all_colorscale_names() -> Tuple[str]:
"""Returns a tuple with all available colorscale names."""
return tuple(_COLORSCALE_MAPPING.keys())
def get_colorscale(name: str) -> ColorScaleType:
"""Helper to get a known colorscale.
Parameters
----------
name
The colorscale name. This argument is case-insensitive. For instance,
"YlOrRd" and "ylorrd" map to the same colorscale. Colorscale names
ending in '*_r' represent to a _reversed_ colorscale.
Raises
------
:py:func:`ValueError`
If an unknown name is provided
"""
name = name.lower()
if name not in _COLORSCALE_MAPPING:
raise ValueError(
f"Could not find colorscale '{name}'. The available colorscale"
f" names are {tuple(_COLORSCALE_MAPPING.keys())}."
)
return _COLORSCALE_MAPPING[name]
def get_color(colorscale: ColorScaleType, midpoint: float) -> str:
"""Given a colorscale, it interpolates the expected color at a given
midpoint, on a scale from 0 to 1."""
if not (0 <= midpoint <= 1):
raise ValueError(f"The 'midpoint' should be a float value between 0 and 1, not {midpoint}.")
scale = [s for s, _ in colorscale]
colors = [_any_to_rgb(c) for _, c in colorscale]
del colorscale
if midpoint in scale:
return colors[scale.index(midpoint)]
ceil = min(filter(lambda s: s > midpoint, scale))
floor = max(filter(lambda s: s < midpoint, scale))
midpoint_normalised = normalise_min_max(midpoint, min_=floor, max_=ceil)
color: str = find_intermediate_color(
lowcolor=colors[scale.index(floor)],
highcolor=colors[scale.index(ceil)],
intermed=midpoint_normalised,
colortype="rgb",
)
return color
def apply_alpha(color: Union[tuple, str], alpha: float) -> str:
color = _any_to_rgb(color)
return f"rgba({color[4:-1]}, {alpha})"