Skip to content

[MNT]: Remove the label API from Artist #29422

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

Open
timhoffm opened this issue Jan 7, 2025 · 12 comments
Open

[MNT]: Remove the label API from Artist #29422

timhoffm opened this issue Jan 7, 2025 · 12 comments

Comments

@timhoffm
Copy link
Member

timhoffm commented Jan 7, 2025

Summary

Artist provides the label API intended to for legend labels:

image

However, only a small subset of Artists are "data" Artists and can be reasonably used in a legend. From the subclasses of Artist:

  • suitable for legend: Line2D, Collection, Patch
  • not suitable for legend: AnnotationBbox, Text, Tick, Axis, FigureBase, Legend, _AxesBase, Table, OffsetBox, _ImageBase, QuiverKey

In some of these not-suitable classes label is repurposed for other use cases, e.g. axis labels, figure labels being displayed as window title. That's quite confusing.

Proposed fix

Create a Protocol or Mixin or DataArtist subclass for legend labels and use that in Line2D, Collection, Patch. Deprecate and remove get/set_label from all other Artists; possibly taking special measures where label is currently repurposed.

@anntzer
Copy link
Contributor

anntzer commented Jan 7, 2025

I can't say I really like that, e.g. I currently have code to serialize the data of (all subplots of) a figure into a dict of {artist_label: {"class": ..., "data": ...}}. At least it would be nice to keep a way to attach user-defined strings ("name", or even other metadata) to artists; maybe overloading the (legend) "label" for that purpose is not great but that API should not go away until a replacement is provided.

@timhoffm
Copy link
Member Author

timhoffm commented Jan 7, 2025

serialize the data of (all subplots of) a figure into a dict of {artist_label: {"class": ..., "data": ...}}

Not sure I understand what you do. Most of the labels are not set and there's no guarantee that they are unique. For example, each Tick has 6 labels associated, one for Tick class itself, and additionally one for each of tick.tick1line, tick.tick2line, tick.gridline, tick.label1, tick.label2. All of them are empty strings by default. Do you set them all so that your artist_label becomes unique?

Are you sure you need labelling capability on all ( / most) of the artist subclasses (https://matplotlib.org/stable/api/artist_api.html#inheritance-diagrams)?

At least it would be nice to keep a way to attach user-defined strings ("name", or even other metadata) to artists.

One can always monkey-patch such an information onto the artist if that's needed in some special cases. Making that a builtin public API is only necessary if we need a formalized way to access such information, i.e. when that information needs to be shared/accessed in our code and/or 3rd parties. Do you have such a use case?

@anntzer
Copy link
Contributor

anntzer commented Jan 7, 2025

serialize the data of (all subplots of) a figure into a dict of {artist_label: {"class": ..., "data": ...}}

Not sure I understand what you do. Most of the labels are not set and there's no guarantee that they are unique. For example, each Tick has 6 labels associated, one for Tick class itself, and additionally one for each of tick.tick1line, tick.tick2line, tick.gridline, tick.label1, tick.label2. All of them are empty strings by default. Do you set them all so that your artist_label becomes unique?

I only serialize axes.lines, axes.images, etc. so e.g. the ticks don't show up, and I also filter based on the presence of a non-default label (i.e. check whether the label starts with an underscore, similarly to what legend() does). For the relevant data artists I indeed ensure that the labels are unique (the serializer is not a fully general purpose code, just something I wrote for various complex plots that I fully control and that I want to serialize).

Are you sure you need labelling capability on all ( / most) of the artist subclasses (https://matplotlib.org/stable/api/artist_api.html#inheritance-diagrams)?

Not all, but I do use this for ImageBase at least.

At least it would be nice to keep a way to attach user-defined strings ("name", or even other metadata) to artists.

One can always monkey-patch such an information onto the artist if that's needed in some special cases. Making that a builtin public API is only necessary if we need a formalized way to access such information, i.e. when that information needs to be shared/accessed in our code and/or 3rd parties. Do you have such a use case?

Obviously I could just attach a custom attribute on the artist (line._my_label = "foo") but this feels pretty dirty (somewhat related to #26881, too).

@timhoffm
Copy link
Member Author

timhoffm commented Jan 7, 2025

Well, we could introduce a simple Artist.metadata dict, that is explicitly for information only and we don't use it for anything. OTOH it feels a bit like YAGNI. I'm not aware of any other library doing this. - pandas.DataFrame.attrs is somewhat similar in that the library does not care about the content. But pandas propagates that information to derived DataFrames, which makes it necessarily a builtin feature.

In particular, I find it important to strictly separate such information-only data provided by the user and anything that has an effect in the library. Abusing the label parameter for "additional information" this is a recipe for confusion because depending on the Artist and the formatting (underscore prefix) you may influence the library behavior or not.

(somewhat related to #26881, too).

Widgets and animations are dedicated concepts in the library. If we need to store them on a figure or axes, that should get explicit attributes. I therefore claim this is a completely separate topic.

@timhoffm
Copy link
Member Author

timhoffm commented Jan 7, 2025

Semi-OT: It may be reasonable to be able to name artists, i.e. make it possible to retrieve them back by a string. such that this would be equivalent

fig, ax = plt.subplots()
line, = ax.plot(x, y)
fig, ax = plt.subplots()
ax.plot(x, y, name="myline")

[...]

line = fig.get_artist("myline")

This would also help with FuncAnimations, which are currently quite cobbled together: typically, the plot is created globally, and relevant Artists are stored in global variables that are accessed from the function. This means, the FuncAnimation is not self-contained but the function depends on globally accessible Artists at run-time.

Taking from https://matplotlib.org/stable/gallery/animation/simple_anim.html we could then evolve the API such that

[...]
line, = ax.plot(x, np.sin(x))

def animate(i):
    artists['line'].set_ydata(np.sin(x + i / 50))  # update the data.
    return line,

[...]

could be replaced by

[...]
ax.plot(x, np.sin(x), name="myline")

def animate(i, fig):
    line = fig.get_artist("myline")
    line.set_ydata(np.sin(x + i / 50))  # update the data.
    return line,

[...]

where we pass the figure into the function and from that retrieve the relevant artists. This would eliminate the global state.

@anntzer I believe this kind of naming would cover your use case as well?

@anntzer
Copy link
Contributor

anntzer commented Jan 7, 2025

This is indeed more or less what I do by hand right now in other places (unrelated to the serialization problem): directly copy-pasting from my internal codebase, I have

    artists = {}
    def register(art):
        assert art.get_label()
        artists[art.get_label()] = art
        return art
# then use as
    register(ax.plot(...)[0])
    register(ax.imshow(...))
# etc.
# and access via the artists dict.

@story645
Copy link
Member

story645 commented Jan 7, 2025

Create a Protocol or Mixin or DataArtist subclass for legend labels and use that in Line2D, Collection, Patch. Deprecate and remove get/set_label from all other Artists; possibly taking special measures where label is currently repurposed.

What about an explicit LegendKeyArtist ? (DataArtist is too semantically overloaded for me). OT but looking at @anntzer's answer, thinking it'd be nice to be able to register artists with the legend in a way that isn't either on creation (via label kwarg) or all at once (by passing in a list of handles) but possibly on the fly. Particularly if you want multiple legends or to modify a third party legend.

Semi-OT: It may be reasonable to be able to name artists, i.e. make it possible to retrieve them back by a string. such that this would be equivalent

Seems to me like a clean solution for separating out legend label from id label? Also "fig.get_artist("myline")" would be great for my use case of putting stuff in functions but then wanting to tweak the artist just a drop when I'm putting it somewhere else. Currently I'm just stashing stuff in a dict.

@timhoffm
Copy link
Member Author

timhoffm commented Jan 7, 2025

I've moved the Artist name/id discussion to #29429.

What about an explicit LegendKeyArtist ? (DataArtist is too semantically overloaded for me)

Maybe. But it could be that this is more specific than we want. I feel a distinction between "data" (everything that is related to the data coordinates) and "decoration" (title, legend, spines, ticks, etc.) could be helpful. - That's not exactly the same as "can have a legend entry" ("decoration" can never have legend entries, but there can be "data" Artists like images that do not fit in a legend either) - but I'd be ok with that level of imprecision. It's not the end of the world if images would continue to have an (unused) label property.

OT: it'd be nice to be able to register artists with the legend in a way that isn't either on creation [...] or all at once

Agreed, but that's orthogonal. You basically have to add a method Legend.add_entry(handle, label) that properly updates the internal state.

@story645
Copy link
Member

story645 commented Jan 8, 2025

feel a distinction between "data" (everything that is related to the data coordinates) and "decoration" (title, legend, spines, ticks, etc.) could be helpfu

So far as I remember, some of the motivation for the data work @ksunden is doing is so you can feed data to labels and ticks. Like the tick location/labeling: the x axis loc/lab are built around the x data values and the y axis loc/lab re built around the y data. Yes there's level of indirection b/c it's fed from the axis but I dunno if we make that distinction anywherem

And I think the most basic artists like patch and Line2D gets reused when making both "plot type (visual idiom)" visual elements and "labeling/annotation" visual elements

But also like you point out, you're making the distinction based on coordinate space - but like axline is mixed coordinate and annotation is whatever coordinate you choose. And if this abstraction will include artists that don't participate in the legend then it seems to not solve the original request of removing label from artists that don't participate in legend.

@timhoffm
Copy link
Member Author

timhoffm commented Jan 8, 2025

We're in a bad situation as Artist.set_label is defined as "Set a label that is displayed in a legend", but this is confusing / not helpful for some Artists that are clearly non-data (c.f. #27971, #29338 (comment)).

There are different ways to improve:

Overall, I'm not clear what the best solution is.

@tacaswell
Copy link
Member

A more radical change here is to add an API to Artist which is something like

def give_me_a_legend_entry(self) -> list[tuple[str, Artist]]:...

and relax label to "some descriptive text". Objects used in a context where they should not have a legend entry (either as decoration or because it makes no sense to be a legend entry for the legend in the legend) could raise or return a "Nope!" value of some sort. This would let us move a whole bunch of the complexity (the multi-layered legend factory registry) to the artist objects and allow for more natural tuning of them at the artist level.

@timhoffm
Copy link
Member Author

timhoffm commented Jan 10, 2025

Maybe I'm not fully understanding this. give_me_a_legend_entry() is an internal-facing API. We can always do that, but that doesn't help on the user-facing API: We won't get a way from plot(..., label="This shows up in the legend"), so label will always stay connected to legend for at least some of the Artists. The question is how do we handle label on on other artists, which are basically the three options from #29422 (comment), and "Nope!" is basically variant 2?

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

No branches or pull requests

4 participants