From 33cfd88c64405cfaaaa489c26c1a99f66c68977b Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 12:02:38 +0100 Subject: [PATCH 01/18] allows cycle to be a tuple --- ultraplot/axes/plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index 2ba144c71..79323eedc 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -2488,7 +2488,7 @@ def _parse_cycle( resolved_cycle = constructor.Cycle(rc["axes.prop_cycle"]) case str() if cycle.lower() == "none": resolved_cycle = None - case str() | int(): + case str() | int() | tuple(): resolved_cycle = constructor.Cycle(cycle, **cycle_kw) case constructor.Cycle(): resolved_cycle = constructor.Cycle(cycle) From 8c96eab1403c179c8fa6c9536b990769cc063bcc Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 12:36:46 +0100 Subject: [PATCH 02/18] allow iterable --- ultraplot/axes/plot.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index 79323eedc..2efc6f027 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -9,7 +9,8 @@ import re import sys from numbers import Integral, Number -from typing import Any, Iterable +from typing import Any +from collections.abc import Iterable import matplotlib.artist as martist import matplotlib.axes as maxes @@ -2488,7 +2489,7 @@ def _parse_cycle( resolved_cycle = constructor.Cycle(rc["axes.prop_cycle"]) case str() if cycle.lower() == "none": resolved_cycle = None - case str() | int() | tuple(): + case str() | int() | Iterable(): resolved_cycle = constructor.Cycle(cycle, **cycle_kw) case constructor.Cycle(): resolved_cycle = constructor.Cycle(cycle) From 0de22b7fef2bf9fab83234a1513dcfb753466609 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 14:30:40 +0100 Subject: [PATCH 03/18] add colormap tests --- ultraplot/tests/test_format.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/ultraplot/tests/test_format.py b/ultraplot/tests/test_format.py index 9d8be905d..94530cc9e 100644 --- a/ultraplot/tests/test_format.py +++ b/ultraplot/tests/test_format.py @@ -340,3 +340,32 @@ def test_label_settings(): ax.format(xlabel="xlabel", ylabel="ylabel") ax.format(labelcolor="red") return fig + + +def test_colormap_parsing(): + """Test colormaps merging""" + reds = uplt.colormaps.get_cmap("reds") + blues = uplt.colormaps.get_cmap("blues") + + # helper function to test specific values in the colormaps + # threshold is used due to rounding errors + def test_range( + a: uplt.Colormap, + b: uplt.Colormap, + threshold=1e-10, + ranges=[0.0, 1.0], + ): + for i in ranges: + d = np.sqrt((np.array(a(i)) - np.array(b(i))) ** 2).sum() + if d >= threshold: + raise ValueError(f"Colormaps differ! Failed at {i} with {d}") + print(d) + + # Test if the colormaps are the same + test_range(uplt.Colormap("blues"), blues) + test_range(uplt.Colormap("reds"), reds) + # For joint colormaps, the lower value should be the lower of the first cmap and the highest should be the highest of the second cmap + test_range(uplt.Colormap("blues", "reds"), reds, ranges=[1.0]) + # Note: the ranges should not match either of the original colormaps + with pytest.raises(ValueError): + test_range(uplt.Colormap("blues", "reds"), reds) From a1b61d937938db80946d425083f2cef9e3298faa Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 15:38:43 +0100 Subject: [PATCH 04/18] added high level unittest --- ultraplot/tests/test_1dplots.py | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/ultraplot/tests/test_1dplots.py b/ultraplot/tests/test_1dplots.py index 5510695c6..b19c12a70 100644 --- a/ultraplot/tests/test_1dplots.py +++ b/ultraplot/tests/test_1dplots.py @@ -2,6 +2,7 @@ """ Test 1D plotting overrides. """ +from matplotlib.dates import WE import numpy as np import numpy.ma as ma import pandas as pd @@ -458,3 +459,52 @@ def test_norm_not_modified(): assert norm.vmin == 0 assert norm.vmax == 1 return fig + + +@pytest.mark.mpl_image_compare +def test_line_plot_cyclers(): + # Sample data + M, N = 50, 10 + state = np.random.RandomState(51423) + data1 = (state.rand(M, N) - 0.48).cumsum(axis=1).cumsum(axis=0) + data2 = (state.rand(M, N) - 0.48).cumsum(axis=1).cumsum(axis=0) * 1.5 + data1 += state.rand(M, N) + data2 += state.rand(M, N) + data1 *= 2 + + cmaps = ("Blues", "Reds") + cycle = uplt.Cycle(*cmaps) + + # Use property cycle for columns of 2D input data + fig, ax = uplt.subplots(ncols=3, sharey=True) + + # Intention of subplots + ax[0].set_title("Property cycle") + ax[1].set_title("Joined cycle") + ax[2].set_title("Separate cycles") + + ax[0].plot( + data1 + data2, + cycle="black", # cycle from monochromatic colormap + cycle_kw={"ls": ("-", "--", "-.", ":")}, + ) + + # Plot all dat with both cyclers on + # note: capitalization is done different here on purpose + ax[1].plot( + (data1 + data2), + cycle=cycle, + ) + + # Test cyclers separately + cycle = uplt.Cycle(*cmaps) + for idx in range(0, N): + print(idx) + ax[2].plot( + (data1[..., idx] + data2[..., idx]), + cycle=cycle, + cycle_kw={"N": N, "left": 0.3}, + ) + + fig.format(xlabel="xlabel", ylabel="ylabel", suptitle="On-the-fly property cycles") + return fig From 0bf7990cc5e7a2cbaa28985b79e5f8d46be9066b Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 15:38:57 +0100 Subject: [PATCH 05/18] added low level tests for cycle and colormap --- ultraplot/tests/test_format.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/ultraplot/tests/test_format.py b/ultraplot/tests/test_format.py index 94530cc9e..6bf0e483f 100644 --- a/ultraplot/tests/test_format.py +++ b/ultraplot/tests/test_format.py @@ -369,3 +369,32 @@ def test_range( # Note: the ranges should not match either of the original colormaps with pytest.raises(ValueError): test_range(uplt.Colormap("blues", "reds"), reds) + + +def test_input_parsing_cycle(): + """ + Test the potential inputs to cycle + """ + # The first argument is a string or an iterable of strings + + with pytest.raises(ValueError): + cycle = uplt.Cycle(None) + + # Empty should also be handled + cycle = uplt.Cycle() + + # Test singular string + cycle = uplt.Cycle("Blues") + target = uplt.colormaps.get_cmap("blues") + first_color = cycle.get_next()["color"] + first_color = uplt.colors.to_rgba(first_color) + assert np.allclose(first_color, target(0)) + + # test composition + cycle = uplt.Cycle("Blues", "Reds", N=2) + lower_half = uplt.colormaps.get_cmap("blues") + upper_half = uplt.colormaps.get_cmap("reds") + first_color = uplt.colors.to_rgba(cycle.get_next()["color"]) + last_color = uplt.colors.to_rgba(cycle.get_next()["color"]) + assert np.allclose(first_color, lower_half(0.0)) + assert np.allclose(last_color, upper_half(1.0)) From 6ef4e7ca236becd94316efc60848da156f667719 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 15:39:48 +0100 Subject: [PATCH 06/18] added com. function for cycler object --- ultraplot/constructor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ultraplot/constructor.py b/ultraplot/constructor.py index d5aae8bd3..a70e0bb84 100644 --- a/ultraplot/constructor.py +++ b/ultraplot/constructor.py @@ -938,6 +938,12 @@ def _build_cycler(self, dicts): mcycler = cycler.cycler(**props) super().__init__(mcycler) + def __eq__(self, other: Cycle) -> bool: + for a, b in zip(self, other): + if a != b: + return False + return True + def get_next(self): # Get the next set of properties if self._iterator is None: From 3ba6afb3818e7ee628830241737c2a945100bf7c Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 15:40:40 +0100 Subject: [PATCH 07/18] remove erroneous logic when plotting singular data and a new cycler is used --- ultraplot/axes/plot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index 2efc6f027..9fa1559f3 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -2497,11 +2497,10 @@ def _parse_cycle( resolved_cycle = None # Ignore cycle for single-column plotting - resolved_cycle = None if ncycle == 1 else resolved_cycle - if resolved_cycle and resolved_cycle != self._active_cycle: self.set_prop_cycle(resolved_cycle) + # Apply manual cycle properties if cycle_manually: current_prop = self._get_lines._cycler_items[self._get_lines._idx] From dc8e9cac2e974074b8932717c0c72236e82014de Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 15:41:27 +0100 Subject: [PATCH 08/18] black formatting --- ultraplot/axes/plot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index 9fa1559f3..56241de0b 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -2500,7 +2500,6 @@ def _parse_cycle( if resolved_cycle and resolved_cycle != self._active_cycle: self.set_prop_cycle(resolved_cycle) - # Apply manual cycle properties if cycle_manually: current_prop = self._get_lines._cycler_items[self._get_lines._idx] From 2366208a1964063b16491f1eaaed21db9a010f41 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 15:42:13 +0100 Subject: [PATCH 09/18] remove print from test --- ultraplot/tests/test_format.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ultraplot/tests/test_format.py b/ultraplot/tests/test_format.py index 6bf0e483f..82fe010fa 100644 --- a/ultraplot/tests/test_format.py +++ b/ultraplot/tests/test_format.py @@ -359,7 +359,6 @@ def test_range( d = np.sqrt((np.array(a(i)) - np.array(b(i))) ** 2).sum() if d >= threshold: raise ValueError(f"Colormaps differ! Failed at {i} with {d}") - print(d) # Test if the colormaps are the same test_range(uplt.Colormap("blues"), blues) From 1eb29a5e71e20bb9bbd7950f490706aaae0e34d4 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 15:44:00 +0100 Subject: [PATCH 10/18] removed spurious input --- ultraplot/tests/test_1dplots.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ultraplot/tests/test_1dplots.py b/ultraplot/tests/test_1dplots.py index b19c12a70..12cb737e3 100644 --- a/ultraplot/tests/test_1dplots.py +++ b/ultraplot/tests/test_1dplots.py @@ -2,7 +2,6 @@ """ Test 1D plotting overrides. """ -from matplotlib.dates import WE import numpy as np import numpy.ma as ma import pandas as pd From 05820917ac73f660457b31563bc12a3ecf08a9e1 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 15:45:00 +0100 Subject: [PATCH 11/18] replace logic with numpy --- ultraplot/tests/test_format.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ultraplot/tests/test_format.py b/ultraplot/tests/test_format.py index 82fe010fa..a28a84807 100644 --- a/ultraplot/tests/test_format.py +++ b/ultraplot/tests/test_format.py @@ -356,8 +356,7 @@ def test_range( ranges=[0.0, 1.0], ): for i in ranges: - d = np.sqrt((np.array(a(i)) - np.array(b(i))) ** 2).sum() - if d >= threshold: + if not np.allclose(a(i), b(i)): raise ValueError(f"Colormaps differ! Failed at {i} with {d}") # Test if the colormaps are the same From 335f44841fcd46c0807ce26aa012cc76b757ff3f Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 15:46:09 +0100 Subject: [PATCH 12/18] updated singular comment --- ultraplot/axes/plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index 56241de0b..6aafecdf4 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -2496,7 +2496,7 @@ def _parse_cycle( case _: resolved_cycle = None - # Ignore cycle for single-column plotting + # Ignore cycle for single-column plotting unless cycle is different if resolved_cycle and resolved_cycle != self._active_cycle: self.set_prop_cycle(resolved_cycle) From 8421577a8e987288292b128201d91b34c798b28a Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 15:47:51 +0100 Subject: [PATCH 13/18] remove type hinting --- ultraplot/constructor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/constructor.py b/ultraplot/constructor.py index a70e0bb84..a673668ea 100644 --- a/ultraplot/constructor.py +++ b/ultraplot/constructor.py @@ -938,7 +938,7 @@ def _build_cycler(self, dicts): mcycler = cycler.cycler(**props) super().__init__(mcycler) - def __eq__(self, other: Cycle) -> bool: + def __eq__(self, other): for a, b in zip(self, other): if a != b: return False From 3253f35607550868af12f761d00e90c3cddc38f3 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 15:51:30 +0100 Subject: [PATCH 14/18] correct value error --- ultraplot/tests/test_format.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/tests/test_format.py b/ultraplot/tests/test_format.py index a28a84807..b55aa2019 100644 --- a/ultraplot/tests/test_format.py +++ b/ultraplot/tests/test_format.py @@ -357,7 +357,7 @@ def test_range( ): for i in ranges: if not np.allclose(a(i), b(i)): - raise ValueError(f"Colormaps differ! Failed at {i} with {d}") + raise ValueError(f"Colormaps differ !") # Test if the colormaps are the same test_range(uplt.Colormap("blues"), blues) From 963c30775812ac4cb51e5d99600a9dfd2a644cda Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 16:18:50 +0100 Subject: [PATCH 15/18] rm another print statement --- ultraplot/tests/test_1dplots.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ultraplot/tests/test_1dplots.py b/ultraplot/tests/test_1dplots.py index 12cb737e3..8d4dc5ea2 100644 --- a/ultraplot/tests/test_1dplots.py +++ b/ultraplot/tests/test_1dplots.py @@ -498,7 +498,6 @@ def test_line_plot_cyclers(): # Test cyclers separately cycle = uplt.Cycle(*cmaps) for idx in range(0, N): - print(idx) ax[2].plot( (data1[..., idx] + data2[..., idx]), cycle=cycle, From 8cceab60e0b37ee0c976d47dde9c5905628ea7cc Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 16:20:08 +0100 Subject: [PATCH 16/18] rm comment --- ultraplot/tests/test_1dplots.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ultraplot/tests/test_1dplots.py b/ultraplot/tests/test_1dplots.py index 8d4dc5ea2..0d4dac394 100644 --- a/ultraplot/tests/test_1dplots.py +++ b/ultraplot/tests/test_1dplots.py @@ -489,7 +489,6 @@ def test_line_plot_cyclers(): ) # Plot all dat with both cyclers on - # note: capitalization is done different here on purpose ax[1].plot( (data1 + data2), cycle=cycle, From 61d4a92805ccc8cce0ca400e0c4155686ea551bb Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 17:10:11 +0100 Subject: [PATCH 17/18] reorder cases --- ultraplot/axes/plot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index 6aafecdf4..91d1137de 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -2487,12 +2487,12 @@ def _parse_cycle( resolved_cycle = None case True: resolved_cycle = constructor.Cycle(rc["axes.prop_cycle"]) + case constructor.Cycle(): + resolved_cycle = constructor.Cycle(cycle) case str() if cycle.lower() == "none": resolved_cycle = None case str() | int() | Iterable(): resolved_cycle = constructor.Cycle(cycle, **cycle_kw) - case constructor.Cycle(): - resolved_cycle = constructor.Cycle(cycle) case _: resolved_cycle = None From 5a02dd5d2123079223a01530324568d287c513fd Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 20 Feb 2025 17:41:18 +0100 Subject: [PATCH 18/18] styling the comments --- ultraplot/tests/test_format.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ultraplot/tests/test_format.py b/ultraplot/tests/test_format.py index b55aa2019..cd073e2ba 100644 --- a/ultraplot/tests/test_format.py +++ b/ultraplot/tests/test_format.py @@ -374,7 +374,6 @@ def test_input_parsing_cycle(): Test the potential inputs to cycle """ # The first argument is a string or an iterable of strings - with pytest.raises(ValueError): cycle = uplt.Cycle(None) @@ -388,7 +387,7 @@ def test_input_parsing_cycle(): first_color = uplt.colors.to_rgba(first_color) assert np.allclose(first_color, target(0)) - # test composition + # Test composition cycle = uplt.Cycle("Blues", "Reds", N=2) lower_half = uplt.colormaps.get_cmap("blues") upper_half = uplt.colormaps.get_cmap("reds")