Skip to content

Feature request: Named cyclers #10840

Closed as not planned
Closed as not planned

Description

@timhoffm

Feature Request / Discussion

Summary

Like we have named colormaps, it would be useful to have named cyclers as well.

This would allow code like

grafik

or even by name:

grafik

Is it useful?

  • I think there's a need to efficiently switch cyclers depending on the plot.
  • Also, as part of this, we can provide a number of default cyclers.
  • Styles are already available, however I have the feeling that they are too broad as I often adapt just the colors and defining a new style for each color scheme seems a bit heavy-weight.
  • Apart from the places where a cycler is explicitly used, this could also be used for other methods with a colors argument, e.g. plt.pie(data, colors='desert').

What would be needed?

Similar to colormaps:

  • register_cycler()
  • get_cycler()
  • a number of default cyclers.
  • Maybe the upstream Cycler class should get a name property.

Activity

  1. ImportanceOfBeingErnest commented on Mar 19, 2018

    @ImportanceOfBeingErnest
    Member

    A few comments:

    • I don't think one should introduce color cycles separate from colormaps, or inversely put, possible color cycles should be made available as ListedColormap instances.

    • The above being said, it is of course possible to use the colormaps to provide the colors to cycle over.

      df = pd.DataFrame(np.random.rand(4,3), columns=list("ABC"))
      
      with plt.rc_context({"axes.prop_cycle" : plt.cycler("color",plt.cm.Set3(range(3)))}):
         df.plot.bar()
      

      image

      or equally possible

      with plt.rc_context({"axes.prop_cycle" : plt.cycler("color",plt.get_cmap("Set3").colors)}):
          df.plot.bar()
      
    • In view of the above I do not actually see much of a benefit introducing the option to write
      with plt.rc_context({"axes.prop_cycle" : plt.get_cycler("Set3") }):
      instead of
      with plt.rc_context({"axes.prop_cycle" : plt.cycler("color",plt.cm.Set3(range(3)))}):
      To simplify things in a way that would truely ease people's lifes one would rather need
      with plt.use_cycle("Set3"):

    • Seaborn does actually provide this very convenient way of setting the cycle (resulting in the same plot as above):

      import seaborn as sns
      sns.set_palette("Set3")
      df.plot.bar()
      

      or

      with sns.color_palette("Set3"):
          df.plot.bar()
      

    Some problems one should probably tackle first:

    • Resolve some of the problems with ListedColormaps compared to SegmentedColormaps. It is in general not very transparent how to handle those and for non-experienced users it is a non-trivial task to get 3 colors out of a ListedColormap:
      image

    • Obtain a cycler from a colormap, i.e. something like ListedColormap.as_cycler(), such that one could do

      cycler = plt.get_cmap(...).as_cycler()
      

      With this, the proposed register_cycler would not be needed at all and the proposed get_cycler() would reduce to a single line of code. If all "default cyclers" are colormaps, there is also no need for a name attribute of the Cycler class itself.

  2. added this to the milestone on Mar 19, 2018
  3. timhoffm commented on Mar 20, 2018

    @timhoffm
    MemberAuthor

    @ImportanceOfBeingErnest thanks for the comments. ListedColormap doesn't feel quite right.

    My concerns are:

    • Colormaps are a mapping [0, 1] -> colors. This is conceptually different from a discrete sequence. ListedColormap is just the special case of segment-wise constant values. It does not provide expected list-like features such as indexing and iteration. We have to use ListedColormap.colors to get what we actually need. So, a ListedColormap is basically an object for another purpose, that just happens to have a name and a list of colors stored which we could use to create a cycler. That doesn't feel quite clean and intuitive to me.

    • There's also a good chance of users mixing up ListedColormaps and other colormaps. While plt.cycler("color", plt.get_cmap("Set3").colors) works, it wouldn't work with other colormaps such as "jet". Same with a possible .as_cycler() method. Having to know that get_cmap returns objects with different APIs is not something I want to burden on the user.

    • Colors are probably the most used property to cycle over. However, if we go into the direction of easily changing cycling properties, we should at least consider the more general case of having names for other cyclers as well.

    I think, there is real value in cleanly distinguishing between colormaps and colorcyclers (or more general cyclers). That said, i really like the syntax of with plt.use_cycle("Set3"), maybe also sns.set_palette("Set3") - only the argument should not be a colormap name.

  4. jklymak commented on Mar 20, 2018

    @jklymak
    Member

    There is already context management for styles. Is that not very similar. Certainly you can have a style that only changes the cycler.

  5. ImportanceOfBeingErnest commented on Mar 20, 2018

    @ImportanceOfBeingErnest
    Member

    My point is that possible cycler names should exactly be the colormap names. Because they contain the same colors. Having the "tab10" colormap, I would want the corresponding cycler to also be accessible through the name "tab10" and not something completely different. Or the other way around, if there is a "desert" color cycle, one would want to have the corresponding colors available as a ListedColormap get_cmap("desert") as well. Why deprive the user of this option and burden them with having to manually create cyclers from colormaps and vice versa?

    Users already now have to deal with get_cmap resulting in different objects depending on what they input. The burden is hence present and confuses people. But one might of course try to unify things and make this easier. This is what the first stated problem from my previous post wants to make clear.

    Indeed, to use a LinearSegmentedColormap like jet as a cycler, you would need to call it differently.

    with plt.rc_context({"axes.prop_cycle" : plt.cycler("color",plt.get_cmap("jet")(np.linspace(0,1,3)))}):
        df.plot.bar()
    

    So to resolve this problem, an as_cycler() method would make sense.

    Up to now I was under the assumption that there is a clear distinction between LinearSegmentedColormaps and ListedColormaps in that all qualitatitive colormaps are Listed ones and all other are Segmented ones. This does not seem to be the case though and any given colormap does not even know which type it belongs to. Again, utterly confusing. But in any case, a possible implementation, although working a bit heuristically, would be

    from matplotlib.colors import LinearSegmentedColormap, ListedColormap
    import matplotlib._cm
    
    def get_cycle(cmap, N=None, use_index="auto"):
        if isinstance(cmap, str):
            if use_index == "auto":
                if cmap in ['Pastel1', 'Pastel2', 'Paired', 'Accent',
                            'Dark2', 'Set1', 'Set2', 'Set3',
                            'tab10', 'tab20', 'tab20b', 'tab20c']:
                    use_index=True
                else:
                    use_index=False
            cmap = plt.get_cmap(cmap)
        if not N:
            N = cmap.N
        if use_index=="auto":
            if cmap.N > 100:
                use_index=False
            elif isinstance(cmap, LinearSegmentedColormap):
                use_index=False
            elif isinstance(cmap, ListedColormap):
                use_index=True
        if use_index:
            ind = np.arange(int(N)) % cmap.N
            return plt.cycler("color",cmap(ind))
        else:
            colors = cmap(np.linspace(0,1,N))
            return plt.cycler("color",colors)
    

    image

    Here the user does not need to know anything about different return types or the true differences between colormaps, get_cycle(name, number) will give him the colorcycle with number colors from name colormap.

    Concerning cycler names, agreed, if you want to have a linestyle cycle you can name it differently, just not with any name that is reserved by a colormap I'd say.

  6. efiring commented on Mar 21, 2018

    @efiring
    Member

    @ImportanceOfBeingErnest, Matplotlib colormaps are internally all the same at the point at which they are used: a lookup table with machinery for executing the lookup. LinearSegmented versus Listed are just different ways of specifying how to generate that table. Typically (and by default) LinearSegmentedColormaps have 256 normal colors in the lookup table, so they are a good approximation of a continuum. In addition, all colormaps have 3 special values: one for masked points, one for under-range values, and one for over-range values. The lookups can use integers for direct indexing, or they can use floating point values in the 0-1 range.

  7. ImportanceOfBeingErnest commented on Mar 23, 2018

    @ImportanceOfBeingErnest
    Member

    @efiring That is precisely the problem. There is no distinction between qualitative colormaps like "tab10" and gradient colormaps like "viridis". Those colormaps are the same, but would need to be used in a totally different manner. If each colormap would know which kind it belongs to, usage would be simplified a lot.
    Nonetheless it is possible to use both kinds of colormaps as shown above for producing cyclers. So I still think this is the way to go.

  8. ngoldbaum commented on May 30, 2018

    @ngoldbaum
    Contributor

    I agree with the original reporter of this issue that it would be nice to have a listing of predefined named color cycles.

  9. tacaswell commented on May 30, 2018

    @tacaswell
    Member

    (moving discussion from gitter)

    There seems to be 3 issues here:

    1. providing a get_cycler_by_name(name : str) -> cycler) function some place (maybe in matplotlib.colors?). Do we want to allow this to return any cycler? If so do we also need to provide a way to filter out keys the user does not want? Do we provide a registration mechanism for users to 'bring-their-own'? This overlaps a bit with having a color-cycle-only style and mpl.style.use, but I like the non-global-state version of this!
    2. providing a get_color_cycler_from_colormap(cmap : Union[str, ColorMap], N=15) -> cycler function which will make a Norm and do the right slicing into a color map to get a set of colors and then stuff them into a cycler.
    3. automatically populate 1) with the output of 2) from all of the color maps we ship / automatically fall back.

    I think 1 is a good idea (pinning down those questions), 2 is a nice helper (and people are doing to do it anyway), 3 is probably not a good idea to do automatically (as not all color maps go well into a color cycle and we may not want to expose N trough number 1), but we should evaluate our color maps to populate the default set (for example, a number of our color maps started life as color cycles that got re-purposed and there are reasons to want categorical color maps).

  10. ImportanceOfBeingErnest commented on May 30, 2018

    @ImportanceOfBeingErnest
    Member

    The get_cycle method I proposed above basically does all 3 points. (Of course N is optional, if people don't like/need it, they are not forced to use it.) I do not understand the opposition against point 3. If people want 16 colors from the "viridis" colormap why make life so very hard for them???

  11. jklymak commented on May 30, 2018

    @jklymak
    Member

    I don't see how the proposed get_cycle defines named cycles in the absence of an already existing colormap.... Are you proposing that every cycle would need a named colormap first?

  12. ImportanceOfBeingErnest commented on May 30, 2018

    @ImportanceOfBeingErnest
    Member

    Are you proposing that every cycle would need a named colormap first?

    Absolutely! That is my point from the beginning.

  13. jklymak commented on May 30, 2018

    @jklymak
    Member

    But there are lots of cycles that we have or would like to have that don't rely on a colormap; it'd be a shame if they couldn't get names, and even more overhead if the only way to define them was to register a colormap first...

  14. ImportanceOfBeingErnest commented on May 30, 2018

    @ImportanceOfBeingErnest
    Member

    The fact that there are cycles which do not have a colormap equivalent is already suboptimal. Think of it the other way around: Do you want to have a situation where a bunch of colors named "tab10" is a colormap, but the other bunch of colors named "desert" is not? Do you want users to run into the situation where they mix up names between what is provided as colorcycle and what as colormap?

    image

    Is this really what people want?

  15. 8 remaining items

  16. timhoffm commented on May 31, 2018

    @timhoffm
    MemberAuthor

    Is this a case for a MEP?

  17. efiring commented on May 31, 2018

    @efiring
    Member

    I think a MEP might be overkill, but I certainly wouldn't object to one.
    It's not clear to me that this should involve more general cyclers; they can be invoked via styles. They can be generated by composition.
    Unification of named color lists seems like something that can be done independently.

  18. timhoffm commented on May 31, 2018

    @timhoffm
    MemberAuthor

    Can we agree that I will work out a proposal? I think we're a bit talking in circles now and it may help to discuss based on some API concept and use-cases.

  19. added
    API: changesChanges to the public API, typically requiring deprecation.
    on May 31, 2018
  20. efiring commented on May 31, 2018

    @efiring
    Member

    Certainly, please proceed.

  21. jklymak commented on May 31, 2018

    @jklymak
    Member

    Can style sheets call style sheets? i.e. can you have a tableau23-cycler style that then is embedded in other style sheets? That would be another way to go with this rather than give line cyclers special handling and having yet another mysterious registry that folks need to figure out how to hack into to add their own line cycles.

  22. ImportanceOfBeingErnest commented on May 31, 2018

    @ImportanceOfBeingErnest
    Member

    I think it's inevitable to provide a registration mechanism for cyclers. But if that is completely analoguous to how colormaps are registered, the learning curve for users is pretty flat. My point here would again be that a registered cycler should automatically register its color part as a colormap. And the inverse should be true as well, any colormap that is registered by the user should become a registered color cycler.

    Turning more towards general cyclers, I think an open question that would need to be discussed is how to handle compositions.

    • I could imagine to restrict named cyclers to a single property. A cycler named "desert" would be a colorcycle, a cycler named "triangles" would be marker cycle, a cycler named "shortstitch" would be a linestyle cycler. Such cyclers can be composed in the usual fashion, but possibly also via composed names, like "desert+triangles" or "triangles*shortstitch".
    • One could also allow for named composed cyclers. This makes things a bit more complicated from a user's perspective because the number of unique names grows with the power of possible properties and people will have problems to remember if some named cycler contains a certain property or not. On the other hand it feels more natural to just use a single name for the cycler you want to use.

    In view of this, it might make sense to think about whether or not a name should include the number of elements of that cycler. For the + composition this seems necessary, unless all cyclers have the same number of elements. E.g. if "triangles" has 4 elements, and "shortstitch" has 3, one could get an addititve marker-linestyle-cycler via "triangles3+shortstitch".

  23. modified the milestones: v3.0, on Jul 26, 2018
  24. modified the milestones: , needs sorting on Oct 6, 2022
  25. github-actions commented on Oct 11, 2023

    @github-actions

    This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!

  26. timhoffm commented on Oct 11, 2023

    @timhoffm
    MemberAuthor

    I'll close for now as there is not general consensus on a way forward and my personal priority has shifted.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    API: changesChanges to the public API, typically requiring deprecation.New featurestatus: inactiveMarked by the “Stale” Github Action

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions