-
-
Notifications
You must be signed in to change notification settings - Fork 7.8k
Standardize creation of FigureManager from a given FigureCanvas class. #18854
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
Conversation
5c03446
to
9fda6eb
Compare
832d219
to
10edb9a
Compare
10edb9a
to
0eeb15c
Compare
I suppose this is reasonable, but need to think more about it. |
0eeb15c
to
04368ed
Compare
Can we put |
Reopening the PR based on #22380. |
We can make FigureManager an attribute, although here we'd be saving even fewer lines than in #22250 so I'm not sure it's really worth it. In particular, note that end users cannot use that class to instantiate arbitrary figuremanagers (due to the inconsistent signatures, which is exactly why they should use new_manager instead); I guess they could use that for instancechecks but then they may just as well do an instancecheck on the canvas (which should be preferred IMO because that's less attached to pyplot). Also wrt #22380, probably another needed step is to move mainloop() to become a staticmethod on either the canvas or the manager (perhaps rather a manager method as it should only ever be used in a pyplot context?). But that can easily wait after this PR, I think. |
04368ed
to
2a20f69
Compare
I think the logic here make a lot of sense.
The figure is backend independent, and then once the Canvas is attached you can walk up its attributes to find the rest of the parts of the backend that you may need (without having to consult any other global state that may be out of sync). If think I agree on the static method, although it seems for tk we need access to at least one of the Managers to extract the main loop. In mpl_gui I was experimenting with a |
Yeah, I noticed the problem with tk. Well, we can decide on that once we get there; let's not derail this PR on that discussion. (But I think we need mainloop as a customization point for the backends? There's a lot of surrounding logic in show_managers which probably needs not to be repeated again and again.) |
The `new_manager` classmethod may appear to belong more naturally to the FigureManager class rather than the FigureCanvas class, but putting it on FigureCanvas has multiple advantages: - One may want to create managers at times where all one has is a FigureCanvas instance, which may not even have a corresponding manager (e.g. `subplot_tool`). - A given FigureManager class can be associated with many different FigureCanvas classes (indeed, FigureManagerQT can manage both FigureCanvasQTAgg and FigureCanvasQTCairo and also the mplcairo Qt canvas classes), whereas we don't have multiple FigureManager classes for a given FigureCanvas class.
2a20f69
to
8bb2bb1
Compare
@tacaswell @timhoffm Do we want to try to push this forward? |
I feel I understand too little of the mechanisms involved to be able to judge this. Would it make sense to write up the design, possibly as part of the docs? I think @tacaswell's comment above (#18854 (comment)) is a good start. What's still missing there is the relationships. Do we have a 1:1 relation between figure and canvas and manager? Also, who owns / holds references to the others? |
No worries, I pinged you as you expressed interest earlier on this thread. Perhaps @tacaswell will have more to say. The Figure is the base Artist of the Artist tree; it is agnostic as to how it gets concretely represented. The FigureCanvas is responsible for generating this concrete representation: as a RGBA array painted on a GUI widget, as a RGBA array that goes to a file, as some other vector representation that goes to a file. A given canvas may be able to generate multiple representations (if it has multiple print_foo methods), or perhaps none (the base FigureCanvasBase, which has no print_foo). There is a reference cycle between the Figure and the FigureCanvas, but a Figure can swap its canvas. Indeed, all Figures start with a FigureCanvasBase attached. For pyplot-managed figures, this quickly gets swapped by the pyplot machinery to the FigureCanvas subclass specified by the active pyplot backend. The other time where FigureCanvases are swapped is in Figure.savefig, which checks if the current canvas can generate the requested representation, and if not, temporarily swaps in an appropriate canvas (e.g. FigureCanvasPdf to print to pdf) and restores the original canvas after the representation is generated. It is also important to note that interactive FigureCanvas classes are typically subclasses of the native GUI widget class (QtWidget, wx.Window (which is really a "widget"), etc.), and in particular, in the case of wx, such a widget cannot exist independently of an owning toplevel window, which imposes certain restrictions on API design. For example, we cannot say "the backend-generic API to generate a FigureCanvas is FigureCanvas(figure)", because that cannot work for wx. The FigureManager owns the embedding of the FigureCanvas in a GUI Window, also handling toolbar, keyboard shortcuts, etc. (or a "null" object for non-interactive backends) for pyplot. There is a reference cycle between the FigureCanvas and the FigureManager. In my view, FigureManagers are a pyplot concept, and custom embeddings of Matplotlib into GUI toolkits should embed FigureCanvases instead, although (if I understand correctly) #8777 tries to make FigureManagers embeddable independently of pyplot (I don't agree, but in any case this PR doesn't change the statu quo, AFAICT). For a given FigureCanvas class, there is typically only one FigureManager class that makes sense, but a same FigureManager class can be used for multiple FigureCanvas classes: for example, QtAgg and QtCairo and mplcairo.qt have different canvas classes (using different approaches to render on the canvas), but share the same FigureManagerQt class (the embedding into a Qt window is the same). As explained at the top, it is sometimes useful to generate a FigureManager on a new canvas when owning just a canvas which may not even be pyplot-controlled (e.g. I have my own embedding and want to pop up a subplot-layout control window). This PR makes this possible by adding a class method on the FigureCanvas class that allows creating a new manager (for a new canvas) but using the same canvas class as the current FigureCanvas. |
@anntzer Thanks for the detailed explanation! I'll try to wrap my head around this later. |
https://github.com/matplotlib/mpl-gui is also part of my now ~5year quest to make manager more useful outside of pyplot. I co-sign @anntzer 's description of how this all works. I agree we need some version of my previous message and @anntzer 's last post in the docs but do not want to hold this PR on that. |
@classmethod | ||
def new_manager(cls, figure, num): | ||
# docstring inherited | ||
return FigureManagerGTK3(cls(figure), num) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find it a bit odd that we code the relation between the canvas and the matching manager into a class method on the canvas. Also the hierarchy feels unclear.
Two alternative suggestions:
-
How about storing this information either in a canvas class attribute, or in global dict? That way, we could have a standalone function
new_manager(canvas_class, figure, num)
, which feels a bit better than having a method on the canvas that creates a manager. -
Or one goes the alternative route and considers the manager to be an optional part of the canvas.
canvas = [make the canvas instance] canvas.attach_manager()
Possibly there is a better name than attach. But the point here is that you amend an existing canvas instance. The original
new_manager
proposal has the canvas class as entry point, but OTOH you get the manager, that holds a reference to the canvas. In my view this mixes the priority/hierarchy. In the first part, the canvas seems to be the leading object, whereas in the second the manager seems to take over.
The APIs you propose are fairly natural, and I have tried to make some variation of them work in #10606, but they are made unworkable by one fundamental limitation: because of wx's design, a FigureCanvasWx cannot even be instantiated without first having a parent embedding window -- either a FigureManagerWx in pyplot's case, or some other user-owned window in custom embedding case. (See #10606 for detailed descriptions.) This means that the natural API of saying "all backends must support (Actually, I did think of one other way to hack around the wx limitation, which is to abuse The other option would be to say, OK, let's have a different new_manager() per backend -- and then the first parameter canvas_class could be dropped, as it is specified by the backend, effectively resulting in the new_figure_manager_given_figure API. But this basically brings me to the part I forgot to write above, which is: how does this API design relate to pyplot? Remember that plt.figure() basically wants to be able to create a new manager from a naked Figure() object (either a Figure() it has just instantiated itself, or an unpickled one in the case of pickle support (pickling drops canvases, as GUI widgets cannot be pickled). This is why the "classical" backend API is that backend modules must provide a new_figure_manager() function and a new_figure_manager_given_figure() function; and a few more, e.g. for event loop handling, which I briefly mention above and will take care of in a future PR. However, this pattern doesn't "inherit" well: there's no way qtagg and qtcairo and mplcairo.qt can share these functions with different canvas classes but the same manager, so they got reimplemented again and again, leading to a lot of copy-paste code. In #8773 I introduced a private _Backend class that allows dynamically generating these functions, but the API is a bit awkward and requires yet another (_Backend) helper class. Here, by storing everything on the FigureCanvas class, this makes it possible for backend_qtagg/backend_qtcairo/mplcairo.qt to just import FigureCanvasQt from backend_qt, which implicitly pulls in FigureCanvasQt.new_manager, and just override whatever it wants. |
This is tricky. do I understand this correctly: sometimes I can have canvases without a manager But for WX I need a manager first to be able to instantiate the canvas? I still feel the relationship between the classes is not quite right and there should be a better way. However that might need a more fundamental rethinking of the architecture. Anyway, option (1) from above should work for now. It’s only a slight rewriting of your proposal: we attach the manager class To the canvas class as an attribute instead of writing it into the new manager method explicitly. And then take the class method new_manager Out as a standalone function and pass the canvas class in. - The conceptual idea behind this: The canvas class knows with which manager it can work together, but it’s not the responsibility of the canvas class to create managers. |
One alternative idea (sorry if this is a bit piecemeal): If the standalone new_manager function doesn’t cut it for some reason, delegate the canvas creation to the manager via a factory method |
@timhoffm I am happy to discuss your proposals, but to keep the discussion focused, can you specify whether the APIs you propose ( |
@anntzer sure, I will write up a more concise proposal in a couple of days (I’m away from my dev machine and larger posts and code inspections don’t work well on a mobile phone. |
Superseeded by #22925. |
#16818 showed that it would be nice to have a way to create a new manager
with the same backend as a given canvas, even when operating outside of
pyplot (so the global backend may not exist). This PR implements such a
feature as the
FigureCanvas.new_manager
classmethod.The
new_manager
classmethod may appear to belong more naturally to theFigureManager class rather than the FigureCanvas class, but putting it
on FigureCanvas has multiple advantages:
FigureCanvas instance, which may not even have a corresponding manager
(e.g.
subplot_tool
).FigureCanvas classes (indeed, FigureManagerQT can manage both
FigureCanvasQTAgg and FigureCanvasQTCairo and also the mplcairo Qt
canvas classes), whereas we don't have multiple FigureManager classes
for a given FigureCanvas class.
PR Summary
PR Checklist
pytest
passes).flake8
on changed files to check).flake8-docstrings
and runflake8 --docstring-convention=all
).doc/users/next_whats_new/
(follow instructions in README.rst there).doc/api/next_api_changes/
(follow instructions in README.rst there).