Skip to content

Commit

Permalink
Merge pull request #20 from tpvasconcelos/lazy-colormap
Browse files Browse the repository at this point in the history
🔧  Lazy load `PLOTLY_COLORSCALES` from `colors.json`
  • Loading branch information
tpvasconcelos committed Aug 26, 2021
2 parents 925b7a0 + 21a6d7f commit e5ab481
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 94 deletions.
76 changes: 7 additions & 69 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,67 +5,34 @@
# add custom entries here...


# PyCharm ---
# exclude everything under
.idea/*
# except for...
!.idea/ridgeplot.iml


# ============================================================================
# My custom additions missing from
# https://github.com/github/gitignore (see bellow)
# ============================================================================

# Virtual environments ---
envs/
venvs/
pyvenv.cfg
pip-selfcheck.json

# poetry ---
poetry.lock

# celery ---
**/celery*.log
**/celery*.pid

# dynaconf ---
**/*.local*

# Pycharm ---
*.iml
modules.xml
*.ipr

# System files ---
Desktop.ini
Thumbs.db

# Misc ---
# TODO: what is this?
vcs.xml
# .ssh/id_rsa
id_rsa


# ============================================================================
# Templates from https://github.com/github/gitignore
# ============================================================================

# Unused templates ---
# https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# https://github.com/github/gitignore/blob/master/Swift.gitignore
# https://github.com/github/gitignore/blob/master/Ruby.gitignore
# https://github.com/github/gitignore/blob/master/Rails.gitignore
# https://github.com/github/gitignore/blob/master/R.gitignore
# https://github.com/github/gitignore/blob/master/Qt.gitignore
# https://github.com/github/gitignore/blob/master/Node.gitignore
# https://github.com/github/gitignore/blob/master/Julia.gitignore
# https://github.com/github/gitignore/blob/master/Jekyll.gitignore
# https://github.com/github/gitignore/blob/master/Go.gitignore
# https://github.com/github/gitignore/blob/master/GitBook.gitignore
# https://github.com/github/gitignore/blob/master/Fortran.gitignore
# https://github.com/github/gitignore/blob/master/Dart.gitignore
# https://github.com/github/gitignore/blob/master/CUDA.gitignore
# https://github.com/github/gitignore/blob/master/CMake.gitignore
# https://github.com/github/gitignore/blob/master/C.gitignore
# https://github.com/github/gitignore/blob/master/C%2B%2B.gitignore
# https://github.com/github/gitignore/blob/master/Terraform.gitignore

# macOS ---
# ---> https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
# General
Expand All @@ -91,27 +58,6 @@ Network Trash Folder
Temporary Items
.apdisk

# JetBrains ---
# ---> https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore
# ---> https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea/
# CMake
cmake-build-*/
# File-based project format
*.iws
# IntelliJ
out/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests


# Python ---
# ---> https://github.com/github/gitignore/blob/master/Python.gitignore
# ---> https://github.com/github/gitignore/blob/master/community/Python/JupyterNotebooks.gitignore
Expand Down Expand Up @@ -227,13 +173,6 @@ dmypy.json
.pytype/
# Cython debug symbols
cython_debug/
# Jupyter
.ipynb_checkpoints
*/.ipynb_checkpoints/*
# IPython
profile_default/
ipython_config.py


# Archives ---
# ---> https://github.com/github/gitignore/blob/master/Global/Archives.gitignore
Expand All @@ -260,7 +199,6 @@ ipython_config.py
*.dmg
*.xpi
*.gem
*.egg
*.deb
*.rpm
*.msi
Expand Down
26 changes: 26 additions & 0 deletions .idea/ridgeplot.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ This document outlines the list of changes to ridgeplot between each release. Fo
Unreleased changes
------------------

- ...
- 🔧 Implement `LazyMapping` helper to allow `ridgeplot._colors.PLOTLY_COLORSCALES` to lazy-load from
`colors.json` ([#20](https://github.com/tpvasconcelos/ridgeplot/pull/20))

0.1.14
------
Expand Down
55 changes: 39 additions & 16 deletions ridgeplot/_colors.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,51 @@
import json
from numbers import Number
from pathlib import Path
from typing import Dict, List, Union
from typing import Dict, List, 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 normalise
from ridgeplot._utils import LazyMapping, normalise
from ridgeplot.exceptions import InvalidColorscaleError

# Ideally: ColorScaleType = List[Tuple[Number, str]]
ColorScaleType = List[List[Union[Number, str]]]
ColorScaleType = List[Tuple[float, str]]
ColorScaleMappingType = Dict[str, ColorScaleType]

# Load all plotly colorscales
_path_to_colors_dict = Path(__file__).parent.joinpath("colors.json")
PLOTLY_COLORSCALES: Dict[str, ColorScaleType] = json.loads(_path_to_colors_dict.read_text())

def _colormap_loader() -> ColorScaleMappingType:
_path_to_colors_dict = Path(__file__).parent.joinpath("colors.json")
colors: dict = json.loads(_path_to_colors_dict.read_text())
for name, colorscale in colors.items():
colors[name] = [tuple(entry) for entry in colorscale]
return colors


PLOTLY_COLORSCALES = LazyMapping(loader=_colormap_loader)


def validate_colorscale(colorscale: ColorScaleType) -> None:
"""Validate the structure, scale values, and colors of colorscale.
Adapted from ``_plotly_utils.colors.validate_colorscale``, changing the
requirement that a colorscale must be a list of tuples instead of a list of
lists.
"""
if not isinstance(colorscale, list):
raise InvalidColorscaleError("A valid colorscale must be a list.")
if not all(isinstance(inner_tuple, tuple) for inner_tuple in colorscale):
raise InvalidColorscaleError("A valid colorscale must be a list.")
scale, colors = zip(*colorscale)
validate_scale_values(scale=scale)
validate_colors(colors=colors)


def _any_to_rgb(color: Union[tuple, str]) -> str:
if isinstance(color, tuple):
color = label_rgb(color)
if color.startswith("#"):
color = label_rgb(hex_to_rgb(color))
if not color.startswith("rgb("):
c: str = label_rgb(color) if isinstance(color, tuple) else color
if c.startswith("#"):
c = label_rgb(hex_to_rgb(c))
if not c.startswith("rgb("):
raise RuntimeError("Something went wrong with the logic above!")
return color
return c


def get_plotly_colorscale(name: str) -> ColorScaleType:
Expand All @@ -49,7 +72,7 @@ def get_color(colorscale: ColorScaleType, midpoint: float) -> str:
ceil = min(filter(lambda s: s > midpoint, scale))
floor = max(filter(lambda s: s < midpoint, scale))
midpoint_normalised = normalise(midpoint, min_=floor, max_=ceil)
color = find_intermediate_color(
color: str = find_intermediate_color(
lowcolor=colors[scale.index(floor)],
highcolor=colors[scale.index(ceil)],
intermed=midpoint_normalised,
Expand All @@ -58,6 +81,6 @@ def get_color(colorscale: ColorScaleType, midpoint: float) -> str:
return color


def apply_alpha(color: Union[tuple, str], alpha) -> str:
def apply_alpha(color: Union[tuple, str], alpha: float) -> str:
color = _any_to_rgb(color)
return f"rgba({color[4:-1]}, {alpha})"
2 changes: 1 addition & 1 deletion ridgeplot/_figure_factory.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from typing import Callable, Dict, List, Optional

import numpy as np
from _plotly_utils.colors import validate_colorscale
from plotly import graph_objects as go

from ridgeplot._colors import (
ColorScaleType,
apply_alpha,
get_color,
get_plotly_colorscale,
validate_colorscale,
)
from ridgeplot._utils import get_extrema_3d, normalise

Expand Down
38 changes: 35 additions & 3 deletions ridgeplot/_utils.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from typing import List, Tuple
from typing import Callable, Iterator, List, Mapping, Optional, Tuple, TypeVar

import numpy as np
import numpy.typing as npt


def get_extrema_2d(arr) -> Tuple[float, float]:
def get_extrema_2d(arr: npt.ArrayLike) -> Tuple[float, float]:
"""Calculates and returns the extrema (min, max) of a 2D (N, M) array."""
arr = np.asarray(arr).flat
return np.min(arr), np.max(arr)


def get_extrema_3d(arr: List[np.ndarray]) -> Tuple[float, float, float, float]:
def get_extrema_3d(arr: List[npt.ArrayLike]) -> Tuple[float, float, float, float]:
"""Calculates and returns the x-y extrema (x_min, x_max, y_min, y_max) of
a 3D (N, 2, M) array."""
x_min = 0
Expand All @@ -27,3 +28,34 @@ def get_extrema_3d(arr: List[np.ndarray]) -> Tuple[float, float, float, float]:
def normalise(val: float, min_: float, max_: float) -> float:
assert max_ > min_
return (val - min_) / (max_ - min_)


KT = TypeVar("KT") # Mapping key type
VT = TypeVar("VT") # Mapping value type


class LazyMapping(Mapping[KT, VT]):
def __init__(self, loader: Callable[[], Mapping[KT, VT]]):
self._loader = loader
self._inner_mapping: Optional[Mapping[KT, VT]] = None

@property
def _mapping(self) -> Mapping[KT, VT]:
if self._inner_mapping is None:
self._inner_mapping = self._loader()
return self._inner_mapping

def __getitem__(self, item: KT) -> VT:
return self._mapping.__getitem__(item)

def __iter__(self) -> Iterator[KT]:
return self._mapping.__iter__()

def __len__(self) -> int:
return self._mapping.__len__()

def __str__(self) -> str:
return self._mapping.__str__()

def __repr__(self) -> str:
return self._mapping.__repr__()
6 changes: 6 additions & 0 deletions ridgeplot/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class RidgeplotError(Exception):
"""Base ridgeplot exception."""


class InvalidColorscaleError(RidgeplotError):
"""Invalid format or type for colorscale."""
Empty file added ridgeplot/py.typed
Empty file.
13 changes: 9 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
from typing import Any, Dict

from pytest import Session


def _patch_plotly_show() -> None:
"""Monkey patch ``plotly.io.show`` as to not perform any rendering and,
instead, simply call ``plotly.io._utils.validate_coerce_fig_to_dict``"""
import plotly.io
from typing import Union

# noinspection PyProtectedMember
from plotly.io._utils import validate_coerce_fig_to_dict
import plotly.io
from plotly.graph_objs import Figure
from plotly.io._utils import validate_coerce_fig_to_dict # noqa

def wrapped(fig, renderer=None, validate=True, **kwargs):
def wrapped(
fig: Union[Figure, dict], renderer: str = None, validate: bool = True, **kwargs: Dict[str, Any]
) -> None:
validate_coerce_fig_to_dict(fig, validate)

plotly.io.show = wrapped
Expand Down
9 changes: 9 additions & 0 deletions tests/unit/test_colors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from ridgeplot._colors import PLOTLY_COLORSCALES, validate_colorscale
from ridgeplot._utils import LazyMapping


def test_plotly_colorscales() -> None:
assert isinstance(PLOTLY_COLORSCALES, LazyMapping)
for name, colorscale in PLOTLY_COLORSCALES.items():
assert isinstance(name, str)
validate_colorscale(colorscale=colorscale)

0 comments on commit e5ab481

Please sign in to comment.