From 8137ba9c68d7765734b84a399238a263205d93c1 Mon Sep 17 00:00:00 2001 From: ahuang11 Date: Mon, 16 Oct 2023 21:55:42 -0700 Subject: [PATCH] Add tbar, precommit, and cfg --- .pre-commit-config.yaml | 28 ++++ setup.cfg | 16 +++ tastymap/__init__.py | 6 +- tastymap/core.py | 38 ++--- tastymap/models.py | 153 ++++++++++---------- tests/test_core.py | 304 +++++++--------------------------------- tests/test_models.py | 299 +++++++++++++++++++++++++++++++++++++++ tests/test_utils.py | 6 +- 8 files changed, 494 insertions(+), 356 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 setup.cfg create mode 100644 tests/test_models.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f967f3a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +default_stages: [commit] +repos: + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 23.9.1 + hooks: + - id: black + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-builtin-literals + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-toml + - id: detect-private-key + - id: end-of-file-fixer + exclude: \.min\.js$ + - id: trailing-whitespace + - repo: https://github.com/codespell-project/codespell + rev: v2.2.5 + hooks: + - id: codespell + additional_dependencies: + - tomli diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..ca26cd7 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,16 @@ +[flake8] +max-line-length = 88 +extend-ignore = E203 + +[isort] +known_first_party = ahlive +multi_line_output = 3 +include_trailing_comma = True +force_grid_wrap = 0 +use_parentheses = True +ensure_newline_before_comments = True +line_length = 88 + +[doc8] +max-line-length = 88 +ignore-path = .eggs/ diff --git a/tastymap/__init__.py b/tastymap/__init__.py index a22cc87..4435d51 100644 --- a/tastymap/__init__.py +++ b/tastymap/__init__.py @@ -1,8 +1,8 @@ """colormap palettes for your palate""" -from .models import TastyColorMap, TastyColorBar -from .core import cook_tcmap, pair_tcbar +from .core import cook_tmap, pair_tbar +from .models import TastyBar, TastyMap __version__ = "0.0.0" -__all__ = ["cook_tcmap", "pair_tcbar", "TastyColorMap", "TastyColorBar"] +__all__ = ["cook_tmap", "pair_tbar", "TastyMap", "TastyBar"] diff --git a/tastymap/core.py b/tastymap/core.py index dfa4f13..55677ff 100644 --- a/tastymap/core.py +++ b/tastymap/core.py @@ -1,14 +1,14 @@ from __future__ import annotations -from typing import Any from collections.abc import Sequence +from typing import Any from matplotlib.colors import Colormap, LinearSegmentedColormap, ListedColormap -from tastymap.models import ColorModel, TastyColorMap, MatplotlibTastyColorBar +from tastymap.models import ColorModel, MatplotlibTastyBar, TastyMap -def cook_tcmap( +def cook_tmap( colors_or_cmap: Sequence | str | Colormap, num_colors: int | None = None, reverse: bool = False, @@ -17,7 +17,7 @@ def cook_tcmap( under: str | tuple | None = None, over: str | tuple | None = None, from_color_model: ColorModel | str | None = None, -) -> TastyColorMap: +) -> TastyMap: """Cook a completely new colormap or modify an existing one. Args: @@ -32,21 +32,21 @@ def cook_tcmap( colors_or_cmap is an Sequence and not hexcodes. Defaults to None. Returns: - TastyColorMap: A new TastyColorMap instance with the new colormap. + TastyMap: A new TastyMap instance with the new colormap. """ if isinstance(colors_or_cmap, str): - tmap = TastyColorMap.from_str(colors_or_cmap) + tmap = TastyMap.from_str(colors_or_cmap) elif isinstance(colors_or_cmap, LinearSegmentedColormap): - tmap = TastyColorMap(colors_or_cmap) + tmap = TastyMap(colors_or_cmap) elif isinstance(colors_or_cmap, ListedColormap): - tmap = TastyColorMap.from_listed_colormap(colors_or_cmap) + tmap = TastyMap.from_listed_colormap(colors_or_cmap) elif isinstance(colors_or_cmap, Sequence): if not isinstance(colors_or_cmap[0], str) and from_color_model is None: raise ValueError( "Please specify from_color_model to differentiate " "between RGB and HSV color models." ) - tmap = TastyColorMap.from_list( + tmap = TastyMap.from_list( colors_or_cmap, color_model=from_color_model or ColorModel.RGB ) else: @@ -71,26 +71,26 @@ def cook_tcmap( return tmap -def pair_tcbar( +def pair_tbar( plot: Any, - colors_or_cmap_or_tcmap: (Sequence | str | Colormap | TastyColorMap), + colors_or_cmap_or_tmap: (Sequence | str | Colormap | TastyMap), bounds: tuple[float, float] | Sequence[float], labels: list[str] | None = None, uniform_spacing: bool = True, - **tcbar_kwargs, + **tbar_kwargs, ): - tcmap = colors_or_cmap_or_tcmap - if not isinstance(tcmap, TastyColorMap): - tcmap = cook_tcmap(colors_or_cmap_or_tcmap) + tmap = colors_or_cmap_or_tmap + if not isinstance(tmap, TastyMap): + tmap = cook_tmap(colors_or_cmap_or_tmap) if hasattr(plot, "axes"): - tcbar = MatplotlibTastyColorBar( - tcmap, + tbar = MatplotlibTastyBar( + tmap, bounds=bounds, labels=labels, uniform_spacing=uniform_spacing, - **tcbar_kwargs, + **tbar_kwargs, ) else: raise NotImplementedError("Only matplotlib plots are supported.") - return tcbar.add_to(plot) + return tbar.add_to(plot) diff --git a/tastymap/models.py b/tastymap/models.py index 71aaa77..db4eca9 100644 --- a/tastymap/models.py +++ b/tastymap/models.py @@ -1,24 +1,24 @@ from __future__ import annotations -from abc import abstractmethod, ABC +from abc import ABC, abstractmethod from collections.abc import Generator, Sequence from enum import Enum from typing import Any, Literal -import numpy as np import matplotlib.pyplot as plt +import numpy as np from matplotlib import colormaps from matplotlib.colors import ( + BoundaryNorm, Colormap, LinearSegmentedColormap, ListedColormap, + Normalize, hsv_to_rgb, rgb2hex, rgb_to_hsv, - BoundaryNorm, - Normalize, ) -from matplotlib.ticker import FuncFormatter, Formatter +from matplotlib.ticker import FuncFormatter from .utils import cmap_to_array, get_cmap, subset_cmap @@ -39,7 +39,7 @@ class ColorModel(Enum): HEX = "hex" -class TastyColorMap: +class TastyMap: """A class to represent and manipulate colormaps in a tasty manner. Attributes: @@ -52,7 +52,7 @@ def __init__( cmap: Colormap, name: str | None = None, ): - """Initializes a TastyColorMap instance. + """Initializes a TastyMap instance. Args: cmap: The colormap to be used. @@ -69,14 +69,14 @@ def __init__( self._cmap_array = cmap_to_array(cmap) @classmethod - def from_str(cls, string: str) -> TastyColorMap: - """Creates a TastyColorMap instance from a string name. + def from_str(cls, string: str) -> TastyMap: + """Creates a TastyMap instance from a string name. Args: string: Name of the colormap. Returns: - TastyColorMap: A new TastyColorMap instance. + TastyMap: A new TastyMap instance. """ cmap = get_cmap(string) # type: ignore cmap_array = cmap_to_array(cmap) @@ -85,7 +85,7 @@ def from_str(cls, string: str) -> TastyColorMap: cmap = LinearSegmentedColormap.from_list( new_name, cmap_array, N=len(cmap_array) ) - return TastyColorMap(cmap, name=new_name) + return TastyMap(cmap, name=new_name) @classmethod def from_list( @@ -93,15 +93,15 @@ def from_list( colors: Sequence, name: str = "custom_tastymap", color_model: ColorModel | str = ColorModel.RGBA, - ) -> TastyColorMap: - """Creates a TastyColorMap instance from a list of colors. + ) -> TastyMap: + """Creates a TastyMap instance from a list of colors. Args: colors: List of colors. name: Name of the colormap. Defaults to "custom_tastymap". Returns: - TastyColorMap: A new TastyColorMap instance. + TastyMap: A new TastyMap instance. """ if not isinstance(colors, Sequence): raise TypeError(f"Expected Sequence; received {type(colors)!r}.") @@ -113,70 +113,70 @@ def from_list( cmap_array = hsv_to_rgb(cmap_array) cmap = LinearSegmentedColormap.from_list(name, cmap_array, N=len(cmap_array)) - return TastyColorMap(cmap) + return TastyMap(cmap) @classmethod def from_listed_colormap( cls, listed_colormap: ListedColormap, name: str = "custom_tastymap", - ) -> TastyColorMap: - """Creates a TastyColorMap instance from a ListedColormap. + ) -> TastyMap: + """Creates a TastyMap instance from a ListedColormap. Args: listed_colormap: The colormap to be used. name: Name of the colormap. Defaults to "custom_tastymap". Returns: - TastyColorMap: A new TastyColorMap instance. + TastyMap: A new TastyMap instance. """ return cls.from_list(listed_colormap.colors, name=name) # type: ignore - def resize(self, num_colors: int) -> TastyColorMap: + def resize(self, num_colors: int) -> TastyMap: """Resizes the colormap to a specified number of colors. Args: num_colors: Number of colors to resize to. Returns: - TastyColorMap: A new TastyColorMap instance with the interpolated colormap. + TastyMap: A new TastyMap instance with the interpolated colormap. """ cmap = LinearSegmentedColormap.from_list( self.cmap.name, self._cmap_array, N=num_colors ) # TODO: reset extremes with helper func - return TastyColorMap(cmap) + return TastyMap(cmap) - def register(self, name: str | None = None, echo: bool = True) -> TastyColorMap: + def register(self, name: str | None = None, echo: bool = True) -> TastyMap: """Registers the colormap with matplotlib. Returns: - TastyColorMap: A new TastyColorMap instance with the registered colormap. + TastyMap: A new TastyMap instance with the registered colormap. """ tmap = self.rename(name) if name else self colormaps.register(self.cmap, name=tmap.cmap.name, force=True) if echo: print( - f"Sucessfully registered the colormap; " + f"Successfully registered the colormap; " f"to use, set `cmap={tmap.cmap.name!r}` in your plot." ) return tmap - def rename(self, name: str) -> TastyColorMap: + def rename(self, name: str) -> TastyMap: """Renames the colormap. Args: name (str): New name for the colormap. Returns: - TastyColorMap: A new TastyColorMap instance with the renamed colormap. + TastyMap: A new TastyMap instance with the renamed colormap. """ - return TastyColorMap(self.cmap, name=name) + return TastyMap(self.cmap, name=name) - def reverse(self) -> TastyColorMap: + def reverse(self) -> TastyMap: """Reverses the colormap. Returns: - TastyColorMap: A new TastyColorMap instance with the reversed colormap. + TastyMap: A new TastyMap instance with the reversed colormap. """ return self[::-1] @@ -215,7 +215,7 @@ def set_extremes( bad: str | tuple | None = None, under: str | tuple | None = None, over: str | tuple | None = None, - ) -> TastyColorMap: + ) -> TastyMap: """Sets the colors for bad, underflow, and overflow values. Args: @@ -224,11 +224,11 @@ def set_extremes( over: Color for overflow values. Defaults to None. Returns: - TastyColorMap: A new TastyColorMap instance with the updated colormap. + TastyMap: A new TastyMap instance with the updated colormap. """ cmap = self.cmap.copy() cmap.set_extremes(bad=bad, under=under, over=over) # type: ignore - return TastyColorMap(cmap) + return TastyMap(cmap) def tweak_hsv( self, @@ -236,7 +236,7 @@ def tweak_hsv( saturation: float | None = None, value: float | None = None, name: str | None = None, - ) -> TastyColorMap: + ) -> TastyMap: """Tweaks the hue, saturation, and value of the colormap. Args: @@ -246,7 +246,7 @@ def tweak_hsv( name: Name of the new colormap. Returns: - TastyColorMap: A new TastyColorMap instance with the tweaked colormap. + TastyMap: A new TastyMap instance with the tweaked colormap. """ cmap_array = self._cmap_array.copy() cmap_array[:, :3] = rgb_to_hsv(cmap_array[:, :3]) @@ -267,7 +267,7 @@ def tweak_hsv( cmap = LinearSegmentedColormap.from_list( name or self.cmap.name, cmap_array, N=len(cmap_array) ) - return TastyColorMap(cmap) + return TastyMap(cmap) def __iter__(self) -> Generator[np.ndarray, None, None]: """Iterates over the colormap. @@ -277,17 +277,17 @@ def __iter__(self) -> Generator[np.ndarray, None, None]: """ yield from self._cmap_array - def __getitem__(self, indices: int | float | slice | Sequence) -> TastyColorMap: + def __getitem__(self, indices: int | float | slice | Sequence) -> TastyMap: """Gets a subset of the colormap. Args: indices: Indices to subset the colormap. Returns: - TastyColorMap: A new TastyColorMap instance with the subset colormap. + TastyMap: A new TastyMap instance with the subset colormap. """ cmap = subset_cmap(self.cmap, indices) - return TastyColorMap(cmap) + return TastyMap(cmap) def _repr_html_(self) -> str: """Returns an HTML representation of the colormap. @@ -297,122 +297,122 @@ def _repr_html_(self) -> str: """ return self.cmap._repr_html_() - def __add__(self, hue: float) -> TastyColorMap: + def __add__(self, hue: float) -> TastyMap: """Adds a hue factor to the colormap. Args: hue: Hue factor to add. Returns: - TastyColorMap: A new TastyColorMap instance with the hue + TastyMap: A new TastyMap instance with the hue added to the colormap. """ return self.tweak_hsv(hue=hue) - def __sub__(self, hue: float) -> TastyColorMap: + def __sub__(self, hue: float) -> TastyMap: """Subtracts a hue factor to the colormap. Args: hue: Hue factor to subtract. Returns: - TastyColorMap: A new TastyColorMap instance with the hue + TastyMap: A new TastyMap instance with the hue subtracted from the colormap. """ return self.tweak_hsv(hue=-hue) - def __mul__(self, saturation: float) -> TastyColorMap: + def __mul__(self, saturation: float) -> TastyMap: """Multiplies a saturation factor to the colormap. Args: saturation: Saturation factor to multiply. Returns: - TastyColorMap: A new TastyColorMap instance with the saturation + TastyMap: A new TastyMap instance with the saturation multiplied to the colormap. """ return self.tweak_hsv(saturation=saturation) - def __truediv__(self, saturation: float) -> TastyColorMap: + def __truediv__(self, saturation: float) -> TastyMap: """Divides a saturation factor to the colormap. Args: saturation: Saturation factor to divide. Returns: - TastyColorMap: A new TastyColorMap instance with the saturation + TastyMap: A new TastyMap instance with the saturation divided from the colormap. """ return self.tweak_hsv(saturation=1 / saturation) - def __pow__(self, value: float) -> TastyColorMap: + def __pow__(self, value: float) -> TastyMap: """Raises the brightness value factor to the colormap. Args: value: Brightness value factor to raise. Returns: - TastyColorMap: A new TastyColorMap instance with the brightness value + TastyMap: A new TastyMap instance with the brightness value raised to the colormap. """ return self.tweak_hsv(value=value) - def __invert__(self) -> TastyColorMap: + def __invert__(self) -> TastyMap: """Reverses the colormap. Returns: - TastyColorMap: A new TastyColorMap instance with the reversed colormap. + TastyMap: A new TastyMap instance with the reversed colormap. """ return self.reverse() - def __and__(self, tmap: TastyColorMap) -> TastyColorMap: - """Combines two TastyColorMap instances. + def __and__(self, tmap: TastyMap) -> TastyMap: + """Combines two TastyMap instances. Args: - tmap: Another TastyColorMap instance to combine with. + tmap: Another TastyMap instance to combine with. Returns: - TastyColorMap: A new TastyColorMap instance with the combined colormap. + TastyMap: A new TastyMap instance with the combined colormap. """ - if not isinstance(tmap, TastyColorMap): + if not isinstance(tmap, TastyMap): raise TypeError( - f"Can only combine TastyColorMap instances; received {type(tmap)!r}." + f"Can only combine TastyMap instances; received {type(tmap)!r}." ) name = self.cmap.name + "_" + tmap.cmap.name cmap_array = np.concatenate([self._cmap_array, cmap_to_array(tmap.cmap)]) cmap = LinearSegmentedColormap.from_list(name, cmap_array, N=len(cmap_array)) - return TastyColorMap(cmap) + return TastyMap(cmap) - def __or__(self, num_colors: int) -> TastyColorMap: + def __or__(self, num_colors: int) -> TastyMap: """Interpolates the colormap to a specified number of colors. Args: num_colors: Number of colors to resize to. Returns: - TastyColorMap: A new TastyColorMap instance with the interpolated colormap. + TastyMap: A new TastyMap instance with the interpolated colormap. """ return self.resize(num_colors) - def __lshift__(self, name: str) -> TastyColorMap: + def __lshift__(self, name: str) -> TastyMap: """Renames the colormap. Args: name: New name for the colormap. Returns: - TastyColorMap: A new TastyColorMap instance with the renamed colormap. + TastyMap: A new TastyMap instance with the renamed colormap. """ return self.rename(name) - def __rshift__(self, name: str) -> TastyColorMap: + def __rshift__(self, name: str) -> TastyMap: """Registers the colormap with matplotlib. Args: name: Name of the colormap. Returns: - TastyColorMap: A new TastyColorMap instance with the registered colormap. + TastyMap: A new TastyMap instance with the registered colormap. """ return self.register(name) @@ -437,15 +437,15 @@ def __len__(self) -> int: return len(self._cmap_array) def __eq__(self, other: Any) -> bool: - """Checks if two TastyColorMap instances are equal. + """Checks if two TastyMap instances are equal. Args: - other: Another TastyColorMap instance to compare with. + other: Another TastyMap instance to compare with. Returns: - bool: True if the two TastyColorMap instances are equal; False otherwise. + bool: True if the two TastyMap instances are equal; False otherwise. """ - if not isinstance(other, TastyColorMap): + if not isinstance(other, TastyMap): return False cmap_array = other._cmap_array return bool(np.all(self._cmap_array == cmap_array)) @@ -459,18 +459,18 @@ def __str__(self) -> str: return f"{self.cmap.name} ({len(self)} colors)" def __repr__(self) -> str: - """Returns a string representation of the TastyColorMap instance. + """Returns a string representation of the TastyMap instance. Returns: - str: String representation of the TastyColorMap instance. + str: String representation of the TastyMap instance. """ - return f"TastyColorMap({self.cmap.name!r})" + return f"TastyMap({self.cmap.name!r})" -class TastyColorBar(ABC): +class TastyBar(ABC): def __init__( self, - tmap: TastyColorMap, + tmap: TastyMap, bounds: tuple[float, float] | Sequence[float], labels: list[str] | None = None, uniform_spacing: bool = True, @@ -489,10 +489,10 @@ def add_to(self, plot: Any) -> None: """ -class MatplotlibTastyColorBar(TastyColorBar): +class MatplotlibTastyBar(TastyBar): def __init__( self, - tmap: TastyColorMap, + tmap: TastyMap, bounds: slice[float, float, float] | Sequence[float], labels: list[str] | None = None, uniform_spacing: bool = True, @@ -501,10 +501,10 @@ def __init__( clip: bool | None = None, **colorbar_kwargs: dict[str, Any], ): - """Initializes a MatplotlibTastyColorBar instance. + """Initializes a MatplotlibTastyBar instance. Args: - tmap: A TastyColorMap instance. + tmap: A TastyMap instance. bounds: Bounds for the colorbar. labels: Labels for the colorbar. Defaults to None. uniform_spacing: Whether to use uniform spacing for the colorbar. @@ -534,6 +534,7 @@ def __init__( num_ticks = min(num_colors - 1, 11) ticks = np.linspace(vmin, vmax, num_ticks) else: + provided_ticks = True ticks = np.arange(vmin, vmax + step, step) if center is None and provided_ticks: diff --git a/tests/test_core.py b/tests/test_core.py index f0dd4a2..7a95f38 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2,300 +2,94 @@ import numpy as np import pytest from matplotlib.colors import LinearSegmentedColormap, ListedColormap +from matplotlib.pyplot import get_cmap -from tastymap.models import ColorModel, TastyColorMap, cook_tcmap -from tastymap.utils import get_cmap +from tastymap.core import cook_tmap, pair_tbar +from tastymap.models import TastyMap -@pytest.fixture -def tmap(): - cmap = LinearSegmentedColormap.from_list("testcmap", ["red", "green", "blue"]) - return TastyColorMap(cmap) - - -class TestTastyMap: - def test_init(self, tmap): - assert tmap.cmap.name == "testcmap" - assert len(tmap._cmap_array) == 256 - - def test_from_str(self): - tmap = TastyColorMap.from_str("viridis") - assert tmap.cmap.name == "viridis" - - def test_from_list(self): - colors = ["red", "green", "blue"] - tmap = TastyColorMap.from_list(colors) - assert tmap.cmap.name == "custom_tastymap" - assert len(tmap._cmap_array) == 3 - - def test_from_list_hsv(self): - colors = [(0.0, 1.0, 1.0), (0.5, 1.0, 1.0), (1.0, 1.0, 1.0)] - tmap = TastyColorMap.from_list(colors, color_model="hsv") - assert tmap.cmap.name == "custom_tastymap" - assert len(tmap._cmap_array) == 3 - - def test_empty_colormap(self): - with pytest.raises(ValueError): - TastyColorMap.from_list([]) - - def test_invalid_color(self): - with pytest.raises(ValueError): - TastyColorMap.from_list(["red", "not_a_color"]) - - def test_invalid_colormap_name(self): - with pytest.raises(ValueError): - TastyColorMap.from_str("not_a_real_colormap") - - def test_non_iterable_colors(self): - with pytest.raises(TypeError): - TastyColorMap.from_list(123) - - def test_non_linearsegmented_colormap(self): - with pytest.raises(TypeError): - TastyColorMap("not_a_colormap") - - def test_interpolate(self, tmap): - interpolated = tmap.resize(10) - assert len(interpolated._cmap_array) == 10 - - def test_reverse(self, tmap): - reversed_map = tmap.reverse() - assert reversed_map._cmap_array[0].tolist() == tmap._cmap_array[-1].tolist() - - def test_to(self, tmap): - rgba_array = tmap.to_model(ColorModel.RGBA) - assert rgba_array.shape == (256, 4) - rgb_array = tmap.to_model(ColorModel.RGB) - assert rgb_array.shape == (256, 3) - hsv_array = tmap.to_model(ColorModel.HSV) - assert hsv_array.shape == (256, 3) - hex_array = tmap.to_model(ColorModel.HEX) - assert hex_array.shape == (256,) - - def test_set_bad(self, tmap): - tmap = tmap.set_extremes(bad="black", under="black", over="black") - assert tmap.cmap.get_bad().tolist() == [0.0, 0.0, 0.0, 1.0] - assert tmap.cmap.get_over().tolist() == [0.0, 0.0, 0.0, 1.0] - assert tmap.cmap.get_under().tolist() == [0.0, 0.0, 0.0, 1.0] - - def test_getitem(self, tmap): - subset = tmap[10:20] - assert len(subset._cmap_array) == 10 - - def test_and(self, tmap): - cmap2 = LinearSegmentedColormap.from_list("testcmap2", ["yellow", "cyan"]) - tmap2 = TastyColorMap(cmap2) - combined = tmap & tmap2 - assert len(combined._cmap_array) == 256 + 256 - - def test_len(self, tmap): - assert len(tmap) == 256 - - def test_str(self, tmap): - assert str(tmap) == "testcmap (256 colors)" - - def test_repr(self, tmap): - assert repr(tmap) == "TastyColorMap('testcmap')" - - def test_add_non_tastymap(self, tmap): - with pytest.raises(TypeError): - tmap + "not_a_tastymap" - - def test_unsupported_color_model(self, tmap): - with pytest.raises(ValueError): - tmap.to_model("unsupported_model") - - def test_invalid_indices(self, tmap): - with pytest.raises(IndexError): - tmap[1000:2000] - with pytest.raises(TypeError): - tmap["not_an_index"] - - def test_invalid_color_model(self, tmap): - with pytest.raises(ValueError): - tmap.to_model("not_a_real_color_model") - - def test_tweak_hue(self, tmap): - tweaked = tmap.tweak_hsv(hue=50) - assert isinstance(tweaked, TastyColorMap) - - def test_tweak_saturation(self, tmap): - tweaked = tmap.tweak_hsv(saturation=5) - assert isinstance(tweaked, TastyColorMap) - - def test_tweak_value(self, tmap): - tweaked = tmap.tweak_hsv(value=2) - assert isinstance(tweaked, TastyColorMap) - - def test_tweak_all(self, tmap): - tweaked = tmap.tweak_hsv(hue=50, saturation=5, value=2) - assert isinstance(tweaked, TastyColorMap) - - def test_tweak_edge_values(self, tmap): - tweaked_min_hue = tmap.tweak_hsv(hue=-255) - tweaked_max_hue = tmap.tweak_hsv(hue=255) - tweaked_min_saturation = tmap.tweak_hsv(saturation=-10) - tweaked_max_saturation = tmap.tweak_hsv(saturation=10) - tweaked_min_value = tmap.tweak_hsv(value=0) - tweaked_max_value = tmap.tweak_hsv(value=3) - - assert isinstance(tweaked_min_hue, TastyColorMap) - assert isinstance(tweaked_max_hue, TastyColorMap) - assert isinstance(tweaked_min_saturation, TastyColorMap) - assert isinstance(tweaked_max_saturation, TastyColorMap) - assert isinstance(tweaked_min_value, TastyColorMap) - assert isinstance(tweaked_max_value, TastyColorMap) - - def test_tweak_out_of_range(self, tmap): - with pytest.raises(ValueError): - tmap.tweak_hsv(hue=300) - with pytest.raises(ValueError): - tmap.tweak_hsv(saturation=20) - with pytest.raises(ValueError): - tmap.tweak_hsv(value=4) - - def test_empty_string(self): - with pytest.raises(ValueError): - TastyColorMap.from_str("") - - def test_non_string_non_colormap(self): - with pytest.raises(TypeError): - TastyColorMap(12345) - - def test_arithmetic_with_non_numeric(self, tmap): - with pytest.raises(TypeError): - tmap + "string" - with pytest.raises(TypeError): - tmap - "string" - with pytest.raises(TypeError): - tmap * "string" - with pytest.raises(TypeError): - tmap / "string" - - def test_from_list_empty_colors(self): - with pytest.raises(ValueError, match="Must provide at least one color."): - TastyColorMap.from_list([]) - - def test_and_operator_with_non_tastymap(self): - tmap = TastyColorMap.from_str("viridis") - with pytest.raises(TypeError): - tmap & "some_string" - - def test_cook_tcmap_iterable_without_color_model(self): - with pytest.raises(ValueError, match="Please specify from_color_model"): - cook_tcmap([(0.5, 0.5, 0.5)]) - - def test_invert(self): - tmap = TastyColorMap.from_str("viridis") - inverted = ~tmap - np.testing.assert_equal(inverted._cmap_array, tmap._cmap_array[::-1]) - - def test_pow(self): - tmap = TastyColorMap.from_str("viridis") - result = tmap**2 - assert result == tmap.tweak_hsv(value=2) - - def test_eq_operator_with_non_tastymap(self): - tmap = TastyColorMap.from_str("viridis") - assert not (tmap == "some_string") - - def test_or_operator(self): - tmap = TastyColorMap.from_str("viridis") - result = tmap | 10 - assert len(result) == 10 - - def test_lshift_operator(self): - tmap = TastyColorMap.from_str("viridis") - result = tmap << "new_name" - assert result.cmap.name == "new_name" - - def test_rshift_operator(self): - tmap = TastyColorMap.from_str("viridis") - result = tmap >> "new_name" - assert result.cmap.name == "new_name" - - def test_mod_operator(self): - tmap = TastyColorMap.from_str("viridis") - result = tmap % "rgb" - assert result.shape[1] == 3 - - def test_len_tmap(self): - tmap = TastyColorMap.from_str("viridis") - assert len(tmap) == 256 - - def test_str_tmap(self): - tmap = TastyColorMap.from_str("viridis") - assert str(tmap) == "viridis (256 colors)" - - def test_repr_tmap(self): - tmap = TastyColorMap.from_str("viridis") - assert repr(tmap) == "TastyColorMap('viridis')" - - def test_iter(self): - tmap = TastyColorMap.from_str("viridis") - for color in tmap: - assert isinstance(color, np.ndarray) - - -class TestCookCmap: +class TestCookTmap: def test_cook_from_string(self): - tmap = cook_tcmap("viridis") - assert isinstance(tmap, TastyColorMap) + tmap = cook_tmap("viridis") + assert isinstance(tmap, TastyMap) def test_cook_from_string_reversed(self): - tmap = cook_tcmap("viridis_r") - assert isinstance(tmap, TastyColorMap) + tmap = cook_tmap("viridis_r") + assert isinstance(tmap, TastyMap) # Check if the colormap is reversed by comparing the first color - assert cook_tcmap("viridis_r")[0] == cook_tcmap("viridis")[255] + assert cook_tmap("viridis_r")[0] == cook_tmap("viridis")[255] def test_cook_from_listed_colormap(self): cmap_input = ListedColormap(["red", "green", "blue"]) - tmap = cook_tcmap(cmap_input) - assert isinstance(tmap, TastyColorMap) + tmap = cook_tmap(cmap_input) + assert isinstance(tmap, TastyMap) def test_cook_from_linear_segmented_colormap(self): cmap_input = LinearSegmentedColormap.from_list( - "testcmap", ["red", "green", "blue"] + "testmap", ["red", "green", "blue"] ) - tmap = cook_tcmap(cmap_input) - assert isinstance(tmap, TastyColorMap) + tmap = cook_tmap(cmap_input) + assert isinstance(tmap, TastyMap) def test_cook_from_list(self): cmap_input = ["red", "green", "blue"] - tmap = cook_tcmap(cmap_input, num_colors=28) - assert isinstance(tmap, TastyColorMap) + tmap = cook_tmap(cmap_input, num_colors=28) + assert isinstance(tmap, TastyMap) assert len(tmap) == 28 def test_cook_from_list_no_color_model(self): cmap_input = [(0.0, 1.0, 1.0), (0.5, 1.0, 1.0), (1.0, 1.0, 1.0)] with pytest.raises(ValueError): - cook_tcmap(cmap_input) + cook_tmap(cmap_input) def test_r_flag_with_reverse_true(self): - tmap = cook_tcmap("viridis_r", reverse=True) - assert isinstance(tmap, TastyColorMap) + tmap = cook_tmap("viridis_r", reverse=True) + assert isinstance(tmap, TastyMap) assert np.all(tmap.to_model("rgba")[0] == get_cmap("viridis")(0)) def test_r_flag_with_reverse_false(self): - tmap = cook_tcmap("viridis_r", reverse=False) - assert isinstance(tmap, TastyColorMap) + tmap = cook_tmap("viridis_r", reverse=False) + assert isinstance(tmap, TastyMap) assert np.all(tmap.to_model("rgba")[0] == get_cmap("viridis")(256)) def test_with_num_colors(self): - tmap = cook_tcmap("viridis", num_colors=20) - assert isinstance(tmap, TastyColorMap) + tmap = cook_tmap("viridis", num_colors=20) + assert isinstance(tmap, TastyMap) assert len(tmap) == 20 def test_register(self): - tmap = cook_tcmap("viridis", name="test") + tmap = cook_tmap("viridis", name="test") assert plt.get_cmap("test") == tmap.cmap def test_non_iterable_input(self): with pytest.raises(TypeError): - cook_tcmap(12345) + cook_tmap(12345) def test_bad_under_over(self): - tmap = cook_tcmap("viridis", under="red", over="blue", bad="green") + tmap = cook_tmap("viridis", under="red", over="blue", bad="green") tmap.cmap.get_under().tolist() == [1.0, 0.0, 0.0, 1.0] tmap.cmap.get_over().tolist() == [0.0, 0.0, 1.0, 1.0] tmap.cmap.get_bad().tolist() == [0.0, 1.0, 0.0, 1.0] + + +class TestPairTbar: + def test_pair_tbar(self): + fig, ax = plt.subplots() + img = ax.imshow(np.random.random((10, 10))) + tmap = cook_tmap(["red", "green", "blue"]) + pair_tbar( + img, tmap, bounds=[0, 1], labels=["a", "b", "c"], uniform_spacing=False + ) + assert len(fig.axes) == 2 + + def test_pair_tbar_list(self): + fig, ax = plt.subplots() + img = ax.imshow(np.random.random((10, 10))) + pair_tbar( + img, + ["red", "green", "blue"], + bounds=[0, 1], + labels=["a", "b", "c"], + uniform_spacing=False, + ) + assert len(fig.axes) == 2 diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..e83ef9c --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,299 @@ +import matplotlib.pyplot as plt +import numpy as np +import pytest +from matplotlib.colors import BoundaryNorm, LinearSegmentedColormap, Normalize +from matplotlib.ticker import FuncFormatter + +from tastymap.models import ColorModel, MatplotlibTastyBar, TastyMap + + +@pytest.fixture +def tmap(): + cmap = LinearSegmentedColormap.from_list("testmap", ["red", "green", "blue"]) + return TastyMap(cmap) + + +class TestTastyMap: + def test_init(self, tmap): + assert tmap.cmap.name == "testmap" + assert len(tmap._cmap_array) == 256 + + def test_from_str(self): + tmap = TastyMap.from_str("viridis") + assert tmap.cmap.name == "viridis" + + def test_from_list(self): + colors = ["red", "green", "blue"] + tmap = TastyMap.from_list(colors) + assert tmap.cmap.name == "custom_tastymap" + assert len(tmap._cmap_array) == 3 + + def test_from_list_hsv(self): + colors = [(0.0, 1.0, 1.0), (0.5, 1.0, 1.0), (1.0, 1.0, 1.0)] + tmap = TastyMap.from_list(colors, color_model="hsv") + assert tmap.cmap.name == "custom_tastymap" + assert len(tmap._cmap_array) == 3 + + def test_empty_colormap(self): + with pytest.raises(ValueError): + TastyMap.from_list([]) + + def test_invalid_color(self): + with pytest.raises(ValueError): + TastyMap.from_list(["red", "not_a_color"]) + + def test_invalid_colormap_name(self): + with pytest.raises(ValueError): + TastyMap.from_str("not_a_real_colormap") + + def test_non_iterable_colors(self): + with pytest.raises(TypeError): + TastyMap.from_list(123) + + def test_non_linearsegmented_colormap(self): + with pytest.raises(TypeError): + TastyMap("not_a_colormap") + + def test_interpolate(self, tmap): + interpolated = tmap.resize(10) + assert len(interpolated._cmap_array) == 10 + + def test_reverse(self, tmap): + reversed_map = tmap.reverse() + assert reversed_map._cmap_array[0].tolist() == tmap._cmap_array[-1].tolist() + + def test_to(self, tmap): + rgba_array = tmap.to_model(ColorModel.RGBA) + assert rgba_array.shape == (256, 4) + rgb_array = tmap.to_model(ColorModel.RGB) + assert rgb_array.shape == (256, 3) + hsv_array = tmap.to_model(ColorModel.HSV) + assert hsv_array.shape == (256, 3) + hex_array = tmap.to_model(ColorModel.HEX) + assert hex_array.shape == (256,) + + def test_set_bad(self, tmap): + tmap = tmap.set_extremes(bad="black", under="black", over="black") + assert tmap.cmap.get_bad().tolist() == [0.0, 0.0, 0.0, 1.0] + assert tmap.cmap.get_over().tolist() == [0.0, 0.0, 0.0, 1.0] + assert tmap.cmap.get_under().tolist() == [0.0, 0.0, 0.0, 1.0] + + def test_getitem(self, tmap): + subset = tmap[10:20] + assert len(subset._cmap_array) == 10 + + def test_and(self, tmap): + cmap2 = LinearSegmentedColormap.from_list("testmap2", ["yellow", "cyan"]) + tmap2 = TastyMap(cmap2) + combined = tmap & tmap2 + assert len(combined._cmap_array) == 256 + 256 + + def test_len(self, tmap): + assert len(tmap) == 256 + + def test_str(self, tmap): + assert str(tmap) == "testmap (256 colors)" + + def test_repr(self, tmap): + assert repr(tmap) == "TastyMap('testmap')" + + def test_add_non_tastymap(self, tmap): + with pytest.raises(TypeError): + tmap + "not_a_tastymap" + + def test_unsupported_color_model(self, tmap): + with pytest.raises(ValueError): + tmap.to_model("unsupported_model") + + def test_invalid_indices(self, tmap): + with pytest.raises(IndexError): + tmap[1000:2000] + with pytest.raises(TypeError): + tmap["not_an_index"] + + def test_invalid_color_model(self, tmap): + with pytest.raises(ValueError): + tmap.to_model("not_a_real_color_model") + + def test_tweak_hue(self, tmap): + tweaked = tmap.tweak_hsv(hue=50) + assert isinstance(tweaked, TastyMap) + + def test_tweak_saturation(self, tmap): + tweaked = tmap.tweak_hsv(saturation=5) + assert isinstance(tweaked, TastyMap) + + def test_tweak_value(self, tmap): + tweaked = tmap.tweak_hsv(value=2) + assert isinstance(tweaked, TastyMap) + + def test_tweak_all(self, tmap): + tweaked = tmap.tweak_hsv(hue=50, saturation=5, value=2) + assert isinstance(tweaked, TastyMap) + + def test_tweak_edge_values(self, tmap): + tweaked_min_hue = tmap.tweak_hsv(hue=-255) + tweaked_max_hue = tmap.tweak_hsv(hue=255) + tweaked_min_saturation = tmap.tweak_hsv(saturation=-10) + tweaked_max_saturation = tmap.tweak_hsv(saturation=10) + tweaked_min_value = tmap.tweak_hsv(value=0) + tweaked_max_value = tmap.tweak_hsv(value=3) + + assert isinstance(tweaked_min_hue, TastyMap) + assert isinstance(tweaked_max_hue, TastyMap) + assert isinstance(tweaked_min_saturation, TastyMap) + assert isinstance(tweaked_max_saturation, TastyMap) + assert isinstance(tweaked_min_value, TastyMap) + assert isinstance(tweaked_max_value, TastyMap) + + def test_tweak_out_of_range(self, tmap): + with pytest.raises(ValueError): + tmap.tweak_hsv(hue=300) + with pytest.raises(ValueError): + tmap.tweak_hsv(saturation=20) + with pytest.raises(ValueError): + tmap.tweak_hsv(value=4) + + def test_empty_string(self): + with pytest.raises(ValueError): + TastyMap.from_str("") + + def test_non_string_non_colormap(self): + with pytest.raises(TypeError): + TastyMap(12345) + + def test_arithmetic_with_non_numeric(self, tmap): + with pytest.raises(TypeError): + tmap + "string" + with pytest.raises(TypeError): + tmap - "string" + with pytest.raises(TypeError): + tmap * "string" + with pytest.raises(TypeError): + tmap / "string" + + def test_from_list_empty_colors(self): + with pytest.raises(ValueError, match="Must provide at least one color."): + TastyMap.from_list([]) + + def test_and_operator_with_non_tastymap(self): + tmap = TastyMap.from_str("viridis") + with pytest.raises(TypeError): + tmap & "some_string" + + def test_invert(self): + tmap = TastyMap.from_str("viridis") + inverted = ~tmap + np.testing.assert_equal(inverted._cmap_array, tmap._cmap_array[::-1]) + + def test_pow(self): + tmap = TastyMap.from_str("viridis") + result = tmap**2 + assert result == tmap.tweak_hsv(value=2) + + def test_eq_operator_with_non_tastymap(self): + tmap = TastyMap.from_str("viridis") + assert not (tmap == "some_string") + + def test_or_operator(self): + tmap = TastyMap.from_str("viridis") + result = tmap | 10 + assert len(result) == 10 + + def test_lshift_operator(self): + tmap = TastyMap.from_str("viridis") + result = tmap << "new_name" + assert result.cmap.name == "new_name" + + def test_rshift_operator(self): + tmap = TastyMap.from_str("viridis") + result = tmap >> "new_name" + assert result.cmap.name == "new_name" + + def test_mod_operator(self): + tmap = TastyMap.from_str("viridis") + result = tmap % "rgb" + assert result.shape[1] == 3 + + def test_len_tmap(self): + tmap = TastyMap.from_str("viridis") + assert len(tmap) == 256 + + def test_str_tmap(self): + tmap = TastyMap.from_str("viridis") + assert str(tmap) == "viridis (256 colors)" + + def test_repr_tmap(self): + tmap = TastyMap.from_str("viridis") + assert repr(tmap) == "TastyMap('viridis')" + + def test_iter(self): + tmap = TastyMap.from_str("viridis") + for color in tmap: + assert isinstance(color, np.ndarray) + + +class TestMatplotlibTastyBar: + @pytest.fixture + def tmap(self): + cmap = LinearSegmentedColormap.from_list("testmap", ["red", "green", "blue"]) + return TastyMap(cmap) + + def test_init_provided_ticks(self, tmap): + tmap_bar = MatplotlibTastyBar(tmap, bounds=[0, 4, 18]) + np.testing.assert_equal(tmap_bar.ticks, [0, 4, 18]) + assert isinstance(tmap_bar.norm, BoundaryNorm) + assert tmap_bar.format is None + assert not tmap_bar.norm.clip + assert tmap_bar.norm.extend == "both" + + def test_init_provided_ticks_and_center(self, tmap): + tmap_bar = MatplotlibTastyBar(tmap, bounds=[0, 4, 18], center=True) + np.testing.assert_equal(tmap_bar.ticks, [0, 2.5, 11.5]) + assert isinstance(tmap_bar.norm, BoundaryNorm) + assert isinstance(tmap_bar.format, FuncFormatter) + assert not tmap_bar.norm.clip + assert tmap_bar.norm.extend == "both" + + def test_init_not_provided_ticks(self, tmap): + tmap_bar = MatplotlibTastyBar(tmap, bounds=slice(0, 18, 4)) + np.testing.assert_equal(tmap_bar.ticks, [0, 4, 8, 12, 16, 20]) + assert isinstance(tmap_bar.norm, Normalize) + assert tmap_bar.format is None + assert not tmap_bar.norm.clip + assert tmap_bar.norm.extend == "both" + assert tmap_bar.norm.vmin == 0 + assert tmap_bar.norm.vmax == 20 + + def test_init_not_provided_ticks_no_step(self, tmap): + tmap_bar = MatplotlibTastyBar(tmap, bounds=slice(0, 18)) + assert tmap_bar.ticks is None + assert isinstance(tmap_bar.norm, Normalize) + assert tmap_bar.format is None + assert not tmap_bar.norm.clip + assert tmap_bar.norm.vmin == 0 + assert tmap_bar.norm.vmax == 18 + + def test_plot_settings(self, tmap): + tmap_bar = MatplotlibTastyBar(tmap, bounds=[0, 4, 18]) + plot_settings = tmap_bar.plot_settings + assert len(plot_settings) == 2 + assert plot_settings["cmap"] == tmap.cmap + assert plot_settings["norm"] == tmap_bar.norm + + def test_colorbar_settings(self, tmap): + tmap_bar = MatplotlibTastyBar(tmap, bounds=[0, 4, 18]) + colorbar_settings = tmap_bar.colorbar_settings + assert len(colorbar_settings) == 5 + np.testing.assert_equal(colorbar_settings["ticks"], tmap_bar.ticks) + assert colorbar_settings["format"] == tmap_bar.format + assert colorbar_settings["norm"] == tmap_bar.norm + assert colorbar_settings["spacing"] == "uniform" + assert isinstance(colorbar_settings["norm"], BoundaryNorm) + + def test_add_to(self, tmap): + fig, ax = plt.subplots() + img = ax.imshow(np.random.rand(10, 10)) + tmap_bar = MatplotlibTastyBar(tmap, bounds=[0, 4, 18]) + tmap_bar.add_to(img) + assert len(fig.axes) == 2 diff --git a/tests/test_utils.py b/tests/test_utils.py index ab4e09a..e8e4b9d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,7 +5,7 @@ from tastymap.utils import cmap_to_array, get_cmap, replace_match, subset_cmap -class TestGetCmap: +class TestGetmap: def test_valid(self): cmap = get_cmap("viridis") assert isinstance(cmap, ListedColormap) @@ -42,7 +42,7 @@ def test_non_string_input(self): get_cmap(123) -class TestSubsetCmap: +class TestSubsetmap: @pytest.fixture def basic_cmap(self): return LinearSegmentedColormap.from_list( @@ -92,7 +92,7 @@ def test_invalid_iterable(self, basic_cmap): subset_cmap(basic_cmap, [0, 1000]) -class TestCmapToArray: +class TestmapToArray: def test_from_str(self): arr = cmap_to_array("viridis") assert isinstance(arr, np.ndarray)