matplotlib / ipympl

Matplotlib Jupyter Integration
https://matplotlib.org/ipympl/
BSD 3-Clause "New" or "Revised" License
1.57k stars 225 forks source link

add context manager capabilities to ioff when using ipympl #220

Closed ianhi closed 4 years ago

ianhi commented 4 years ago

Often when I'm using the widget backend in jupyterlab and want to do something like define a small layout of ipywidgets and plots I find myself using this pattern:

plt.ioff()
fig = plt.figure()
#... other things I don't want to be interactive
plt.ion()
....
widgets.VBox([slider1,slider2,fig.canvas])
  1. It took me quite a while to figure out that I needed to use ioff and ion to prevent multiple views of the figure showing up. It would be great if more discussion of this was included in the documentation (https://github.com/matplotlib/ipympl/issues/208) and examples
  2. I like the idea of being able to use ioff as a context manager at least in the context of this backend. I propsed this on the matplotlib repo https://github.com/matplotlib/matplotlib/issues/17013 but that may not have been the right place for it as it would be useful primarily for this backend

Could this backend redefine iofflike so:

class ioff:
    def __call__(self):
        matplotlib.interactive(False)
        uninstall_repl_displayhook()

    def __enter__(self):
        self.call()

    def __exit__(self):
        ion()

I imagine this occuring in the ipympl __init__.py. This would enable the above example to be the slightly cleaner:

with plt.ioff:
   fig = plt.figure()
   #... other things I don't want to be interactive
widgets.VBox([slider1,slider2,fig.canvas])

Another issue where the usage of ioff/ion came up is: https://github.com/matplotlib/ipympl/issues/203 though there was no discussion of the context manager there.

Currently I just always import pyplot as plt and just redefine plt.ioff to be the above class which works fairly well.

thomasaarholt commented 4 years ago

First off, I totally agree that we should make the documentation clearer. I've been asked to do this previously but haven't quite finished a first draft of it.

There is actually already a kind of "context manager" that you should be using instead of the plt.ion/ioff approach - ipywidgets.widgets.Output! I was also using your approach until recently. One of the particularly nice things about the Output widget is that you can do all your "figure generation" in one cell, and show it in a different!

The output widget is a "area" widget that contains other widgets, but is not displayed until it is explicitly so, either by returning it at the end of a notebook cell, or calling the IPython.display.display() function on it. (The latter is automatically imported as display in all ipython/jupyter kernels.

So, an similar example to yours becomes the following:

o = widgets.Output()
with o:
    plt.figure()
    plt.plot([1,2,3])
display(o)

Note that

with widgets.Output() as o:
    ...

doesn't work, since the object o is deleted at the end of it. I haven't looked into whether it might be possible to change that.

thomasaarholt commented 4 years ago

I didn't really respond to your suggestion, but I'd be curious what you think, bearing my first reply in mind.

jasongrout commented 4 years ago

Think of an output widget as a way of capturing output that would normally be displayed in a cell output.

In other words, it is a container for output, not a container for widgets. Naturally, since an output can contain widgets, an output widget can contain widgets alongside text and other rich displays.

tacaswell commented 4 years ago

The context manager discussion should definitely happen in matplotlib/matplotlib (I left a comment over there already).

The current model that pyplot uses for managing open figures is very tied to desktop GUIs (as that is what we were working with) and the idea that you only have 1 view of it.

If I am understanding this right, the issue is that sometimes you want plt.figure() to make a stand-alone widget that get put on the screen and other times you want it to make the widget, but not display it because later you are going to manually pack it into a bigger layout?

ianhi commented 4 years ago

If I am understanding this right, the issue is that sometimes you want plt.figure() to make a stand-alone widget that get put on the screen and other times you want it to make the widget, but not display it because later you are going to manually pack it into a bigger layout?

Yup! I ran into this when working with XYZT stacks of microscopy images. I have several classes in python module that manage viewing these stacks and for things like per image manual segmentation (lasso selector) or cell labelling. Then these classes all have methods like:

    def __init__(self,arr):
        """
        inputs
        --------
        arr : structured array
             with attributes, 'pos', 'time', 'z', 'name', 'image (X,Y)' 
        """
        ....
        # boring initialization stuff
        plt.ioff()
        self.fig = plt.figure()
        plt.ion()
        self.ax = plt.gca()

        self.displayed = self.ax.imshow(self.arr['image'][self.t_idx][0])
        # boring initialization + defining some control sliders

    # other functions

    def _ipython_display_(self):
        display(widgets.VBox([self.z_slider,self.t_slider,self.fig.canvas]))

There is actually already a kind of "context manager" that you should be using instead of the plt.ion/ioff

I don't think that the output widget is the best way to solve my problem. It certainly works, but I'd prefer for no output to be generated when creating my object rather than to have to capture extraneous output. My bad on not posting an example of my actual use case.

Though, this is great to know about for the original example I posted which I definitely run into when playing around with things in the notebook. It's also interesting to thing about throwing these into a sidecar output.

Why is it preferred to use the output rather than suppress the output? It seems like this makes it more difficult to manipulate the canvas position with various widget layout tools.

I've been asked to do this previously but haven't quite finished a first draft of it.

If you want feedback on a draft I'd be happy to provide some, my learning of all this is fresh enough that I'll bet I remain capable of wildly misinterpreting documentation so I could be a stress test for it...

ianhi commented 4 years ago

Context manager has now happened in matplotlib :tada: !

I think it will be included in the 3.4 release. Usage will be:

with plt.ion():
     # ...