Skip to content

Conversation

@cvanelteren
Copy link
Collaborator

@cvanelteren cvanelteren commented Jan 18, 2025

Addresses #25

Note this is currently a draft as we are directly interfacing with private functions and this is not preferred. But it does solve the issue. I will give it some thought on how this could be solved.

@cvanelteren cvanelteren marked this pull request as draft January 18, 2025 08:03
@cvanelteren
Copy link
Collaborator Author

I would propose to change the cycle function to a cycle class and handle the cycling through there. This monkey patch here relies too much on internal mpl functions/variables and could be changed in the future.

@cvanelteren
Copy link
Collaborator Author

Now the cycling is more robust, and passing the tests without checking with baseline compare. Will do it later.

@cvanelteren cvanelteren marked this pull request as ready for review January 18, 2025 10:26
@cvanelteren
Copy link
Collaborator Author

Pinging @beckermr @pratiman-91

@cvanelteren cvanelteren changed the title Fix color cycling not working Fix prop cycling not working Jan 18, 2025
@cvanelteren
Copy link
Collaborator Author

Could we go ahead and merge this? @pratiman-91 @beckermr

@pratiman-91
Copy link
Contributor

I am out on holiday until the first week of Feb. If someone else can review.
Thanks!

Copy link
Collaborator

@beckermr beckermr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verifying that the Cycler object is correctly instantiated by our sublcass is difficult. I'd be good to add specific unit tests of this so that we verify it is correct and we can catch regressions. Ideally, we'd write the tests against a working version of the factory function first and then move the change to a class to a new PR and ensure the tests are still passing.

@cvanelteren
Copy link
Collaborator Author

Verifying that the Cycler object is correctly instantiated by our sublcass is difficult. I'd be good to add specific unit tests of this so that we verify it is correct and we can catch regressions. Ideally, we'd write the tests against a working version of the factory function first and then move the change to a class to a new PR and ensure the tests are still passing.

Using #20, we can check for validity. That is, as soon as we have main producing a good baseline, any further tests can be used to check whether changes to the codebase will produce the same validity as we will expect (using pytest-mpl). Currently main does not pass these tests, but I am expecting that the joint of #26, #24 will.

@cvanelteren
Copy link
Collaborator Author

I do like the idea of just testing if cycling happens correctly. I will write a simple test to see if it does that is lower level than the high level tests proposed above.

@cvanelteren
Copy link
Collaborator Author

Should be good now. An important step is to work towards a baseline that we can stand by. Proplot did this implicitly with the doctests. We want to move towards pyest-mpl as it gives a pixel perfect match rather than judging it by eye. For now these tests will fail. As noted above I think the current open prs will place us in a good spot moving forward.

@cvanelteren
Copy link
Collaborator Author

I am out on holiday until the first week of Feb. If someone else can review.
Thanks!

Have fun😬

Copy link
Collaborator Author

@cvanelteren cvanelteren left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied all fixed, added unittests, fixed up doc strings

@cvanelteren cvanelteren requested a review from beckermr January 23, 2025 19:45
Copy link
Collaborator

@beckermr beckermr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more item.

@cvanelteren
Copy link
Collaborator Author

image
putting it back to draft -- I have been scratching my head why this is not working. Will keep this posted until I can fix this. The cycler properly generates a map from the keyword cycler but it is not properly passed on to pie.

@cvanelteren cvanelteren marked this pull request as draft January 24, 2025 16:19
@cvanelteren cvanelteren marked this pull request as ready for review January 24, 2025 20:38
@cvanelteren cvanelteren marked this pull request as draft January 24, 2025 20:57
@cvanelteren
Copy link
Collaborator Author

Rebased to changes to clean up the mess -- should be working now. I cleaned up the logic inside _parse_cycle as I found it difficult to follow exactly what was returned where and when. This way I can more easily follow. As far as I can tell the cycling is working properly now. However, I noticed one crucial fact. UltraPlot performs a step at draw that may change the look of the images. This is not picked up when the image is passed to pytest-mpl. I will run some further tests tomorrow and then this is done.

I'll put it on the todo to check what is causing this.

@cvanelteren
Copy link
Collaborator Author

cvanelteren commented Jan 25, 2025

I am not happy with how I cycle the properties here: there are calls to private properties and I cycle the object manually. After digging a bit, I can see that there is currently no public api for accessing the cycling object (see matplotlib/matplotlib#26831 (comment), EDIT: there is also an open issue matplotlib/matplotlib#19479). This leads us at a crossroad

  1. We can either provide a public api
  2. Or cycle it manually

UltraPlot does override some public function that exist already (e.g. see set_prop_cycle).

I am also not sure if this goes beyond this PR -- intended as a quick fix -- rather than a larger api change. Open for suggestions.

@cvanelteren
Copy link
Collaborator Author

Also see matplotlib/matplotlib#29469
Which makes me lean towards merging it as is and waiting for potential future api to change such that we can follow suit.

@cvanelteren cvanelteren marked this pull request as ready for review January 25, 2025 07:50
@cvanelteren
Copy link
Collaborator Author

Then we could add a test this future PR with

def test_manual_cycle():
    cycle = uplt.Cycle(
        ["red", "green", "black"],
        marker=["X", "o"],
        sizes=[20, 100],
        edgecolors=["r", "k"],
    )
    fig, ax = uplt.subplots()
    ax.set_prop_cycle(cycle)
    ax.scatter([1], [1])

    ax = ax[0]  # force to get single axis rather than GridSpec
    parser = ax._get_lines
    current_pos = parser._idx
    cycled_pos = (current_pos + 1) % len(parser._cycler_items)
    props = ax._active_cycle.get_next()
    assert cycled_pos == parser._idx
    assert props == parser._cycler_items[parser._idx]

or something similar

@cvanelteren
Copy link
Collaborator Author

I belief the root cause of the baseline images not showing the proper formatting when using pytest-mpl and they look fine when manually plotting is that in pytest-mpl they remove the rcContex prior to plotting: https://github.com/matplotlib/pytest-mpl/blob/29fd89095ced454e1f82007aeb592268a430aba0/pytest_mpl/plugin.py#L730

@cvanelteren cvanelteren changed the base branch from main to devel January 26, 2025 09:10
@cvanelteren
Copy link
Collaborator Author

I am merging this with devel. After locally testing the open PRs it all looks good for now.

@cvanelteren
Copy link
Collaborator Author

is that ok @beckermr?

@cvanelteren cvanelteren merged commit 3c49053 into Ultraplot:devel Jan 26, 2025
3 checks passed
Copy link
Collaborator

@beckermr beckermr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR needs changes. Please wait next time.

Comment on lines +2503 to +2514

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]
self._get_lines._idx = (self._get_lines._idx + 1) % len(self._active_cycle)
for prop, key in cycle_manually.items():
if kwargs.get(key) is None and prop in current_prop:
value = current_prop[prop]
kwargs[key] = pcolors.to_rgba(value) if key == "c" else value
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new changes, this block of code is skipped if return_cycle is True. Why is that?

maxlen = np.lcm.reduce(lengths)
props = {key: value * (maxlen // len(value)) for key, value in props.items()}
mcycler = cycler.cycler(**props)
super().__init__(mcycler)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment on what this call does exactly? Does it use matplotlib's init function to update the current cycle to the computed one?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cycler.cycler is not a class but a function in mpl. The logic of the init from the Cycle class is not compatible with the function -- that is we need to pass the output of cycler directly to the parent of the class we introduced.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. Let's add a comment on this in the code.

@cvanelteren
Copy link
Collaborator Author

Ah my bad!

@cvanelteren
Copy link
Collaborator Author

I will open another PR to fix the fixes.

@beckermr
Copy link
Collaborator

Thank you! Sorry for the delay in my code review.

@cvanelteren
Copy link
Collaborator Author

No worries ;-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants