0

EDIT: Added import statements to make code example completely self-contained.

I'm trying to UPDATE pyplot charts that have already been drawn IN PLACE, without drawing a new chart. I found this thread on the subject and I think I followed all the advice it contained, but I still can't get it to work. That thread was about updating a SINGLE figure. I'm trying to switch back and forth between two figures, and I think that's what causing the problems. It feels like somehow switching back to the already active figure with plt.figure() is creating a new figure instead of re-activating the existing one.

Here's my code:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import ipywidgets as widgets
import time 

    def display_charts( data1, data2, fig1, fig2 ):

#    Plot charts

    #plt.figure(fig1.number) # Intended to make EXISTING figure current, without creating new one

    fig1.clf()       # I was hoping this would clear the figure allowing it to be re-drawn in-price, but
                     # it doesn't seem to help
    ax=fig1.gca()     # Get axes for this figure

    ax.clear()       # Tried this based on Stack Overflow thread, but doesn't seem to help

    data1.plot.line(ax=ax)

    fig1.canvas.draw()           # Got these two lines from a stack overflow discussion about redrawing
    fig1.canvas.flush_events()   # charts in-place but they don't seem to help.

    #plt.figure(fig2.number)
    fig2.clf()
    ax2 = fig2.gca()
    ax2.clear()
    data2.plot.bar(ax=ax2)

    fig2.canvas.draw() 
    fig2.canvas.flush_events()



def update_button_callback(_):
    # My hope was that this would allow the charts to be re-drawn in place with new data. But that
    # doesn't work. Instead, when the button is clicked, 2 new smaller charts are drawn under the
    # original ones.

    display_charts( df3, df4, fig1, fig2 ) # I want this to update the charts in place, not draw them a 2nd time!!!

# Set up sample data for illustration

df1 = pd.DataFrame([1.2,3.4,5.6,7.8,9.0],[3.4,5.6,7.8,9.0,1.2])
df2 = pd.DataFrame([11.2,13.4,15.16,17.18,19.0],[13.14,15.16,17.8,19.0,11.2])
df3 = pd.DataFrame([21.2,23.4,25.6,27.8,29.0],[23.4,25.6,27.8,29.0,21.2])
df4 = pd.DataFrame([31.2,33.4,35.6,37.8,39.0],[33.4,35.6,37.8,39.0,31.2])

# Configure and display UI controls

plt.ion()               # I don't think this should be required, but threw it in just in case...

fig1=plt.figure(figsize=(30,20)) # These figure sizes get used the FIRST time the charts are drawn, but
fig2=plt.figure(figsize=(30,20)) # when I try and update in place, the result is to draw new charts which
                                 # default to a smaller display size.

update_button = widgets.Button(description="Update Chart")
display(update_button)

update_button.on_click(update_button_callback)

display_charts(df1,df2, fig1, fig2) # Display first 2 df's. Button click callback will update them.
plt.show()

time.sleep(5) # Isolate out any issues with the button control being the problem by waiting 5 seconds
              # then calling display_charts() a 2nd time with the other df's.

print("Updating charts now...")
display_charts(df3,df4, fig1, fig2) # Display first 2 df's. Button click callback will update them.
plt.show()

time.sleep(10)
print("exiting cell")

I realize that the calls to ax.clear() are redundant after fig.clf(). I was just trying to cover every possible base. Also, I tried just about every conceivable combination of omitting or including each of the clear, clf, draw and flush operations. Nothing I do seems to work.

Curiously, the figure sizes set in the main code (at the bottom) are used the FIRST time the display_charts() function runs. But all subsequent executions result in new charts being displayed at the much smaller default figure size. That leads me to conclude that new figures are being created rather than old ones being updated, but I can't figure out why.

Thanks in advance for any insights!!!

p.s. The reason I'm using two separate figures rather than 2 subplots in a single figure is that in the actual code (this post contains a simplification for the sake of clarity), there are completely different xticks and yticks settings and a bunch of other figure settings are different. There are also 3 rather than 2 charts. The real code was stripped down to the simplest version that reproduces the problem, shown above.

erik townsend
  • 37
  • 1
  • 6
  • What is `widgets.Button`? Where are you running this? Can you make it a self-contained code, see [mcve]? – ImportanceOfBeingErnest Sep 08 '19 at 17:33
  • widgets.Button is a standard Jupyter widget well-documented here: https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Events.html. This is already a minimal reproducible example, and the function of the button is simply to wait for user input before re-drawing the chart. Replace it with a time.sleep(60) if for some reason that's more intuitive. Both are standard parts of the IPython environment. – erik townsend Sep 08 '19 at 18:35
  • I see. Your code works fine with the `%matplotlib notebook` backend. Using the `%matplotlib inline` backend you have the problem that the pyplot figures are closed once they are diplayed. Hence there is no figure with number 1 at the moment you call `plt.figure(1)`. You could of course pass the figure to the function when calling it `display_charts(df1,df2, figure=fig1)` or so. – ImportanceOfBeingErnest Sep 08 '19 at 19:01
  • I tried changing it to pass fig1, fig2 as arguments to display_charts(), then at the bottom i added a 5-sec time.sleep and a 2nd call to display_charts(df3,df4,fig1,fig2), and I still get the same result - the chart never updates. You're saying it does for you? Thanks in advance! – erik townsend Sep 08 '19 at 19:33
  • No it doesn't update; but it should now use the same figure and display it a second time, right? (If not, you can update the code in the question such that one can see what you've done. Also, again, a complete runnable code would be nice, it's still having some undefined things in it.) – ImportanceOfBeingErnest Sep 08 '19 at 19:37
  • Prior version should have run self-contained. I am on Jupyter notebook in case that wasn't obvious. Just edited to latest version which uses time.sleep to isolate out possible issues with the button callback. Oddly, this version displays two lines stating the dimensions of the plot, but no chart on the first execution. Run it a 2nd time and it produces charts, but no in-place update. After the sleep 2 additional charts are separately displayed, with a smaller (default) figsize. – erik townsend Sep 08 '19 at 19:58
  • The bizarre part (possibly helpful for diagnosis?) is that my original was 2 Jupyter cells, import statements in the first, everything else in 2nd cell. That version ran and displayed charts on the first run. When I moved everything into one cell (to facilitate copying self-contained code here), that caused behavior to change so it doesn't actually show charts till you run it a 2nd time. As if at least one Jupyter cell must have finished running before any charts will display. Weird. – erik townsend Sep 08 '19 at 20:01
  • Yes, that is a known problem with IPython. Best use the first cell for imports and start code in the second. – ImportanceOfBeingErnest Sep 08 '19 at 20:03
  • You still have `plt.figure(fig1.number)` in your code. The purpose of passing the figures to the function would of course be to *use* them instead of creating a new pyplot figure. – ImportanceOfBeingErnest Sep 08 '19 at 20:05
  • I'm missing something... I'm passing fig1 (a Figure object) to the function, then using the .number attribute of that figure object as the argument to plt.figure. Are you suggesting I should call the function using fig1.number as the argument to display_charts, then pass that as the argument to plt.figure()? Why would that be different? I feel like I'm missing something obvious but not sure what... – erik townsend Sep 08 '19 at 20:13
  • And for sake of clarity... My understanding from the documentation is that plt.figure(existing-figure) makes that figure the current figure without creating a new figure. Are you saying that's not what it does? – erik townsend Sep 08 '19 at 20:17
  • `plt.figure(1)` creates a new figure in case there isn't already a figure with figure number `1` present. As said earlier, since the inline backend closes the figure, at the moment you use `plt.figure(1)` there is no figure with number 1 in the pyplot state machine. Hence a new figure will be created. As a consequence, if you want to use the inline backend, you need to avoid making use of the pyplot state machine. – ImportanceOfBeingErnest Sep 08 '19 at 20:31
  • Pretty sure I understand what you mean now. Code updated to reflect it. I had tried this before. It never updates the chart to the new data. Chart stays the same to the very end. I also added code at the end to keep the cell running long after the update had been attempted. Also tried both plt.draw() and plt.show() to make the update visible, neither worked. plot.show() still in the version just updated. – erik townsend Sep 08 '19 at 20:31
  • Yep, `plt.show()` cannot show anything because there is nothing in `plt` to show (reason explained above, all figures are closed). You can still `display` the figure though, `display(fig1)`. – ImportanceOfBeingErnest Sep 08 '19 at 20:37
  • Ok, we're still talking past one another. The very first version I posted displayed the charts TWICE. That was the whole PROBLEM. The point of the thread is to figure out how to get the charts to UPDATE IN PLACE rather than re-displaying them. By following your advice and using display(fig1) at the end, the result is to display a new chart in addition to the prior one. That was the whole PROBLEM we were trying to solve. How does this help? Or am I still missing something? – erik townsend Sep 08 '19 at 22:15
  • Mhh, you need to be aware of the fact that you are displaying png images. Those cannot be "updated", so the option here is to remove the old image and display the new one. I added such solution below. – ImportanceOfBeingErnest Sep 08 '19 at 22:18

1 Answers1

0

Here is one option, clearing the output upon pressing the button.

#Cell 1
%matplotlib inline

#Cell 2
import matplotlib.pyplot as plt
import pandas as pd
import ipywidgets as widgets
from IPython.display import display, clear_output
import time

#Cell 3
def display_charts(data1, data2):
    ax1.clear()
    data1.plot.line(ax=ax1)

    ax2.clear()
    data2.plot.line(ax=ax2)

df1 = pd.DataFrame([1.2,3.4,5.6,7.8,9.0],[3.4,5.6,7.8,9.0,1.2])
df2 = pd.DataFrame([11.2,13.4,15.16,17.18,19.0],[13.14,15.16,17.8,19.0,11.2])
df3 = pd.DataFrame([21.2,23.4,25.6,27.8,29.0],[23.4,25.6,27.8,29.0,21.2])
df4 = pd.DataFrame([31.2,33.4,35.6,37.8,39.0],[33.4,35.6,37.8,39.0,31.2])


fig1, ax1 = plt.subplots(figsize=(8,6)) 
fig2, ax2 = plt.subplots(figsize=(8,6)) 

def update_button_callback(evt):
        clear_output(True)
        display(update_button)
        display_charts(df3, df4)
        display(fig1)
        display(fig2)

update_button = widgets.Button(description="Update Chart")   
update_button.on_click(update_button_callback)

display(update_button)
display_charts(df1,df2)
ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • I already had this workaround in place before posting the original post. Let's reframe this whole discussion. The question at hand (per the title of the thread) is how to RE-DRAW charts IN PLACE. After reviewing your many comments, it now seems clear that you are not aware of any way to do that. Is that the bottom line? It would have been easier had you said that up front... – erik townsend Sep 08 '19 at 23:34
  • I don't think this is about me being aware of something or not. My very first comment suggests to use the `%matplotlib notebook` backend. It seems that you do not consider that as option. So if you want to continue using the `%matplotlib inline` backend, there is no way to redraw anything inplace. That's a simple technical fact. Once a png image is created and added to the DOM tree of the notebook, it cannot be changed anymore. The only option is then to replace that image with a new image - and that is what this answer does. – ImportanceOfBeingErnest Sep 09 '19 at 00:10