From 0c5bba0de1153e918583be5096d1a55b9b6ba61e Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Tue, 16 Jul 2024 13:29:10 -0700 Subject: [PATCH 1/6] Add map_values to Gradient --- arcadia_pycolor/gradient.py | 48 ++++++++++++++++++++++++ arcadia_pycolor/tests/test_gradients.py | 50 +++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/arcadia_pycolor/gradient.py b/arcadia_pycolor/gradient.py index 3de6458..595f8e9 100644 --- a/arcadia_pycolor/gradient.py +++ b/arcadia_pycolor/gradient.py @@ -6,6 +6,7 @@ from arcadia_pycolor.hexcode import HexCode from arcadia_pycolor.palette import ColorSequence, Palette from arcadia_pycolor.utils import ( + NumericSequence, distribute_values, interpolate_x_values, is_monotonic, @@ -94,6 +95,53 @@ def resample_as_palette(self, steps: int = 5) -> Palette: colors=colors, ) + def map_values( + self, + values: NumericSequence, + min_value: Union[float, None] = None, + max_value: Union[float, None] = None, + ) -> list[str]: + """Map a sequence of values to their corresponding colors from a gradient + + Args: + min_value: + Determines which value corresponds to the first color in the spectrum. + Values less than this are given this color. If not provided, min(values) is + chosen. + max_value: + Determines which value corresponds to the last color in the spectrum. Values + greater than this are given this color. If not provided, max(values) is + chosen. + + Returns: + A list of hex code strings. + """ + + if not len(values): + return [] + + if min_value is None: + min_value = min(values) + + if max_value is None: + max_value = max(values) + + if min_value > max_value: + raise ValueError( + f"max_value ({max_value}) must be greater than min_value ({min_value})." + ) + + cmap = self.to_mpl_cmap() + + if min_value == max_value: + # Value range is 0. Return the midrange color for each value. + return [mcolors.to_hex(cmap(0.5))] * len(values) + + normalized_values = [(value - min_value) / (max_value - min_value) for value in values] + clamped_values = [max(0.0, min(1.0, value)) for value in normalized_values] + + return [mcolors.to_hex(cmap(value)) for value in clamped_values] + def interpolate_lightness(self) -> "Gradient": """ Interpolates the gradient to new values based on lightness. diff --git a/arcadia_pycolor/tests/test_gradients.py b/arcadia_pycolor/tests/test_gradients.py index 8d4ea3e..5d02396 100644 --- a/arcadia_pycolor/tests/test_gradients.py +++ b/arcadia_pycolor/tests/test_gradients.py @@ -2,6 +2,7 @@ import pytest from arcadia_pycolor import Gradient, HexCode +from arcadia_pycolor.colors import black, white from .test_hexcode import INVALID_HEXCODES @@ -116,3 +117,52 @@ def test_gradient_swatch_steps(): ) def test_gradient_swatch_repr(name, colors, swatch): assert Gradient(name, colors).__repr__() == swatch + + +@pytest.fixture +def black_to_white_gradient() -> Gradient: + return Gradient("_", [black, white], [0.0, 1.0]) + + +@pytest.mark.parametrize( + "values, expected_colors", + [ + ([0, 1], ["#000000", "#ffffff"]), + ([0, 0.5, 1], ["#000000", "#808080", "#ffffff"]), + ([1, 2, 3, 4, 5], ["#000000", "#404040", "#808080", "#c0c0c0", "#ffffff"]), + ([-1, 0, 1], ["#000000", "#808080", "#ffffff"]), + ([], []), + ], +) +def test_map_values_basic_cases( + black_to_white_gradient: Gradient, + values: list[float], + expected_colors: list[str], +): + assert black_to_white_gradient.map_values(values) == expected_colors + + +@pytest.mark.parametrize( + "values, min_value, max_value, expected_colors", + [ + ([0, 0.5, 1], 0, 1, ["#000000", "#808080", "#ffffff"]), + ([0, 0.5, 1], 0.25, 0.75, ["#000000", "#808080", "#ffffff"]), + ([-1, 0.5, 2], 0, 1, ["#000000", "#808080", "#ffffff"]), + ([0, 10], 0, 20, ["#000000", "#808080"]), + ([0, 10], 0, 0, ["#808080", "#808080"]), + ], +) +def test_map_values_custom_ranges( + black_to_white_gradient: Gradient, + values: list[float], + min_value: float, + max_value: float, + expected_colors: list[str], +): + assert black_to_white_gradient.map_values(values, min_value, max_value) == expected_colors + + +def test_map_values_invalid_cases(black_to_white_gradient: Gradient): + # You can't pass min larger than max + with pytest.raises(ValueError, match="must be greater than"): + black_to_white_gradient.map_values([0, 1], min_value=1, max_value=0) From 566b705389626922ce6260cc2451564e1705dee8 Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Tue, 16 Jul 2024 15:46:57 -0700 Subject: [PATCH 2/6] Return HexCode --- arcadia_pycolor/gradient.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arcadia_pycolor/gradient.py b/arcadia_pycolor/gradient.py index 595f8e9..0d40bb7 100644 --- a/arcadia_pycolor/gradient.py +++ b/arcadia_pycolor/gradient.py @@ -100,7 +100,7 @@ def map_values( values: NumericSequence, min_value: Union[float, None] = None, max_value: Union[float, None] = None, - ) -> list[str]: + ) -> list[HexCode]: """Map a sequence of values to their corresponding colors from a gradient Args: @@ -140,7 +140,7 @@ def map_values( normalized_values = [(value - min_value) / (max_value - min_value) for value in values] clamped_values = [max(0.0, min(1.0, value)) for value in normalized_values] - return [mcolors.to_hex(cmap(value)) for value in clamped_values] + return [HexCode(f"{value}", mcolors.to_hex(cmap(value))) for value in clamped_values] def interpolate_lightness(self) -> "Gradient": """ From 8328e6781d6cc92eef33ff465c33887c4cce4d08 Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Tue, 16 Jul 2024 15:56:42 -0700 Subject: [PATCH 3/6] Complain if min equals max --- arcadia_pycolor/gradient.py | 6 +----- arcadia_pycolor/tests/test_gradients.py | 5 ++++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/arcadia_pycolor/gradient.py b/arcadia_pycolor/gradient.py index 0d40bb7..f91ca97 100644 --- a/arcadia_pycolor/gradient.py +++ b/arcadia_pycolor/gradient.py @@ -126,17 +126,13 @@ def map_values( if max_value is None: max_value = max(values) - if min_value > max_value: + if min_value >= max_value: raise ValueError( f"max_value ({max_value}) must be greater than min_value ({min_value})." ) cmap = self.to_mpl_cmap() - if min_value == max_value: - # Value range is 0. Return the midrange color for each value. - return [mcolors.to_hex(cmap(0.5))] * len(values) - normalized_values = [(value - min_value) / (max_value - min_value) for value in values] clamped_values = [max(0.0, min(1.0, value)) for value in normalized_values] diff --git a/arcadia_pycolor/tests/test_gradients.py b/arcadia_pycolor/tests/test_gradients.py index 5d02396..42d7a25 100644 --- a/arcadia_pycolor/tests/test_gradients.py +++ b/arcadia_pycolor/tests/test_gradients.py @@ -149,7 +149,6 @@ def test_map_values_basic_cases( ([0, 0.5, 1], 0.25, 0.75, ["#000000", "#808080", "#ffffff"]), ([-1, 0.5, 2], 0, 1, ["#000000", "#808080", "#ffffff"]), ([0, 10], 0, 20, ["#000000", "#808080"]), - ([0, 10], 0, 0, ["#808080", "#808080"]), ], ) def test_map_values_custom_ranges( @@ -166,3 +165,7 @@ def test_map_values_invalid_cases(black_to_white_gradient: Gradient): # You can't pass min larger than max with pytest.raises(ValueError, match="must be greater than"): black_to_white_gradient.map_values([0, 1], min_value=1, max_value=0) + + # Or min equal to max + with pytest.raises(ValueError, match="must be greater than"): + black_to_white_gradient.map_values([0, 1], min_value=1, max_value=1) From c3edb8285493da686dd1d1bde031223be4e09838 Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Tue, 16 Jul 2024 15:57:43 -0700 Subject: [PATCH 4/6] Add all negatives case --- arcadia_pycolor/tests/test_gradients.py | 1 + 1 file changed, 1 insertion(+) diff --git a/arcadia_pycolor/tests/test_gradients.py b/arcadia_pycolor/tests/test_gradients.py index 42d7a25..7fa4cba 100644 --- a/arcadia_pycolor/tests/test_gradients.py +++ b/arcadia_pycolor/tests/test_gradients.py @@ -131,6 +131,7 @@ def black_to_white_gradient() -> Gradient: ([0, 0.5, 1], ["#000000", "#808080", "#ffffff"]), ([1, 2, 3, 4, 5], ["#000000", "#404040", "#808080", "#c0c0c0", "#ffffff"]), ([-1, 0, 1], ["#000000", "#808080", "#ffffff"]), + ([-3, -2, -1], ["#000000", "#808080", "#ffffff"]), ([], []), ], ) From 9f0c6110aec7960bad39a4f65b327542de394db5 Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Tue, 16 Jul 2024 16:01:29 -0700 Subject: [PATCH 5/6] Improve docstring --- arcadia_pycolor/gradient.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/arcadia_pycolor/gradient.py b/arcadia_pycolor/gradient.py index f91ca97..d4db1d9 100644 --- a/arcadia_pycolor/gradient.py +++ b/arcadia_pycolor/gradient.py @@ -106,15 +106,15 @@ def map_values( Args: min_value: Determines which value corresponds to the first color in the spectrum. - Values less than this are given this color. If not provided, min(values) is - chosen. + Any values below this minimum are assigned to the first color. If not + provided, min(values) is chosen. max_value: - Determines which value corresponds to the last color in the spectrum. Values - greater than this are given this color. If not provided, max(values) is - chosen. + Determines which value corresponds to the last color in the spectrum. + Any values greater than this maximum are assigned to the last color. If + not provided, max(values) is chosen. Returns: - A list of hex code strings. + list[HexCode]: A list of hex codes. """ if not len(values): From 0c1c8af9cc3530276271874484599f8c28c03480 Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Wed, 17 Jul 2024 09:45:26 -0700 Subject: [PATCH 6/6] Clamping is the default cmap behavior --- arcadia_pycolor/gradient.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/arcadia_pycolor/gradient.py b/arcadia_pycolor/gradient.py index d4db1d9..2cac517 100644 --- a/arcadia_pycolor/gradient.py +++ b/arcadia_pycolor/gradient.py @@ -134,9 +134,8 @@ def map_values( cmap = self.to_mpl_cmap() normalized_values = [(value - min_value) / (max_value - min_value) for value in values] - clamped_values = [max(0.0, min(1.0, value)) for value in normalized_values] - return [HexCode(f"{value}", mcolors.to_hex(cmap(value))) for value in clamped_values] + return [HexCode(f"{value}", mcolors.to_hex(cmap(value))) for value in normalized_values] def interpolate_lightness(self) -> "Gradient": """