Repository: Ultraplot/UltraPlot

Issue:
color cycler
I just updated to v1.08, I think it should have a fix for uplt.Cycle if I'm not mistaking but I get the following behaviour on python 3.10 and 3.13 (didnt test in between):


```python
color = ["#7FACFA", "#FA654D", "#8CAD36"]
cycle = uplt.Cycle(color=color)
list(cycle)

>>> [{'color': 'black'}]
```

expected:

```python
from cycler import cycler
cycle =cycler(color=["#7FACFA", "#FA654D", "#8CAD36"])
list(cycle)

>>> [{'color': '#7FACFA'}, {'color': '#FA654D'}, {'color': '#8CAD36'}]
```

Discussion:
@cvanelteren: First argument is not named. Not sure if this is different from proplot but the first argument should be a color.
```
color = ["#7FACFA", "#FA654D", "#8CAD36"]
cycle = uplt.Cycle(color)
print(list(cycle))
```@Jhsmit: ah yes thanks that works, does indeed differ from proplot@cvanelteren: I see the issue. Fixing it now. On 1.08 arg has to be empty.

On Tue, 25 Feb 2025 at 16:35, Jochem Smit ***@***.***> wrote:

> I just updated to v1.08, I think it should have a fix for uplt.Cycle if
> I'm not mistaking but I get the following behaviour on python 3.10 and 3.13
> (didnt test in between):
>
> color = ["#7FACFA", "#FA654D", "#8CAD36"]cycle = uplt.Cycle(color=color)list(cycle)
> >>> [{'color': 'black'}]
>
> expected:
>
> from cycler import cyclercycle =cycler(color=["#7FACFA", "#FA654D", "#8CAD36"])list(cycle)
> >>> [{'color': '#7FACFA'}, {'color': '#FA654D'}, {'color': '#8CAD36'}]
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/Ultraplot/ultraplot/issues/98>, or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AEUVDVZ376Y6JHPH2RGRU3L2RSES7AVCNFSM6AAAAABX26D42OVHI2DSMVQWIX3LMV43ASLTON2WKOZSHA3TQNZWG44DAOA>
> .
> You are receiving this because you are subscribed to this thread.Message
> ID: ***@***.***>
> [image: Jhsmit]*Jhsmit* created an issue (Ultraplot/ultraplot#98)
> <https://github.com/Ultraplot/ultraplot/issues/98>
>
> I just updated to v1.08, I think it should have a fix for uplt.Cycle if
> I'm not mistaking but I get the following behaviour on python 3.10 and 3.13
> (didnt test in between):
>
> color = ["#7FACFA", "#FA654D", "#8CAD36"]cycle = uplt.Cycle(color=color)list(cycle)
> >>> [{'color': 'black'}]
>
> expected:
>
> from cycler import cyclercycle =cycler(color=["#7FACFA", "#FA654D", "#8CAD36"])list(cycle)
> >>> [{'color': '#7FACFA'}, {'color': '#FA654D'}, {'color': '#8CAD36'}]
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/Ultraplot/ultraplot/issues/98>, or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AEUVDVZ376Y6JHPH2RGRU3L2RSES7AVCNFSM6AAAAABX26D42OVHI2DSMVQWIX3LMV43ASLTON2WKOZSHA3TQNZWG44DAOA>
> .
> You are receiving this because you are subscribed to this thread.Message
> ID: ***@***.***>
>


-- 


   - Casper van Elteren | Computational & Complexity Scientist
   - Checkout my website <http://thefriendlyghost.nl> for contact info and
   current projects!
@cvanelteren: Fixing it on #99 @cvanelteren: Fixed on main

Solution patch:
diff --git a/ultraplot/constructor.py b/ultraplot/constructor.py
index de7874c2..3be50358 100644
--- a/ultraplot/constructor.py
+++ b/ultraplot/constructor.py
@@ -881,10 +881,10 @@ markeredgecolors, markerfacecolors
 
     def _handle_empty_args(self, props, kwargs):
         """Handle case when no positional arguments are provided."""
-        props.setdefault("color", "black")
+        props.setdefault("color", ["black"])
         if kwargs:
             warnings._warn_ultraplot(f"Ignoring Cycle() keyword arg(s) {kwargs}.")
-        self._build_cycler(())
+        self._build_cycler((props,))
 
     def _handle_cycler_args(self, args, props, kwargs):
         """Handle case when arguments are cycler objects."""
@@ -928,13 +928,17 @@ markeredgecolors, markerfacecolors
                 props.setdefault(key, []).extend(value)
         # Build cycler with matching property lengths
         # Ensure at least a default color property exists
-        if not props:
-            props = {"color": ["black"]}
-
         # Build cycler with matching property lengths
         lengths = [len(value) for value in props.values()]
         maxlen = np.lcm.reduce(lengths)
-        props = {key: value * (maxlen // len(value)) for key, value in props.items()}
+        props = {
+            key: value * (maxlen // len(value))
+            for key, value in props.items()
+            if len(value)
+        }
+        # Set default color property if not present
+        if "color" not in props or not props:
+            props = {"color": ["black"]}
         mcycler = cycler.cycler(**props)
         super().__init__(mcycler)
 


Test patch:
diff --git a/ultraplot/tests/test_constructor.py b/ultraplot/tests/test_constructor.py
index 91243761..e017c0a9 100644
--- a/ultraplot/tests/test_constructor.py
+++ b/ultraplot/tests/test_constructor.py
@@ -65,6 +65,10 @@ def test_not_allowed_keyword():
 def test_cycler_edge_cases():
     """Test edge cases and error conditions"""
     # Test with empty lists
+    cycle = uplt.Cycle()
+    assert cycle.get_next() == dict(color="black")  # default fallback
+    cycle = uplt.Cycle(color=[])
+    assert cycle.get_next() == dict(color="black")  # default fallback
     cycle = uplt.Cycle(colors=[])
     assert cycle.get_next() == dict(color="black")  # default fallback
 
@@ -74,6 +78,7 @@ def test_cycler_edge_cases():
     props1 = cycle.get_next()
     props2 = cycle.get_next()
     assert props1["marker"] == props2["marker"]  # marker should stay same
+    assert props1["color"] == "red"
     assert props1["color"] != props2["color"]  # color should cycle
 
 
@@ -108,3 +113,12 @@ def test_pass_on_cycle():
     for color in colors:
         assert color == active_cycle.get_next()["color"]
         assert color == cycle.get_next()["color"]
+
+    colors = ["red", "green", "black"]
+    cycle = uplt.Cycle(color=colors)
+    fig, ax = uplt.subplots()
+    ax.set_prop_cycle(cycle)
+    active_cycle = ax.axes._active_cycle
+    for color in colors:
+        assert color == active_cycle.get_next()["color"]
+        assert color == cycle.get_next()["color"]