Repository navigation
Closed
Description
I have some code that continuously updates an MPL plot, and used fig.canvas.start_event_loop(0.0001) to listen for keyboard input. As of MPL 3.4, this code is broken in two ways:
- Commit c53570d converts the timeout to int milliseconds. This rounds down 0.0001 to 0 and fails to display the window at all. I might suggest
math.ceil(1000*timeout)instead ofint(1000*timeout)? - After changing 0.0001 to 0.001 in my code, Commit 630e806 causes MPL to throw an error
invalid command name "140668474408832stop_event_loop" while executing "140668474408832stop_event_loop" ("after" script)when closing the window before pausing the loop. Expected behavior would be for it to simply terminate the loop. It's a harmless error in the minimal code below (you can still launch new runs), but my full code updates using anasyncioevent loop, which this behavior fails to exit, so this error prevents launching new runs in that case (asyncio.run() cannot be called from a running event loop). Not sure how to fix this one, but I'll note that usingfig.canvas.flush_events()in my code instead ofstart_event_loopproduces the desired behavior.
Not sure if #2 is worth fixing given that flush_events() restores the desired behavior, but @richardsheridan suggested I post an issue in my comment on that commit. Minimal example below. Launch an MPL window and then close it before pausing to trigger the error:
from random import randint
import matplotlib, matplotlib.pyplot as plt
from tkinter import *
class Visual:
running = False
#Construct a launch button
def __init__(self):
self.parent = Tk()
self.runButton = Button(self.parent, text='Run', command=self.launchVisual, padx=10, pady=10)
self.runButton.pack(fill="x", side=TOP)
def start(self):
self.runButton['text'] = 'Pause'
self.runButton['command'] = self.stop
self.running = True
while self.running:
self.data.append(randint(0,100))
self.line.set_ydata(self.data)
self.line.set_xdata(range(len(self.data)))
self.axes.relim()
self.axes.autoscale_view(tight=False)
if self.fig.stale: self.fig.canvas.draw_idle()
#This rounds down to 0 and doesn't draw the window at all
#self.fig.canvas.start_event_loop(0.0001)
#This throws an error and breaks asyncio if you close the window before pausing
self.fig.canvas.start_event_loop(0.001)
#This works
#self.fig.canvas.flush_events()
def stop(self, *args):
self.running = False
self.runButton['text'] = 'Run'
self.runButton['command'] = self.start
def terminate(self, evt=False):
self.running = False
self.runButton['text'] = 'New Model'
self.runButton['command'] = self.launchVisual
def launchVisual(self):
matplotlib.use('TkAgg')
self.fig, self.axes = plt.subplots()
self.fig.canvas.mpl_connect('close_event', self.terminate)
#Keyboard input
def pause(event):
if event.key==' ' and event.canvas is self.fig.canvas:
if self.running: self.stop()
else: self.start()
self.fig.canvas.mpl_connect('key_press_event', pause)
self.data = []
self.line, = self.axes.plot(self.data, color='#330099')
self.fig.canvas.draw_idle()
plt.show(block=False)
self.start()
viz = Visual()
viz.parent.mainloop()
Activity
richardsheridan commentedon Apr 11, 2021
These commits were part of #17789. I think we could deal with the granularity of the TCL
aftercommand the same way we did in #18284 fixingTimerTk. As for the error message, I'll try to fix it withafter_cancelin the canvas destroy methods, similar to the logic related todraw_idle.richardsheridan commentedon Apr 12, 2021
Fixing
start_event_looptimeouts less than 0.001 was trivial as expected, but whatever race condition is going on with thedestroymethods is a real deep rabbit hole. If you do useafter_cancel, some other error pops up whereTkis unable to dodeletecommandonstop_event_loop, which looks maybe like aTkinterbug?Also, you may need to use
flush_eventsto drive the Tcl events while integrating with asyncio, but as your example is written, the better option is in principle to useafterto schedule updates:Unfortunately, this appears to trigger yet another error/race when closing the window related to
idle_drawabout half the time... even though the callback seems to be properly cancelled. I'm going to stew on this a little longer, hopefully inspiration will strike.