8000 Shareable property cyclers by anntzer · Pull Request #19484 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

Shareable property cyclers #19484

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed

Shareable property cyclers #19484

wants to merge 1 commit into from

Conversation

anntzer
Copy link
Contributor
@anntzer anntzer commented Feb 9, 2021

First commit: Simplify _process_plot_var_args.set_prop_cycle.

It is private, so we can restrict it to only support cycler instances
and None.

Second commit: Shareable property cyclers.

Allow a single property cycler to be reused across multiple axes, with
plots on any axes advancing the same joint iterator.

e.g.

from matplotlib import pyplot as plt
from matplotlib.axes._base import to_shareable_cycler

shared = to_shareable_cycler(plt.rcParams["axes.prop_cycle"])
fig, axs = plt.subplots(2, sharex=True)
axs[0].set_prop_cycle(shared)
axs[1].set_prop_cycle(shared)

axs[0].plot([0, 1])
axs[1].plot([0, 1])
# Check that fill uses a separate iterator.
axs[1].fill([2, 3, 4], [0, 1, 0])
axs[0].fill([2, 3, 4], [0, 1, 0])
# Check that the second axes goes back to the first color, even though it
# didn't use it on the first cycle.
for i in range(10):
    axs[1].plot([5+i, 5+i], [0, 1])
plt.show()

test

We may want to move to_shareable_cycler and _get_cycling_iterator to the cycler repo itself (possibly under different names), here I just made them free functions to keep the PR self-contained.

An example use case:

from matplotlib import pyplot as plt
from matplotlib.axes._base import to_shareable_cycler
import numpy as np

fig, axs = plt.subplots(2, 3, sharex=True, sharey=True)
axs = axs.ravel()
shared = to_shareable_cycler(plt.rcParams["axes.prop_cycle"])
for ax in axs[1:]:
    ax.set_prop_cycle(shared)

xs = np.arange(10)
all_ys = (5 + np.random.randn(5)[:, None]) * xs + 3 * np.random.randn(5, 10)
axs[0].set_title("all datasets")
for i, ys in enumerate(all_ys):
    axs[0].plot(ys)
    axs[i + 1].set_title(f"dataset #{i} & fit")
    axs[i + 1].plot(ys)
    axs[i + 1].plot(xs, np.polyval(np.polyfit(xs, ys, 1), xs), c="k", zorder=.5)

plt.show()

test
(Obviously this is also doable by manually passing the correct colors to each plot call, although it's quite harder to do so if you want to support arbitrary property cycles...)

Closes #19479.

@timhoffm
Copy link
Member
timhoffm commented Feb 9, 2021

While this is a clever way to hack the sharing behavior into the existing structure. However I'm not convinced of this because

  1. I'm unclear of the intended scope of Cycler and whether it holds state (see Support sharing cycler instances across axes #19479 (comment)). Only if it holds the cycling state, sharing cyclers can be reasonable semantically.
  2. I don't like the conversion to_shareable_cycler(). Distinguishing between sharable and non-sharable cyclers would be an advanced concept which I don't want to throw at and explain to the user.

IMHO you should share something that is an iterator. So either cyclers would need to become iterators, or you should accept iterators in set_prop_cycle and share iter(cycler). Both is probably not as easy implementation-wise, but it would be the cleaner API.

@anntzer
Copy link
Contributor Author
anntzer commented Feb 9, 2021

Turning cyclers into iterators is basically going to break anyone c 8000 alling set_prop_cycle(cycler) on multiple Axes and expecting the old behavior (which is likely the most commonly required one anyways). So "shareable cyclers" need to be something else: here, I implemented them as cyclers with an extra internal flag, but I could easily have returned a non-Cycler object instead (let's just call it a Foo, better names welcome) e.g. by moving the current Cycler implementation to a hidden _CyclerBase base class and making Cycler and Foo both inherit from _CyclerBase, with Foo just having the extra flag; also to_shareable_cycler would be renamed to_foo.

On the other hand, I believe that actually overloading __iter__ for that purpose will not work (unless you are willing to break the already existing semantics of iter(cycler), which is that it yields the non-cycling list of properties), because Matplotlib expects to be able to call next(self._prop_cycler) as needed (in process_plot_var_args._getdefaults), but that means that iter(cycler) will eventually reach the end of the list and raise a StopIteration).

@jklymak jklymak marked this pull request as draft May 10, 2021 16:40
@anntzer anntzer added status: needs comment/discussion needs consensus on next step and removed status: needs tests labels May 10, 2021
@anntzer
Copy link
Contributor Author
anntzer commented May 10, 2021

Added a test, so this should be good to go -- modulo the discussion of whether we actually want that or not (and under what API), of course...

Also split out the first commit (which should be uncontroversial) to #20201.

@anntzer anntzer marked this pull request as ready for review May 10, 2021 17:18
@jklymak jklymak marked this pull request as draft May 11, 2021 13:56
@anntzer anntzer force-pushed the cyclers branch 2 times, most recently from 16e72cf to 988df3c Compare July 8, 2021 08:20
Allow a single property cycler to be reused across multiple axes, with
plots on any axes advancing the same joint iterator.

e.g.
```python
from matplotlib import pyplot as plt
from matplotlib.axes._base import to_shareable_cycler

shared = to_shareable_cycler(plt.rcParams["axes.prop_cycle"])
fig, axs = plt.subplots(2, sharex=True)
axs[0].set_prop_cycle(shared)
axs[1].set_prop_cycle(shared)

axs[0].plot([0, 1])
axs[1].plot([0, 1])
# Check that fill uses a separate iterator.
axs[1].fill([2, 3, 4], [0, 1, 0])
axs[0].fill([2, 3, 4], [0, 1, 0])
# Check that the second axes goes back to the first color, even though it
# didn't use it on the first cycle.
for i in range(10):
    axs[1].plot([5+i, 5+i], [0, 1])
plt.show()
```
@github-actions
8000 Copy link
github-actions bot commented Oct 6, 2023

Since this Pull Request has not been updated in 60 days, it has been marked "inactive." This does not mean that it will be closed, though it may be moved to a "Draft" state. This helps maintainers prioritize their reviewing efforts. You can pick the PR back up anytime - please ping us if you need a review or guidance to move the PR forward! If you do not plan on continuing the work, please let us know so that we can either find someone to take the PR over, or close it.

@github-actions github-actions bot added the status: inactive Marked by the “Stale” Github Action label Oct 6, 2023
@anntzer
Copy link
Contributor Author
anntzer commented Oct 6, 2023

I still think this would be useful, but this is basically tracked by #19479. @oscargus you did express some interest, perhaps you may be willing to champion something along these lines?

@anntzer anntzer closed this Oct 6, 2023
@anntzer anntzer deleted the cyclers branch October 6, 2023 09:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: inactive Marked by the “Stale” Github Action status: needs comment/discussion needs consensus on next step status: needs rebase
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support sharing cycler instances across axes
2 participants
0