Skip to content
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

Updating GridspecLayout on click #2942

Closed
haykh opened this issue Aug 4, 2020 · 5 comments
Closed

Updating GridspecLayout on click #2942

haykh opened this issue Aug 4, 2020 · 5 comments
Labels
resolved-locked Closed issues are locked after 30 days inactivity. Please open a new issue for related discussion.

Comments

@haykh
Copy link

haykh commented Aug 4, 2020

Hi, thanks to the developers for putting this thing together -- it looks awesome!

I have a usage question though. I'm trying to make a layout of plots with NxM rows and columns. Right now I'm prototyping with just buttons. The idea is to be able to expand the layout (i.e. add new panels) by clicking on one of the buttons.

Here's a class I have so far (probably not the best way, but anyway).

import ipywidgets as ipyW
from IPython.display import display

class PlotGrid():
  def __init__(self, nn):
    self.panels = []
    for n in range(nn):
      self.createNewPanel(n=n)
    self.redraw()
  
  def createNewPanel(self, **kwargs):
    button = ipyW.Button(description='Button {}'.format(kwargs['n']), 
                         layout=ipyW.Layout(height='auto', width='auto'))
    button.on_click(self.appendGrid)
    self.panels.append(button)
    
  def appendGrid(self, status):
    self.createNewPanel(n=len(self.panels))
    self.redraw()
  
  def redraw(self):
    ncols = int(np.sqrt(len(self.panels)))
    nrows = int(np.ceil(len(self.panels) / ncols))
    self.grid = ipyW.GridspecLayout(nrows, ncols)
    n = 0
    for i in range(self.grid.n_rows):
      for j in range(self.grid.n_columns):
        if (n < len(self.panels)):
          self.grid[i, j] = self.panels[n]
        n += 1
  def show(self):
    display(self.grid)

So when I run this:

plotgrid = PlotGrid(12)
plotgrid.show()

it does what it should: fills in the .grid object with panels. When I click on the button the appendGrid function is indeed called, and it does append the new panels to the grid as it should. But for some reason the result is not displayed in the cell: it just remains the same. If I later update the cell by running plotgrid.show() again -- it draws the new extended .grid as it should.

So I think I'm not understanding something fundamental with how the display function works and Jupyter handles the events.


These are the versions of the relevant packages I got through conda:

ipykernel                 5.2.0            py38h23f93f0_1    conda-forge
ipython                   7.13.0           py38h32f6830_2    conda-forge
jupyterlab                2.1.0                      py_0    conda-forge
widgetsnbextension        3.5.1                    py38_0    conda-forge
@haykh
Copy link
Author

haykh commented Aug 5, 2020

After thinking a bit about this, I was able to solve the problem like this:

class PlotGrid():
  def __init__(self, nn):
    self.plotgrid = ipyW.Box() # <----
    self.panels = []
    for n in range(nn):
      self.createNewPanel(n=n)
    self.redraw()
  
  def createNewPanel(self, **kwargs):
    button = ipyW.Button(description='Button {}'.format(kwargs['n']), 
                         layout=ipyW.Layout(height='auto', width='auto'))
    button.on_click(self.appendGrid)
    self.panels.append(button)
    
  def appendGrid(self, status):
    self.createNewPanel(n=len(self.panels))
    self.redraw()
  
  def redraw(self):
    ncols = int(np.sqrt(len(self.panels)))
    nrows = int(np.ceil(len(self.panels) / ncols))
    grid = ipyW.GridspecLayout(nrows, ncols)
    n = 0
    for i in range(grid.n_rows):
      for j in range(grid.n_columns):
        if (n < len(self.panels)):
          grid[i, j] = self.panels[n]
        n += 1
    self.plotgrid.children = [grid] # <----
  def show(self):
    display(self.plotgrid)

Looks a bit hacky, but the idea is that I don't rewrite the object, but rather have a constant one and just update its children.

Will keep the issue open for a bit, in case people have other solutions.

@ianhi
Copy link
Contributor

ianhi commented Aug 10, 2020

Hey @haykh your solution is probably the best approach, so congrats on figuring that out! I think the issue was that display doesn't work quite the way you'd expect. If you change the objects that went into a display they will not immediately update. Instead you need to explicitly update the display (you can get a reference to it by providing an id). Like so:

cell 1:

some_var = 'a'
display_handle = display(some_var, display_id='some-unique-id')
some_var = 'b'

Will have a as the output
cell 2:

display_handle.update(some_var)

now cell 1's output will be b.

You can read more about this here, here and here. I would particularly recommend implementing the _ipython_display_ method on your class.

In your working example you are circumventing having to do something like that by using the widget update system. The Box object listens for changes to it's children and updates any views of itself whenever it's children changes. Unfortunately that is the best solution as my example above with update_display doesn't actually work with ipywidgets, which is a known bug #1180

@haykh haykh closed this as completed Aug 10, 2020
@ianhi
Copy link
Contributor

ianhi commented Aug 10, 2020

Out of curiosity are you going to be using matplotlib to generate these plots? And if so are you using the ipympl backend? If yes to both then a piece of pre-emptive advice is to wrap the figure creation into a context where matplotlib's interactive mode is off i.e. plt.ioff(); fig = plt.figure(); plt.ion() and then explicitly display the fig.canvas whereever you want to put it.

Alternatively create the figure in the context of an Output widget and then display that output widget whereever you want. Both of these solutions prevent the figure being displayed twice

@haykh
Copy link
Author

haykh commented Aug 10, 2020

I got something like this (modulo all the interactive stuff):

import matplotlib.pyplot as plt
import ipywidgets as ipyW
from IPython.display import display

# i think this enables ipympl backend:
%matplotlib widget 

class myPlot():
  def __init__(self):
    output = ipyW.Output()
    plt.close('all')
    with output:
      self.fig, self.ax = plt.subplots()
    self.ax.plot([0, 1], [0, 1])
    
  def show(self):
    display(self.fig)
    
plot = myPlot()
plot.show()

So, yes, I use the output widget. But maybe indeed fig.canvas is a bit cleaner.

@ianhi
Copy link
Contributor

ianhi commented Aug 10, 2020

i think this enables ipympl backend:

Yup it does!

Your approach is totally reasonable, it's the "ipywidgets" solution whereas I was suggesting the "matplotlib" solution. More about this in this unmerged PR updating ipympl examples: matplotlib/ipympl#244

I see you're using display(fig). Beware that display(fig) and display(fig.canvas) are actually pretty different. The former will display a static png, while the latter will create an interactive plot so you can can pan, zoom, and easily update the contents.

@github-actions github-actions bot added the resolved-locked Closed issues are locked after 30 days inactivity. Please open a new issue for related discussion. label Feb 7, 2021
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 7, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
resolved-locked Closed issues are locked after 30 days inactivity. Please open a new issue for related discussion.
Projects
None yet
Development

No branches or pull requests

2 participants