mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-06 02:30:55 -08:00
Added Color.categorical_set that generates a creates a list of visually distinct colors
This commit is contained in:
parent
f3b080e351
commit
4507d78fff
2 changed files with 296 additions and 89 deletions
|
|
@ -34,6 +34,7 @@ from __future__ import annotations
|
||||||
# other pylint warning to temp remove:
|
# other pylint warning to temp remove:
|
||||||
# too-many-arguments, too-many-locals, too-many-public-methods,
|
# too-many-arguments, too-many-locals, too-many-public-methods,
|
||||||
# too-many-statements, too-many-instance-attributes, too-many-branches
|
# too-many-statements, too-many-instance-attributes, too-many-branches
|
||||||
|
import colorsys
|
||||||
import copy as copy_module
|
import copy as copy_module
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
|
|
@ -1345,6 +1346,74 @@ class Color:
|
||||||
"""Color repr"""
|
"""Color repr"""
|
||||||
return f"Color{str(tuple(self))}"
|
return f"Color{str(tuple(self))}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def categorical_set(
|
||||||
|
cls,
|
||||||
|
color_count: int,
|
||||||
|
starting_hue: ColorLike | float = 0.0,
|
||||||
|
alpha: float | Iterable[float] = 1.0,
|
||||||
|
) -> list[Color]:
|
||||||
|
"""Generate a palette of evenly spaced colors.
|
||||||
|
|
||||||
|
Creates a list of visually distinct colors suitable for representing
|
||||||
|
discrete categories (such as different parts, assemblies, or data
|
||||||
|
series). Colors are evenly spaced around the hue circle and share
|
||||||
|
consistent lightness and saturation levels, resulting in balanced
|
||||||
|
perceptual contrast across all hues.
|
||||||
|
|
||||||
|
Produces palettes similar in appearance to the **Tableau 10** and **D3
|
||||||
|
Category10** color sets—both widely recognized standards in data
|
||||||
|
visualization for their clarity and accessibility. These values have
|
||||||
|
been empirically chosen to maintain consistent perceived brightness
|
||||||
|
across hues while avoiding overly vivid or dark colors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
color_count (int): Number of colors to generate.
|
||||||
|
starting_hue (ColorLike | float): Either a Color-like object or
|
||||||
|
a hue value in the range [0.0, 1.0] that defines the starting color.
|
||||||
|
alpha (float | Iterable[float]): Alpha value(s) for the colors. Can be a
|
||||||
|
single float or an iterable of length `color_count`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Color]: List of generated colors.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If starting_hue is out of range or alpha length mismatch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# --- Determine starting hue ---
|
||||||
|
if isinstance(starting_hue, float):
|
||||||
|
if not (0.0 <= starting_hue <= 1.0):
|
||||||
|
raise ValueError("Starting hue must be within range 0.0–1.0")
|
||||||
|
elif isinstance(starting_hue, int):
|
||||||
|
if starting_hue < 0:
|
||||||
|
raise ValueError("Starting color integer must be non-negative")
|
||||||
|
rgb = tuple(Color(starting_hue))[:3]
|
||||||
|
starting_hue = colorsys.rgb_to_hls(*rgb)[0]
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
"Starting hue must be a float in [0,1] or an integer color literal"
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Normalize alpha values ---
|
||||||
|
if isinstance(alpha, (float, int)):
|
||||||
|
alphas = [float(alpha)] * color_count
|
||||||
|
else:
|
||||||
|
alphas = list(alpha)
|
||||||
|
if len(alphas) != color_count:
|
||||||
|
raise ValueError("Number of alpha values must match color_count")
|
||||||
|
|
||||||
|
# --- Generate color list ---
|
||||||
|
hues = np.linspace(
|
||||||
|
starting_hue, starting_hue + 1.0, color_count, endpoint=False
|
||||||
|
)
|
||||||
|
colors = [
|
||||||
|
cls(*colorsys.hls_to_rgb(h % 1.0, 0.55, 0.9), a)
|
||||||
|
for h, a in zip(hues, alphas)
|
||||||
|
]
|
||||||
|
|
||||||
|
return colors
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _rgb_from_int(triplet: int) -> tuple[float, float, float]:
|
def _rgb_from_int(triplet: int) -> tuple[float, float, float]:
|
||||||
red, remainder = divmod(triplet, 256**2)
|
red, remainder = divmod(triplet, 256**2)
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,9 @@ license:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import colorsys
|
||||||
import copy
|
import copy
|
||||||
|
import math
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
@ -36,44 +37,84 @@ from build123d.geometry import Color
|
||||||
|
|
||||||
|
|
||||||
# Overloads
|
# Overloads
|
||||||
@pytest.mark.parametrize("color, expected", [
|
@pytest.mark.parametrize(
|
||||||
|
"color, expected",
|
||||||
|
[
|
||||||
pytest.param(Color("blue"), (0, 0, 1, 1), id="name"),
|
pytest.param(Color("blue"), (0, 0, 1, 1), id="name"),
|
||||||
pytest.param(Color("blue", alpha=0.5), (0, 0, 1, 0.5), id="name + kw alpha"),
|
pytest.param(Color("blue", alpha=0.5), (0, 0, 1, 0.5), id="name + kw alpha"),
|
||||||
pytest.param(Color("blue", 0.5), (0, 0, 1, 0.5), id="name + alpha"),
|
pytest.param(Color("blue", 0.5), (0, 0, 1, 0.5), id="name + alpha"),
|
||||||
])
|
],
|
||||||
|
)
|
||||||
def test_overload_name(color, expected):
|
def test_overload_name(color, expected):
|
||||||
np.testing.assert_allclose(tuple(color), expected, 1e-5)
|
np.testing.assert_allclose(tuple(color), expected, 1e-5)
|
||||||
|
|
||||||
@pytest.mark.parametrize("color, expected", [
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"color, expected",
|
||||||
|
[
|
||||||
pytest.param(Color(0.0, 1.0, 0.0), (0, 1, 0, 1), id="rgb"),
|
pytest.param(Color(0.0, 1.0, 0.0), (0, 1, 0, 1), id="rgb"),
|
||||||
pytest.param(Color(1.0, 1.0, 0.0, 0.5), (1, 1, 0, 0.5), id="rgba"),
|
pytest.param(Color(1.0, 1.0, 0.0, 0.5), (1, 1, 0, 0.5), id="rgba"),
|
||||||
pytest.param(Color(1.0, 1.0, 0.0, alpha=0.5), (1, 1, 0, 0.5), id="rgb + kw alpha"),
|
pytest.param(
|
||||||
pytest.param(Color(red=0.1, green=0.2, blue=0.3, alpha=0.5), (0.1, 0.2, 0.3, 0.5), id="kw rgba"),
|
Color(1.0, 1.0, 0.0, alpha=0.5), (1, 1, 0, 0.5), id="rgb + kw alpha"
|
||||||
])
|
),
|
||||||
|
pytest.param(
|
||||||
|
Color(red=0.1, green=0.2, blue=0.3, alpha=0.5),
|
||||||
|
(0.1, 0.2, 0.3, 0.5),
|
||||||
|
id="kw rgba",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_overload_rgba(color, expected):
|
def test_overload_rgba(color, expected):
|
||||||
np.testing.assert_allclose(tuple(color), expected, 1e-5)
|
np.testing.assert_allclose(tuple(color), expected, 1e-5)
|
||||||
|
|
||||||
@pytest.mark.parametrize("color, expected", [
|
|
||||||
pytest.param(Color(0x996692), (0x99 / 0xFF, 0x66 / 0xFF, 0x92 / 0xFF, 1), id="color_code"),
|
@pytest.mark.parametrize(
|
||||||
pytest.param(Color(0x006692, 0x80), (0, 0x66 / 0xFF, 0x92 / 0xFF, 0x80 / 0xFF), id="color_code + alpha"),
|
"color, expected",
|
||||||
pytest.param(Color(0x006692, alpha=0x80), (0, 102 / 255, 146 / 255, 128 / 255), id="color_code + kw alpha"),
|
[
|
||||||
pytest.param(Color(color_code=0x996692, alpha=0xCC), (153 / 255, 102 / 255, 146 / 255, 204 / 255), id="kw color_code + alpha"),
|
pytest.param(
|
||||||
])
|
Color(0x996692), (0x99 / 0xFF, 0x66 / 0xFF, 0x92 / 0xFF, 1), id="color_code"
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
Color(0x006692, 0x80),
|
||||||
|
(0, 0x66 / 0xFF, 0x92 / 0xFF, 0x80 / 0xFF),
|
||||||
|
id="color_code + alpha",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
Color(0x006692, alpha=0x80),
|
||||||
|
(0, 102 / 255, 146 / 255, 128 / 255),
|
||||||
|
id="color_code + kw alpha",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
Color(color_code=0x996692, alpha=0xCC),
|
||||||
|
(153 / 255, 102 / 255, 146 / 255, 204 / 255),
|
||||||
|
id="kw color_code + alpha",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_overload_hex(color, expected):
|
def test_overload_hex(color, expected):
|
||||||
np.testing.assert_allclose(tuple(color), expected, 1e-5)
|
np.testing.assert_allclose(tuple(color), expected, 1e-5)
|
||||||
|
|
||||||
@pytest.mark.parametrize("color, expected", [
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"color, expected",
|
||||||
|
[
|
||||||
pytest.param(Color((0.1,)), (0.1, 1.0, 1.0, 1.0), id="tuple r"),
|
pytest.param(Color((0.1,)), (0.1, 1.0, 1.0, 1.0), id="tuple r"),
|
||||||
pytest.param(Color((0.1, 0.2)), (0.1, 0.2, 1.0, 1.0), id="tuple rg"),
|
pytest.param(Color((0.1, 0.2)), (0.1, 0.2, 1.0, 1.0), id="tuple rg"),
|
||||||
pytest.param(Color((0.1, 0.2, 0.3)), (0.1, 0.2, 0.3, 1.0), id="tuple rgb"),
|
pytest.param(Color((0.1, 0.2, 0.3)), (0.1, 0.2, 0.3, 1.0), id="tuple rgb"),
|
||||||
pytest.param(Color((0.1, 0.2, 0.3, 0.4)), (0.1, 0.2, 0.3, 0.4), id="tuple rbga"),
|
pytest.param(
|
||||||
|
Color((0.1, 0.2, 0.3, 0.4)), (0.1, 0.2, 0.3, 0.4), id="tuple rbga"
|
||||||
|
),
|
||||||
pytest.param(Color((0.1, 0.2, 0.3, 0.4)), (0.1, 0.2, 0.3, 0.4), id="kw tuple"),
|
pytest.param(Color((0.1, 0.2, 0.3, 0.4)), (0.1, 0.2, 0.3, 0.4), id="kw tuple"),
|
||||||
])
|
],
|
||||||
|
)
|
||||||
def test_overload_tuple(color, expected):
|
def test_overload_tuple(color, expected):
|
||||||
np.testing.assert_allclose(tuple(color), expected, 1e-5)
|
np.testing.assert_allclose(tuple(color), expected, 1e-5)
|
||||||
|
|
||||||
|
|
||||||
# ColorLikes
|
# ColorLikes
|
||||||
@pytest.mark.parametrize("color_like", [
|
@pytest.mark.parametrize(
|
||||||
|
"color_like",
|
||||||
|
[
|
||||||
pytest.param(Quantity_ColorRGBA(1, 0, 0, 1), id="Quantity_ColorRGBA"),
|
pytest.param(Quantity_ColorRGBA(1, 0, 0, 1), id="Quantity_ColorRGBA"),
|
||||||
pytest.param("red", id="name str"),
|
pytest.param("red", id="name str"),
|
||||||
pytest.param("red ", id="name str whitespace"),
|
pytest.param("red ", id="name str whitespace"),
|
||||||
|
|
@ -86,7 +127,9 @@ def test_overload_tuple(color, expected):
|
||||||
pytest.param("#ff0000ff", id="hex str rgba 24bit"),
|
pytest.param("#ff0000ff", id="hex str rgba 24bit"),
|
||||||
pytest.param(" #ff0000ff ", id="hex str rgba 24bit whitespace"),
|
pytest.param(" #ff0000ff ", id="hex str rgba 24bit whitespace"),
|
||||||
pytest.param(("#ff0000ff",), id="tuple hex str rgba 24bit"),
|
pytest.param(("#ff0000ff",), id="tuple hex str rgba 24bit"),
|
||||||
pytest.param(("#ff0000ff", .6), id="tuple hex str rgba 24bit + alpha (not used)"),
|
pytest.param(
|
||||||
|
("#ff0000ff", 0.6), id="tuple hex str rgba 24bit + alpha (not used)"
|
||||||
|
),
|
||||||
pytest.param("#f00", id="hex str rgb 12bit"),
|
pytest.param("#f00", id="hex str rgb 12bit"),
|
||||||
pytest.param(" #f00 ", id="hex str rgb 12bit whitespace"),
|
pytest.param(" #f00 ", id="hex str rgb 12bit whitespace"),
|
||||||
pytest.param(("#f00",), id="tuple hex str rgb 12bit"),
|
pytest.param(("#f00",), id="tuple hex str rgb 12bit"),
|
||||||
|
|
@ -94,47 +137,59 @@ def test_overload_tuple(color, expected):
|
||||||
pytest.param("#f00f", id="hex str rgba 12bit"),
|
pytest.param("#f00f", id="hex str rgba 12bit"),
|
||||||
pytest.param(" #f00f ", id="hex str rgba 12bit whitespace"),
|
pytest.param(" #f00f ", id="hex str rgba 12bit whitespace"),
|
||||||
pytest.param(("#f00f",), id="tuple hex str rgba 12bit"),
|
pytest.param(("#f00f",), id="tuple hex str rgba 12bit"),
|
||||||
pytest.param(("#f00f", .6), id="tuple hex str rgba 12bit + alpha (not used)"),
|
pytest.param(("#f00f", 0.6), id="tuple hex str rgba 12bit + alpha (not used)"),
|
||||||
pytest.param(0xff0000, id="hex int"),
|
pytest.param(0xFF0000, id="hex int"),
|
||||||
pytest.param((0xff0000), id="tuple hex int"),
|
pytest.param((0xFF0000), id="tuple hex int"),
|
||||||
pytest.param((0xff0000, 0xff), id="tuple hex int + alpha"),
|
pytest.param((0xFF0000, 0xFF), id="tuple hex int + alpha"),
|
||||||
pytest.param((1, 0, 0), id="tuple rgb int"),
|
pytest.param((1, 0, 0), id="tuple rgb int"),
|
||||||
pytest.param((1, 0, 0, 1), id="tuple rgba int"),
|
pytest.param((1, 0, 0, 1), id="tuple rgba int"),
|
||||||
pytest.param((1., 0., 0.), id="tuple rgb float"),
|
pytest.param((1.0, 0.0, 0.0), id="tuple rgb float"),
|
||||||
pytest.param((1., 0., 0., 1.), id="tuple rgba float"),
|
pytest.param((1.0, 0.0, 0.0, 1.0), id="tuple rgba float"),
|
||||||
])
|
],
|
||||||
|
)
|
||||||
def test_color_likes(color_like):
|
def test_color_likes(color_like):
|
||||||
expected = (1, 0, 0, 1)
|
expected = (1, 0, 0, 1)
|
||||||
np.testing.assert_allclose(tuple(Color(color_like)), expected, 1e-5)
|
np.testing.assert_allclose(tuple(Color(color_like)), expected, 1e-5)
|
||||||
np.testing.assert_allclose(tuple(Color(color_like=color_like)), expected, 1e-5)
|
np.testing.assert_allclose(tuple(Color(color_like=color_like)), expected, 1e-5)
|
||||||
|
|
||||||
@pytest.mark.parametrize("color_like, expected", [
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"color_like, expected",
|
||||||
|
[
|
||||||
pytest.param(Color(), (1, 1, 1, 1), id="empty Color()"),
|
pytest.param(Color(), (1, 1, 1, 1), id="empty Color()"),
|
||||||
pytest.param(1., (1, 1, 1, 1), id="r float"),
|
pytest.param(1.0, (1, 1, 1, 1), id="r float"),
|
||||||
pytest.param((1.,), (1, 1, 1, 1), id="tuple r float"),
|
pytest.param((1.0,), (1, 1, 1, 1), id="tuple r float"),
|
||||||
pytest.param((1., 0.), (1, 0, 1, 1), id="tuple rg float"),
|
pytest.param((1.0, 0.0), (1, 0, 1, 1), id="tuple rg float"),
|
||||||
])
|
],
|
||||||
|
)
|
||||||
def test_color_likes_incomplete(color_like, expected):
|
def test_color_likes_incomplete(color_like, expected):
|
||||||
np.testing.assert_allclose(tuple(Color(color_like)), expected, 1e-5)
|
np.testing.assert_allclose(tuple(Color(color_like)), expected, 1e-5)
|
||||||
np.testing.assert_allclose(tuple(Color(color_like=color_like)), expected, 1e-5)
|
np.testing.assert_allclose(tuple(Color(color_like=color_like)), expected, 1e-5)
|
||||||
|
|
||||||
@pytest.mark.parametrize("color_like", [
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"color_like",
|
||||||
|
[
|
||||||
pytest.param(Quantity_ColorRGBA(1, 0, 0, 0.6), id="Quantity_ColorRGBA"),
|
pytest.param(Quantity_ColorRGBA(1, 0, 0, 0.6), id="Quantity_ColorRGBA"),
|
||||||
pytest.param(("red", 0.6), id="tuple name str + alpha"),
|
pytest.param(("red", 0.6), id="tuple name str + alpha"),
|
||||||
pytest.param(("#ff0000", 0.6), id="tuple hex str rgb 24bit + alpha"),
|
pytest.param(("#ff0000", 0.6), id="tuple hex str rgb 24bit + alpha"),
|
||||||
pytest.param(("#ff000099"), id="tuple hex str rgba 24bit"),
|
pytest.param(("#ff000099"), id="tuple hex str rgba 24bit"),
|
||||||
pytest.param(("#f00", 0.6), id="tuple hex str rgb 12bit + alpha"),
|
pytest.param(("#f00", 0.6), id="tuple hex str rgb 12bit + alpha"),
|
||||||
pytest.param(("#f009"), id="tuple hex str rgba 12bit"),
|
pytest.param(("#f009"), id="tuple hex str rgba 12bit"),
|
||||||
pytest.param((0xff0000, 153), id="tuple hex int + alpha int"),
|
pytest.param((0xFF0000, 153), id="tuple hex int + alpha int"),
|
||||||
pytest.param((1., 0., 0., 0.6), id="tuple rbga float"),
|
pytest.param((1.0, 0.0, 0.0, 0.6), id="tuple rbga float"),
|
||||||
])
|
],
|
||||||
|
)
|
||||||
def test_color_likes_alpha(color_like):
|
def test_color_likes_alpha(color_like):
|
||||||
expected = (1, 0, 0, 0.6)
|
expected = (1, 0, 0, 0.6)
|
||||||
np.testing.assert_allclose(tuple(Color(color_like)), expected, 1e-5)
|
np.testing.assert_allclose(tuple(Color(color_like)), expected, 1e-5)
|
||||||
np.testing.assert_allclose(tuple(Color(color_like=color_like)), expected, 1e-5)
|
np.testing.assert_allclose(tuple(Color(color_like=color_like)), expected, 1e-5)
|
||||||
|
|
||||||
|
|
||||||
# Exceptions
|
# Exceptions
|
||||||
@pytest.mark.parametrize("name", [
|
@pytest.mark.parametrize(
|
||||||
|
"name",
|
||||||
|
[
|
||||||
pytest.param("build123d", id="invalid color name"),
|
pytest.param("build123d", id="invalid color name"),
|
||||||
pytest.param("#ffg", id="invalid rgb 12bit"),
|
pytest.param("#ffg", id="invalid rgb 12bit"),
|
||||||
pytest.param("#fffg", id="invalid rgba 12bit"),
|
pytest.param("#fffg", id="invalid rgba 12bit"),
|
||||||
|
|
@ -144,21 +199,34 @@ def test_color_likes_alpha(color_like):
|
||||||
pytest.param("#fffff", id="short rgb 24bit"),
|
pytest.param("#fffff", id="short rgb 24bit"),
|
||||||
pytest.param("#fffffff", id="short rgba 24bit"),
|
pytest.param("#fffffff", id="short rgba 24bit"),
|
||||||
pytest.param("#fffffffff", id="long rgba 24bit"),
|
pytest.param("#fffffffff", id="long rgba 24bit"),
|
||||||
])
|
],
|
||||||
|
)
|
||||||
def test_exceptions_color_name(name):
|
def test_exceptions_color_name(name):
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
Color(name)
|
Color(name)
|
||||||
|
|
||||||
@pytest.mark.parametrize("color_type", [
|
|
||||||
pytest.param((dict({"name": "red", "alpha": 1},)), id="dict arg"),
|
@pytest.mark.parametrize(
|
||||||
|
"color_type",
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
(
|
||||||
|
dict(
|
||||||
|
{"name": "red", "alpha": 1},
|
||||||
|
)
|
||||||
|
),
|
||||||
|
id="dict arg",
|
||||||
|
),
|
||||||
pytest.param(("red", "blue"), id="str + str"),
|
pytest.param(("red", "blue"), id="str + str"),
|
||||||
pytest.param((1., "blue"), id="float + str order"),
|
pytest.param((1.0, "blue"), id="float + str order"),
|
||||||
pytest.param((1, "blue"), id="int + str order"),
|
pytest.param((1, "blue"), id="int + str order"),
|
||||||
])
|
],
|
||||||
|
)
|
||||||
def test_exceptions_color_type(color_type):
|
def test_exceptions_color_type(color_type):
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
Color(*color_type)
|
Color(*color_type)
|
||||||
|
|
||||||
|
|
||||||
# Methods
|
# Methods
|
||||||
def test_rgba_wrapped():
|
def test_rgba_wrapped():
|
||||||
c = Color(1.0, 1.0, 0.0, 0.5)
|
c = Color(1.0, 1.0, 0.0, 0.5)
|
||||||
|
|
@ -167,17 +235,87 @@ def test_rgba_wrapped():
|
||||||
assert c.wrapped.GetRGB().Blue() == 0.0
|
assert c.wrapped.GetRGB().Blue() == 0.0
|
||||||
assert c.wrapped.Alpha() == 0.5
|
assert c.wrapped.Alpha() == 0.5
|
||||||
|
|
||||||
|
|
||||||
def test_copy():
|
def test_copy():
|
||||||
c = Color(0.1, 0.2, 0.3, alpha=0.4)
|
c = Color(0.1, 0.2, 0.3, alpha=0.4)
|
||||||
c_copy = copy.copy(c)
|
c_copy = copy.copy(c)
|
||||||
np.testing.assert_allclose(tuple(c_copy), (0.1, 0.2, 0.3, 0.4), rtol=1e-5)
|
np.testing.assert_allclose(tuple(c_copy), (0.1, 0.2, 0.3, 0.4), rtol=1e-5)
|
||||||
|
|
||||||
|
|
||||||
def test_str_repr_is():
|
def test_str_repr_is():
|
||||||
c = Color(1, 0, 0)
|
c = Color(1, 0, 0)
|
||||||
assert str(c) == "Color: (1.0, 0.0, 0.0, 1.0) is 'RED'"
|
assert str(c) == "Color: (1.0, 0.0, 0.0, 1.0) is 'RED'"
|
||||||
assert repr(c) == "Color(1.0, 0.0, 0.0, 1.0)"
|
assert repr(c) == "Color(1.0, 0.0, 0.0, 1.0)"
|
||||||
|
|
||||||
|
|
||||||
def test_str_repr_near():
|
def test_str_repr_near():
|
||||||
c = Color(1, 0.5, 0)
|
c = Color(1, 0.5, 0)
|
||||||
assert str(c) == "Color: (1.0, 0.5, 0.0, 1.0) near 'DARKGOLDENROD1'"
|
assert str(c) == "Color: (1.0, 0.5, 0.0, 1.0) near 'DARKGOLDENROD1'"
|
||||||
assert repr(c) == "Color(1.0, 0.5, 0.0, 1.0)"
|
assert repr(c) == "Color(1.0, 0.5, 0.0, 1.0)"
|
||||||
|
|
||||||
|
|
||||||
|
class TestColorCategoricalSet:
|
||||||
|
def test_returns_expected_number_of_colors(self):
|
||||||
|
colors = Color.categorical_set(5)
|
||||||
|
assert len(colors) == 5
|
||||||
|
assert all(isinstance(c, Color) for c in colors)
|
||||||
|
|
||||||
|
def test_colors_are_evenly_spaced_in_hue(self):
|
||||||
|
count = 8
|
||||||
|
colors = Color.categorical_set(count)
|
||||||
|
hues = [colorsys.rgb_to_hls(*tuple(c)[:3])[0] for c in colors]
|
||||||
|
diffs = [(hues[(i + 1) % count] - hues[i]) % 1.0 for i in range(count)]
|
||||||
|
avg_diff = sum(diffs) / len(diffs)
|
||||||
|
assert all(math.isclose(d, avg_diff, rel_tol=1e-2) for d in diffs)
|
||||||
|
|
||||||
|
def test_starting_hue_as_float(self):
|
||||||
|
(r, g, b, _) = tuple(Color.categorical_set(1, starting_hue=0.25)[0])
|
||||||
|
h = colorsys.rgb_to_hls(r, g, b)[0]
|
||||||
|
assert math.isclose(h, 0.25, rel_tol=0.05)
|
||||||
|
|
||||||
|
def test_starting_hue_as_int_hex(self):
|
||||||
|
# Blue (0x0000FF) should be valid and return a Color
|
||||||
|
c = Color.categorical_set(1, starting_hue=0x0000FF)[0]
|
||||||
|
assert isinstance(c, Color)
|
||||||
|
|
||||||
|
def test_starting_hue_invalid_type(self):
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
Color.categorical_set(3, starting_hue="invalid")
|
||||||
|
|
||||||
|
def test_starting_hue_out_of_range(self):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Color.categorical_set(3, starting_hue=1.5)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Color.categorical_set(3, starting_hue=-0.1)
|
||||||
|
|
||||||
|
def test_starting_hue_negative_int(self):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Color.categorical_set(3, starting_hue=-1)
|
||||||
|
|
||||||
|
def test_constant_alpha_applied(self):
|
||||||
|
colors = Color.categorical_set(3, alpha=0.7)
|
||||||
|
for c in colors:
|
||||||
|
(_, _, _, a) = tuple(c)
|
||||||
|
assert math.isclose(a, 0.7, rel_tol=1e-6)
|
||||||
|
|
||||||
|
def test_iterable_alpha_applied(self):
|
||||||
|
alphas = (0.1, 0.5, 0.9)
|
||||||
|
colors = Color.categorical_set(3, alpha=alphas)
|
||||||
|
for a, c in zip(alphas, colors):
|
||||||
|
(_, _, _, returned_alpha) = tuple(c)
|
||||||
|
assert math.isclose(a, returned_alpha, rel_tol=1e-6)
|
||||||
|
|
||||||
|
def test_iterable_alpha_length_mismatch(self):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Color.categorical_set(4, alpha=[0.5, 0.7])
|
||||||
|
|
||||||
|
def test_hues_wrap_around(self):
|
||||||
|
colors = Color.categorical_set(10, starting_hue=0.95)
|
||||||
|
hues = [colorsys.rgb_to_hls(*tuple(c)[:3])[0] for c in colors]
|
||||||
|
assert all(0.0 <= h <= 1.0 for h in hues)
|
||||||
|
|
||||||
|
def test_alpha_defaults_to_one(self):
|
||||||
|
colors = Color.categorical_set(4)
|
||||||
|
for c in colors:
|
||||||
|
(_, _, _, a) = tuple(c)
|
||||||
|
assert math.isclose(a, 1.0, rel_tol=1e-6)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue