Skip to content

Cycler #4258

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 25 commits into from
Closed

Cycler #4258

wants to merge 25 commits into from

Conversation

tacaswell
Copy link
Member

a = SingleCycler('c', ['r', 'purple', 'lime'])
b = SingleCycler('lw', [1, 5])
c = SingleCycler('markevery', [10, 50])
m = SingleCycler('marker', 'o')   # note length 1


fig, ax_lst = plt.subplots(2, 2)

def cycle_demo(ax, cycler):
    c_len = len(cycler)
    th = np.linspace(0, 2*np.pi, 1024)
    y = np.sin(th) 
    for j, p in zip(range(2*c_len), cycler):
        ax.plot(th, j + y, **p)

for ax, cc in zip(ax_lst.ravel(), [a, a + b, a*b, (c*m)*a]):
    cycle_demo(ax, cc)

so

As discussed in several places the problem of doing arbitrary style cycles over arbitrary parameters and inputs is a bit vexing. Inspired by Holoviews and sensible composable objects, I have implemented Cyclers which give an easy API to construct these cycles.

There are two types of composition + which is an 'inner' product implemented by zipping the two cycles together and * which is an 'outer' product which is implemented with itertools.product.

There are two implementation (one of which will be deleted), but am a bit torn on which one I like better. The Cycler class is a stand alone class which handles both 'single' cycles and 'compound' cycles via a classmethod constructor and some implicit magic. The other is a set of three classes {_base_cycler, SingleCycler, and CompoundCycler} which handle respectively the composition logic, 'single' cycles and compound cycles. I am leaning towards the 3 class implementation because it has less implicit magic and users don't have to use a class method to get useful objects.

In both cases, the objects have len, __iter__ (which is infinite), finite_iter (which in __iter__, but finite), and keys (which says what properties the cycle is over).

The composition fails if the same property appears on both sides.

These are useful on their own, but I think replacing the color cycle on Axes with one of these would be a major usability gain.

I am not wedded to any of the class names.

Documentation, examples, and testing will come later if people think this is a good idea. Should probably also add an in-place version of + and * as well.

attn @WeatherGod @efiring @mdboom @pelson @danielballan

Helper-class to compose arbitrary, complex, property cycles.

An alternate implementation + documentation forth coming.
I think I like this version better.

More classes, but less implicit magic.
@tacaswell tacaswell added this to the next point release milestone Mar 21, 2015
@tacaswell
Copy link
Member Author

Also note that * is non-commutative:

a = SingleCycler('c', ['r', 'purple', 'lime'])
b = SingleCycler('lw', [1, 5])
c = SingleCycler('markevery', [10, 50])
m = SingleCycler('marker', 'o')


fig, ax_lst = plt.subplots(2, 2)

def cycle_demo(ax, cycler):
    c_len = len(cycler)
    th = np.linspace(0, 2*np.pi, 1024)
    y = np.sin(th) 
    for j, p in zip(range(2*c_len), cycler):
        ax.plot(th, j + y, **p)

for ax, cc in zip(ax_lst.ravel(), [b*a, a*b, (c*m)*a, a*(c*m)]):
    cycle_demo(ax, cc)

so

return len(list(self.finite_iter()))


class _base_cycler(object):
Copy link
Member

Choose a reason for hiding this comment

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

It might make sense to make this a fully public base class. The advantage would be that then for either a SingleCycler or a CompoundCycler one could test with isinstance(obj, CyclerBase). This would be getting closer to your first implementation in which there is just a single Cycler class, which is appealing because it is closer to the way one would think about using these objects.

@efiring
Copy link
Member

efiring commented Mar 22, 2015

I'm favoring the Cycler plus cycler approach as providing more concise code and a simpler, more natural API. The classmethod could be private. Slight and obvious modification: the cycler function would check its argument, and return it directly if it is already a Cycler.

@tacaswell
Copy link
Member Author

Also, if we do want to have in-place modification (a += b) then we need to go with the single class approach or users will have to know which class they have or we have to do terrifying class-promotion (which we do already have in the code base in mplot3D).

- make class method private
- provide simple user facing factory method
- make _base_cycler public as BaseCycler
- added `__imul__` and `__iadd__`
- added paranoia copy functions to make sure that child Cyclers can't be
  changed after they are composed (which would mess up the key tracking
  logic)
Don't open the chance for outside users to mess with contents of the
`_keys` attribute.
- if multi-property cycler, raise
- if already the Cycler we want, return copy
- if cycler of a different property, extract the values and continue

This behavior follows the pattern of numpy to copy in `np.array`.
@tacaswell
Copy link
Member Author

Some of these copies should maybe be deep copies if we feel like being really paranoid.

@tacaswell
Copy link
Member Author

both of the above examples work with s/SingleCycler/cycler/

@danielballan
Copy link
Contributor

Yes, I really like this. +1 or the cycler and Cycler approach.

Return a list of the style dictionaries yielded by this Cycler
If we know the length of the left and right and the op, we can directly
calculate what the length will be with out actually iterating over
everything.
@tacaswell
Copy link
Member Author

attn @dchabot

@tacaswell
Copy link
Member Author

@matplotlib/developers We now have two proposals for how to overhaul cycling, this PR and #4314 . I am (trying not to be biased) in favor of this implementation because it provides composition and is a bit less brittle.

Can we get a consensus on which way to go before I spend time to document this and shoe-horn it into the existing cycle infrastructure?

Is it worth merging this as-is (maybe with some examples in the sphinx docs) without touching axes and letting this be a user-space tool for a while?

@efiring
Copy link
Member

efiring commented Apr 19, 2015

I'm in favor of your proposal. I think its generality is not only elegant, but will prove useful.
Using this PR with some documentation, and then having a separate PR for adopting Cycler as the mechanism in axes makes sense also. It keeps both PRs more manageable than doing it all in one.

@dopplershift
Copy link
Contributor

I'm +1 here. I like that this is pretty much just a kwarg generator, and can therefore easily scale to other plots, artists, etc. (both in and outside MPL). I also 2nd Eric's notion that this PR should go by itself and to something separate to change axes.

@mfitzp
Copy link
Member

mfitzp commented Apr 20, 2015

I'm very much +1 for this. It gives a nice clean and concise way to do something that can be currently quite faffy. Particularly like the construction using *+ and would lean towards the single class approach if it gets us += ability without nasty workarounds.

How would the single-class interface look for compound definitions?

 a = Cycler('c', ['r', 'purple', 'lime'], 'lw', [1, 5])

@pelson
Copy link
Member

pelson commented Apr 21, 2015

@tacaswell - I have totally neglected my mpl duties recently, so sorry you've not had and feedback from me 😞.

First things first, I really like it. I think it will help in several situations within the mpl codebase, and will valuable to users who want the ability to define n colors and m arbitrary other styles, and combine them together.

A few high level review points:

  • notice how the module doesn't import anything from matplotlib. This is a clue that you've got something which is more widely applicable than just mpl (I could see other libraries wanting this kind of functionality). It would be on my mind to pull this out into its own package, and work towards adding it as a new mpl dependency (we can always pull it into the source like we do with agg, if we don't want extra burden upon the user)
  • now is the time to write good tests and docs for this code. It should be relatively straightforward to unit test this, and bank the functionality that is desired from the outset.
  • I found the a+b non-intuitive where a and b had different length. I'd be inclined to disable the ability to add two cyclers of a different length together, in doing that, I'd be thinking about implementing __getitem__ so that somebody with two cycles that they really want to combine in that way can index the cyclers to get them to the same length.

@tacaswell
Copy link
Member Author

The other option for the + operator is to wrap both of them in a cycle before zipping so that adding two cycles gets you a cycle of length the least common multiple of their lengths, but I ran into some issues trying to implement that (not sure if they are fundamental issues or just not getting my head around it right).

I like the __getitem__ idea a lot, I will take a crack at that later.

@tacaswell
Copy link
Member Author

and I think I am on board with splitting this off into it's own package. My current plan is to keep the development here and then split it off later (before we tag a version of mpl which depends on it) and make sure it hist pypi + conda before the next version of mpl.

@tacaswell
Copy link
Member Author

@pelson Starting to look at implemeting the slicing, I am not sure how do it for compound cycles. Ex

(cycler('c', ['r', 'g', 'b']) * cycler('lw', [1, 2]))[:5] 

can not be represented as a multiplication so would have to be decomposed into

cycler('c', ['r', 'g', 'b', 'r', 'g']) + cycler('lw', [1, 2, 1, 2, 1])

which seems a bit silly, but now that I type this out does not seem so unreasonable.

Covers everything but the repr

Tests '+' behavior which may be changed.
It is far simpler for users that want infinite cycling to wrap
the ``Cycler`` in a call to `itertools.cycle` than it is for the user
that wants a finite iterator to sort out how to get the finite version.

This simplifies the API and makes it a bit more guessable/intuitive.
Add a method to return an equivalent Cycler composed using only addition
composition.
Add the ability to multiply a Cycler by an integer to increase
the length of the cycle.  This has the side effect of simplifying
the Cycler to only use addition composition
Implement `__getitem__` for Cycler by broadcasting down to
the lists in the transposed data.

Raises on trying to use anything else but a slice.  Numpy-style
fancy slicing might be worth adding.   Adding integer slicing is
probably of minilmal value.
Now that we have both multiplication and slicing, it is easy
to get correct length cyclers to add together
@tacaswell
Copy link
Member Author

@pelson Added slicing and made + only work on equal length cyclers.

@danielballan
Copy link
Contributor

I added PR against this PR adding an IPython rich display hook. Demo

ENH : Add a rich display hook to Cycler.
@tacaswell
Copy link
Member Author

Thanks @danielballan !

@tacaswell
Copy link
Member Author

I have started to split this PR off into it's own repo: https://github.com/tacaswell/cycler I will eventually move it into the matplotlib org once I have fleshed out all extra bits and pieces and people are happy enough with it.

@pelson
Copy link
Member

pelson commented Jun 22, 2015

👍 - thanks @tacaswell. I think we are at the point of closing this PR in favour of a new PR which adds cycler as a dependency, is that right? Is it worth getting other projects on board early so that we can share experiences and ideas? I'm thinking bokeh, vispy etc.

@tacaswell
Copy link
Member Author

Yes. I will try to get functional docs on cycler tonight + a 0.1 tag pushed to pypi + travis to give people an easy way to play with it. The goal would be to do 1.0 on cycler a week before mpl 1.5.

Leaving this branch existing for the time being just in case.

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.

6 participants