Refactor Colours into its own file, make injecting optional via env var
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
# Better Colour Class for matplotlib
|
||||
|
||||
__author__ = "Cal Wing"
|
||||
__version__ = "0.0.1"
|
||||
|
||||
from cycler import cycler, Cycler
|
||||
from typing import Sequence, ItemsView, KeysView
|
||||
from functools import lru_cache
|
||||
import matplotlib.colors as colors
|
||||
|
||||
import numpy as np
|
||||
|
||||
type ValidColour = ColourValue | \
|
||||
str | \
|
||||
tuple[float, float, float, float] | \
|
||||
tuple[float, float, float]
|
||||
|
||||
class ColourValue(str):
|
||||
def __new__(
|
||||
cls,
|
||||
value: ValidColour,
|
||||
/,
|
||||
name: str | None = None
|
||||
) -> "ColourValue":
|
||||
|
||||
hex_val = colors.to_hex(value, True)
|
||||
instance = super().__new__(cls, hex_val)
|
||||
|
||||
instance._name = name
|
||||
|
||||
return instance
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if self.name:
|
||||
name = f"\"{self.name}\" "
|
||||
else:
|
||||
name = ""
|
||||
|
||||
return f"<{self.__class__.__name__}" \
|
||||
f" {name}{self} " \
|
||||
f"({self.rgba[0]:.2f}, {self.rgba[1]:.2f}, " \
|
||||
f"{self.rgba[2]:.2f}, {self.rgba[3]:.2f})>"
|
||||
|
||||
def alpha_adj(self, alpha):
|
||||
r, g, b, _ = self.rgba
|
||||
return (r, g, b, alpha)
|
||||
|
||||
@property
|
||||
@lru_cache
|
||||
def rgba(self) -> tuple[float, float, float, float]:
|
||||
return colors.to_rgba(self)
|
||||
|
||||
@property
|
||||
@lru_cache
|
||||
def rgb(self) -> tuple[float, float, float]:
|
||||
return colors.to_rgb(self)
|
||||
|
||||
@property
|
||||
@lru_cache
|
||||
def hsv(self) -> np.ndarray:
|
||||
return colors.rgb_to_hsv(self.rgb)
|
||||
|
||||
@property
|
||||
@lru_cache
|
||||
def name(self) -> str:
|
||||
if hasattr(self, "_name") and self._name is not None:
|
||||
name = self._name
|
||||
else:
|
||||
name = ""
|
||||
|
||||
assert isinstance(name, str), \
|
||||
TypeError(f"`name` should be an instance of `str` not `{type(name)}`")
|
||||
|
||||
return name
|
||||
|
||||
@property
|
||||
def hex(self) -> str:
|
||||
return self
|
||||
|
||||
|
||||
type ValidColourDicts = dict[str, str | ColourValue] | \
|
||||
dict[str, tuple[float, float, float, float]] | \
|
||||
dict[str, tuple[float, float, float]]
|
||||
|
||||
class ColourContainer:
|
||||
def __init__(
|
||||
self,
|
||||
colours: ValidColourDicts | Sequence[ColourValue],
|
||||
*,
|
||||
lookup_replacement: dict[str, str] = {" ": "_"}
|
||||
) -> None:
|
||||
|
||||
self._colours: dict[str, ColourValue] = {}
|
||||
self._lookup_replacement = lookup_replacement
|
||||
|
||||
if isinstance(colours, dict):
|
||||
for key, value in colours.items():
|
||||
assert isinstance(key, str), \
|
||||
TypeError(f"`key` must be an instance of `str` not type `{type(key)}`")
|
||||
|
||||
self._colours[key] = ColourValue(value, name=key) # ty:ignore[invalid-argument-type]
|
||||
|
||||
setattr(self, key, self._colours[key])
|
||||
|
||||
else:
|
||||
for colour in colours:
|
||||
self._colours[colour.name] = colour
|
||||
|
||||
setattr(self, colour.name, self._colours[colour.name])
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Colour List of {len(self._colours)} " \
|
||||
f"colour{'s' if len(self._colours) > 0 else ''}: " + \
|
||||
str(list(self._colours.keys()))
|
||||
|
||||
def __getitem__(self, key: str) -> ColourValue:
|
||||
for alt_char, original_char in self._lookup_replacement.items():
|
||||
if key.replace(alt_char, original_char) in self.keys():
|
||||
key = key.replace(alt_char, original_char)
|
||||
break
|
||||
|
||||
return self._colours[key]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._colours)
|
||||
|
||||
def items(self) -> ItemsView[str, ColourValue]:
|
||||
return self._colours.items()
|
||||
|
||||
def keys(self) -> KeysView[str]:
|
||||
return self._colours.keys()
|
||||
|
||||
def make_cycler(
|
||||
self,
|
||||
order: Sequence[str] | None = None,
|
||||
/,
|
||||
hex: bool = True
|
||||
) -> Cycler[str, str]:
|
||||
|
||||
cycle_order = []
|
||||
|
||||
if order is None:
|
||||
for _, colour in self.items():
|
||||
c = colour.hex if hex else colour
|
||||
cycle_order.append(c)
|
||||
else:
|
||||
for key in order:
|
||||
c = self[key].hex if hex else self[key]
|
||||
cycle_order.append(c)
|
||||
|
||||
return cycler(color=cycle_order)
|
||||
|
||||
# Generate a matplotlib colour mapping dictionary
|
||||
def mpl_colour_mapping(
|
||||
self,
|
||||
prefix: str,
|
||||
/,
|
||||
hex: bool = True,
|
||||
*,
|
||||
add_separator: bool = True
|
||||
) -> dict[str, str]:
|
||||
|
||||
mapping: dict[str, str] = {}
|
||||
|
||||
if add_separator:
|
||||
prefix += ":"
|
||||
|
||||
for name, colour in self.items():
|
||||
c = colour.hex if hex else colour
|
||||
mapping[prefix + name] = c
|
||||
|
||||
# Allow all alt names
|
||||
for alt_char, original_char in self._lookup_replacement.items():
|
||||
mapping[prefix + name.replace(alt_char, original_char)] = c
|
||||
|
||||
return mapping
|
||||
|
||||
Reference in New Issue
Block a user