Skip to content

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

Closed
wants to merge 1 commit into from

Conversation

anntzer
Copy link
Contributor

@anntzer anntzer commented Oct 31, 2020

#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 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.

PR Summary

PR Checklist

  • Has pytest style unit tests (and pytest passes).
  • Is Flake 8 compliant (run flake8 on changed files to check).
  • New features are documented, with examples if plot related.
  • Documentation is sphinx and numpydoc compliant (the docs should build without error).
  • Conforms to Matplotlib style conventions (install flake8-docstrings and run flake8 --docstring-convention=all).
  • New features have an entry in doc/users/next_whats_new/ (follow instructions in README.rst there).
  • API changes documented in doc/api/next_api_changes/ (follow instructions in README.rst there).

@anntzer anntzer force-pushed the uniform-manager-creation branch from 5c03446 to 9fda6eb Compare October 31, 2020 12:32
@anntzer anntzer mentioned this pull request Dec 16, 2020
7 tasks
@anntzer anntzer added the status: needs comment/discussion needs consensus on next step label Mar 1, 2021
@anntzer anntzer force-pushed the uniform-manager-creation branch 3 times, most recently from 832d219 to 10edb9a Compare March 1, 2021 14:50
@anntzer anntzer force-pushed the uniform-manager-creation branch from 10edb9a to 0eeb15c Compare March 4, 2021 08:43
@timhoffm
Copy link
Member

timhoffm commented Mar 6, 2021

I suppose this is reasonable, but need to think more about it.

@jklymak jklymak marked this pull request as draft May 20, 2021 23:52
@anntzer anntzer force-pushed the uniform-manager-creation branch from 0eeb15c to 04368ed Compare February 3, 2022 18:48
@tacaswell
Copy link
Member

Can we put FigureManager as a class attribute on FigureCanvas and make almost all of the new_manager implementations use the base one?

@anntzer anntzer marked this pull request as ready for review February 3, 2022 18:55
@anntzer
Copy link
Contributor Author

anntzer commented Feb 3, 2022

Reopening the PR based on #22380.

@anntzer
Copy link
Contributor Author

anntzer commented Feb 3, 2022

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.

@anntzer anntzer force-pushed the uniform-manager-creation branch from 04368ed to 2a20f69 Compare February 3, 2022 19:04
@tacaswell
Copy link
Member

I think the logic here make a lot of sense.

  • Figure : this is the Matplotlib side outer-most container which is responsible for knowing its own absolute size, layout out (some of) its children, and being the top of the "draw tree" because it owns a canvas
  • FigureCanvas is the thing that is responsible for producing renders and for being the object that straddles the fence between Matplotlib and what ever toolkit we are embedding in. This has to know about the Figure (incase the toolkit wants to push resizes back) so we unfortunately we have a cycle
  • FigureManager is the embedding of the Canvas in a fully armed and operational battlestation GUI top level window that has a toolbar, keyboard shortcuts, etc all wired up.

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 show_managers static / class method. If we do that we may be able to make mainloop() private?

@anntzer
Copy link
Contributor Author

anntzer commented Feb 4, 2022

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.)

@tacaswell tacaswell mentioned this pull request Feb 17, 2022
7 tasks
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.
@anntzer anntzer force-pushed the uniform-manager-creation branch from 2a20f69 to 8bb2bb1 Compare April 18, 2022 17:58
@anntzer
Copy link
Contributor Author

anntzer commented Apr 18, 2022

@tacaswell @timhoffm Do we want to try to push this forward?

@timhoffm
Copy link
Member

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?

@anntzer
Copy link
Contributor Author

anntzer commented Apr 18, 2022

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.

@timhoffm
Copy link
Member

@anntzer Thanks for the detailed explanation! I'll try to wrap my head around this later.

@tacaswell tacaswell added this to the v3.6.0 milestone Apr 18, 2022
@tacaswell
Copy link
Member

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)
Copy link
Member

@timhoffm timhoffm Apr 19, 2022

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:

  1. 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.

  2. 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.

@anntzer
Copy link
Contributor Author

anntzer commented Apr 19, 2022

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 FigureManager(FigureCanvas(Figure()), num)" (or FigureCanvas(Figure()).attach_manager(), which makes the FigureManager implicit) cannot work (even though I tried very hard for that in #10606), because they create a standalone canvas first. One could make the FigureManager class an attribute of FigureCanvas and provide a new_manager(canvas_class, figure, num) but this doesn't really help because separate backends would need separate implementations of new_manager (for most backends, it would be return canvas_class.manager_class(canvas_class(figure), num) but wx would do a different thing).

(Actually, I did think of one other way to hack around the wx limitation, which is to abuse FigureCanvasWx.__new__ to make FigureCanvasWx(Figure()) return a placeholder object (not a FigureCanvasWx instance) that is recognized by FigureManagerWx, so that FigureManagerWx(FigureCanvasWx(Figure())) actually does the right thing, including instantiating an actual FigureCanvasWx (not the same object) which is accessible as manager.canvas; but that seemed too magical to me. If everyone else prefers this solution, we can probably choose that too...)

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.

@timhoffm
Copy link
Member

timhoffm commented Apr 19, 2022

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.

@timhoffm
Copy link
Member

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 FigureManager.create_with_canvas(canvas_class, …) That way, responsibility for wiring everything up is at the manager class. We still might want the Canvas.new_manager method for convenience but it should only delegate to the correct manager and not implement any custom logic (e.g. for the wx case).

@anntzer
Copy link
Contributor Author

anntzer commented Apr 19, 2022

@timhoffm I am happy to discuss your proposals, but to keep the discussion focused, can you specify whether the APIs you propose (new_manager, for now) are supposed to be 1) free-standing functions in a backend-independent module, or 2) free-standing functions provided by each backend? (or, if you propose further APIs -- they could also be 3) methods on the backend-specific FigureCanvas subclass.)

@timhoffm
Copy link
Member

@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.

@timhoffm
Copy link
Member

timhoffm commented Apr 25, 2022

See #22895 for the alternative approach I suggest.

It's probably most easy to understand if you only look at the changes I made to your code here: 645204f

@timhoffm
Copy link
Member

timhoffm commented May 1, 2022

Superseeded by #22925.

@timhoffm timhoffm closed this May 1, 2022
@anntzer anntzer deleted the uniform-manager-creation branch May 1, 2022 17:27
@anntzer anntzer mentioned this pull request May 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: needs comment/discussion needs consensus on next step
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants