I have been working on a problem for a while (I am a chemical engineer, so it takes me forever to understand how to code something) of how to have several tabs operating in their own process, but with each tab having it's own data to show in a matplotlib plot.
I have been running into many pickling errors and I wondered if anyone had any somewhat simple solutions. I believe the main reasons for the pickling errors is due to the object I am trying to pass as a property into the tab object. This object holds some data as well as many other objects which help fit the data it holds.
I feel like these objects are very nice and rather necessary, but I also realize that they are causing the problems with pickling.
Here is a very simplified version of my code:
(This will still compile if you want to copy/paste to test it out.)
import multiprocessing as mp
from PyQt4 import QtGui, QtCore
import numpy as np
import matplotlib
matplotlib.use('QtAgg')
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib import figure
import sys
import lmfit
# This object will just hold certain objects which will help create data objects ato be shown in matplotlib plots
# this could be a type of species with properties that could be quantized to a location on an axis (like number of teeth)
#, which special_object would hold another quantization of that property (like length of teeth)
class object_within_special_object:
def __init__(self, n, m):
self.n = n
self.m = m
def location(self, i):
location = i*self.m/self.n
return location
def NM(self):
return str(self.n) + str(self.m)
# This is what will hold a number of species and all of their properties,
# as well as some data to try and fit using the species and their properties
class special_object:
def __init__(self, name, X, Y):
self.name = name
self.X = X
self.Y = Y
self.params = lmfit.Parameters()
self.things = self.make_a_whole_bunch_of_things()
for thing in self.things:
self.params.add('something' + str(thing.NM()) + 's', value = 3)
def make_a_whole_bunch_of_things(self):
things = []
for n in range(0,20):
m=1
things.append(object_within_special_object(n,m))
return things
# a special type of tab which holds a (or a couple of) matplotlib plots and a special_object ( which holds the data to display in those plots)
class Special_Tab(QtGui.QTabWidget):
def __init__(self, parent, special_object):
QtGui.QTabWidget.__init__(self, parent)
self.special_object = special_object
self.grid = QtGui.QGridLayout(self)
# matplotlib figure put into tab
self.fig = figure.Figure()
self.plot = self.fig.add_subplot(111)
self.line, = self.plot.plot(self.special_object.X, self.special_object.Y, 'r-')
self.canvas = FigureCanvas(self.fig)
self.grid.addWidget(self.canvas)
self.canvas.show()
self.canvas.draw()
self.canvas_BBox = self.plot.figure.canvas.copy_from_bbox(self.plot.bbox)
ax1 = self.plot.figure.axes[0]
def process_on_special_object(self):
# do a long fitting process involving the properties of the special_object
return
def update_GUI(self):
# change the GUI to reflect changes made to special_object
self.line.set_data(special_object.X, special_object.Y)
self.plot.draw_artist(self.line)
self.plot.figure.canvas.blit(self.plot.bbox)
return
# This window just has a button to make all of the tabs in separate processes
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent = None):
# This GUI stuff shouldn't be too important
QtGui.QMainWindow.__init__(self)
self.resize(int(app.desktop().screenGeometry().width()*.6), int(app.desktop().screenGeometry().height()*.6))
self.tabs_list = []
central_widget = QtGui.QWidget(self)
self.main_tab_widget = QtGui.QTabWidget()
self.layout = QtGui.QHBoxLayout(central_widget)
button = QtGui.QPushButton('Open Tabs')
self.layout.addWidget(button)
self.layout.addWidget(self.main_tab_widget)
QtCore.QObject.connect(button, QtCore.SIGNAL("clicked()"), self.open_tabs)
self.setCentralWidget(central_widget)
central_widget.setLayout(self.layout)
# Here we open several tabs and put them in different processes
def open_tabs(self):
for i in range(0, 10):
# this is just some random data for the objects
X = np.arange(1240.0/1350.0, 1240./200., 0.01)
Y = np.array(np.e**.2*X + np.sin(10*X)+np.cos(4*X))
# Here the special tab is created
new_tab = Special_Tab(self.main_tab_widget, special_object(str(i), X, Y))
self.main_tab_widget.addTab(new_tab, str(i))
# this part works fine without the .start() function
self.tabs_list.append(mp.Process(target=new_tab))
# this is where pickling errors occur
self.tabs_list[-1].start()
return
if __name__ == "__main__":
app = QtGui.QApplication([])
win = MainWindow()
win.show()
sys.exit(app.exec_())
I have noticed that the errors are coming from matplotlib axes (I'm not sure how?) and gives the error pickle.PicklingError: Can't pickle <class 'matplotlib.axes.AxesSubplot'>: it's not found as matplotlib.axes.AxesSubplot. In addition, I have noticed that commenting out the matplotlib plots will also give the pickling error pickle.PicklingError: Can't pickle <function <lambda> at 0x012A2B30>: it's not found as lmfit.parameter.<lambda>. I think this is because lambda functions cannot be pickled and I guess lmfit has a lambda somewhere in it's depths... but I don't really know what to do without some way around these errors.
Oddly enough, the error I see form the original code (not the simplified version shown here) is slightly different, but still basically the same in sentiment. The error I get in my other code is pickle.PicklingError: Can't pickle 'BufferRegion' object: <BufferRegion object at 0x037EBA04>
Does anyone have a better solution to this problem by moving the objects around in regards to where I pass them or any other ideas?
I very much appreciate your time and effort and any help on this problem.
EDIT: I tried unutbu's idea in a way, but with some alterations to the position of the process funciton.
The only problem with the proposed solution is that the do_long_fitting_process() function calls another function which iteratively updates the lines in the matplotlib plots. So the do_long_fitting_process() needs to have some access to the Special_Tab properties to change them and show the updates to the GUI.
I have tried doing this by pushing the do_long_fitting_process() function to just a global function and calling this:
[code]
def open_tabs(self):
for i in range(0, 10):
...
self.tabs_list.append(new_tab)
consumer, producer = mp.Pipe()
process = mp.Process(target=process_on_special_object, args=(producer,))
process.start()
while(True):
message = consumer.recv()
if message == 'done':
break
tab.update_GUI(message[0], message[1])
process_list[-1].join()
[/code]
Where I am passing the data to update_GUI() via a mp.Pipe(), but the window just goes to "Not Responding" as soon as I start the processes.
Separate the GUI code from the computation code. The GUI must run in a single process (though it may spawn multiple threads). Let the computation code be contained within in the special_object.
Let the Special_Tab call mp.Process when you want to perform a long-running computation:
class special_object:
def do_long_fitting_process(self):
pass
class Special_Tab(QtGui.QTabWidget):
def process_on_special_object(self):
# do a long fitting process involving the properties of the
# special_object
proc = mp.Process(target = self.special_object.do_long_fitting_process)
proc.start()
class MainWindow(QtGui.QMainWindow):
def open_tabs(self):
for i in range(0, 10):
...
self.tabs_list.append(new_tab)
new_tab.process_on_special_object()
The problem is that not all parts of an Axes object can be serialized, which is necessary to move data between the processes. I would suggest a slight re-organization of your code to push computation off to separate processes (that is any thing that will take more than a fraction of a second), but keep all of your plotting on the main process and only pass data back and forth.
Another option is to us QThread See time.sleep() required to keep QThread responsive? for two different ways of implementing it (one in the question, one in my answer).
Related
I am currently working on using Vispy to add visualization capabilities to a Python simulation library. I have managed to get some basic visualizations running with the data from the simulations, and am now looking at wrapping it in functions/classes so users of the libraries easily visualize the simulation (by passing the data in a specific format or something) without having to code it themselves.
However, I am having trouble figuring out the right way/best practice to get the timers working properly to update the objects as they change in time.
For example, when running the visualization as script, an example of how I have implemented the timer is using global variables and iterators similar to how it is done in the Vispy Scene "Changing Line Colors" demo on the Vispy website :
def on_timer(event):
global colormaps, line, text, pos
color = next(colormaps)
line.set_data(pos=pos, color=color)
text.text = color
timer = app.Timer(.5, connect=on_timer, start=True)
But when I wrap the entire visualization script in a function/class, I am having trouble getting the timer to work correctly given the difference in scopes of the variables now. If anyone could give some idea of the best way to achieve this that would be great.
EDIT:
I have made an extremely simplified visualization that might be similar to some of the expected use cases. The code when run as a stand alone python script is:
import numpy as np
from vispy import app, scene
# Reproducible data (oscillating wave)
time = np.linspace(0, 5, 300)
rod_positions = []
for t in time:
wave_position = []
for i in range(100):
wave_position.append([0, i, 20 * (np.cos(t + i / 10))])
rod_positions.append(wave_position)
rod_positions = np.array(rod_positions)
# Prepare canvas
canvas = scene.SceneCanvas(keys="interactive", size=(800, 600), bgcolor="black")
canvas.measure_fps()
# Set up a view box to display the image with interactive pan/zoom
view = canvas.central_widget.add_view()
view.camera = scene.TurntableCamera()
rod = scene.visuals.Tube(
points=rod_positions[0], radius=5, closed=False, tube_points=16, color="green",
)
view.add(rod)
view.camera.set_range()
text = scene.Text(
f"Time: {time[0]:.4f}",
bold=True,
font_size=14,
color="w",
pos=(80, 30),
parent=canvas.central_widget,
)
update_counter = 0
max_updates = len(time) - 1
def on_timer_update(event):
global update_counter
update_counter += 1
if update_counter >= max_updates:
timer.stop()
canvas.close()
# Update new rod position and radius
rod_new_meshdata = scene.visuals.Tube(
points=rod_positions[update_counter],
radius=5,
closed=False,
tube_points=16,
color="green",
)._meshdata
rod.set_data(meshdata=rod_new_meshdata)
text.text = f"Time: {time[update_counter]:.4f}"
# Connect timer to app
timer = app.Timer("auto", connect=on_timer_update, start=True)
if __name__ == "__main__":
canvas.show()
app.run()
My attempt at wrapping this in a class is the following:
import numpy as np
from vispy import app, scene
class Visualizer:
def __init__(self, rod_position, time) -> None:
self.rod_positions = rod_position
self.time = time
self.app = app.application.Application()
# Prepare canvas
self.canvas = scene.SceneCanvas(keys="interactive", size=(800, 600), bgcolor="black")
self.canvas.measure_fps()
# Set up a view box to display the image with interactive pan/zoom
self.view = self.canvas.central_widget.add_view()
self.view.camera = scene.TurntableCamera()
self.update_counter = 0
self.max_updates = len(time) - 1
def _initialize_objects(self):
self.rod = scene.visuals.Tube(
points=self.rod_positions[0], radius=5, closed=False, tube_points=16, color="green",
)
self.view.add(self.rod)
self.view.camera.set_range()
self.text = scene.Text(
f"Time: {self.time[0]:.4f}",
bold=True,
font_size=14,
color="w",
pos=(80, 30),
parent=self.canvas.central_widget,
)
def update_timer(self, event):
self.update_counter += 1
if self.update_counter >= self.max_updates:
self.timer.stop()
self.canvas.close()
# Update new rod position and radius
rod_new_meshdata = scene.visuals.Tube(
points=self.rod_positions[self.update_counter],
radius=5,
closed=False,
tube_points=16,
color="green",
)._meshdata
self.rod.set_data(meshdata=rod_new_meshdata)
self.text.text = f"Time: {self.time[self.update_counter]:.4f}"
def run(self):
self._initialize_objects()
# Connect timer to app
self.timer = app.Timer("auto", connect=self.update_timer, start=True, app=self.app)
self.canvas.show()
self.app.run()
if __name__ == "__main__":
# Reproducible data (oscillating wave)
time = np.linspace(0, 5, 150)
rod_positions = []
for t in time:
wave_position = []
for i in range(100):
wave_position.append([0, i, 20 * (np.cos(2 * t + i / 10))])
rod_positions.append(wave_position)
rod_positions = np.array(rod_positions)
Visualizer = Visualizer(rod_positions, time)
Visualizer.run()
It seems to be working now which is good. However this is a minimal reproduction of my problem, so I would just like to make sure that this is the optimal/intended way. As a side note, I also feel as if my way of updating the rod by generating the meshdata and updating the rod in the view with this meshdata is not optimal and slowing the visualization down (it runs at around 10fps). Is the update loop itself optimal?
Thanks
Thanks for updating your question with your example. It makes it much easier to answer and continue our conversation. I started a pull request for VisPy about this topic where I add some examples that use Qt timers and Qt background threads. I hope to finish the PR in the next month or two so if you have any feedback please comment on it:
https://github.com/vispy/vispy/pull/2339
Timers
In general timers are a good way to update a visualization in a GUI event loop friendly way. However, in most GUI frameworks the timer is executed in the current GUI thread. This has the benefit that you can call GUI/drawing functions directly (they are in the same thread), but has the downside that any time you spend in your timer callback is time you're taking away from the GUI framework in order to respond to user interaction and redraw the application. You can see this in my timer example in the above PR if you uncomment the extra sleep line. The GUI basically stops responding while it is running the timer function.
I have a little message on GUI threads and event loops in the vispy FAQ here.
Threads
Threads are a good way around the limitations of timers, but come with a lot of complexities. Threads are also usually best done in a GUI-framework specific manner. I can't tell from your post if you are doing Qt or some other backend, but you will probably get the most bang for your buck by using the GUI framework specific thread functionality rather than generic python threads. If an application that works across GUI frameworks is what you're looking for then this likely isn't an option.
The benefit of using something like a QThread versus a generic python thread is that you can use things like Qt's signals and slots for sending data/information between the threads.
Note that as I described in the FAQ linked above that if you use threads you won't be able to call the set_data methods directly in your background thread(s). You'll need to send the data to the main GUI thread and then have it call the set_data method.
Suggestions
You are correct that your creation of a new Tube visual that you only use for extracting mesh data is a big slow down. The creation of a Visual is doing a lot of low-level GL stuff to get ready to draw the Visual, but then you don't use it so that work is essentially wasted time. If you can find a way to create the MeshData object yourself without the Visual you should see a speed up, but if you have a lot of data switching to threads is likely still needed.
I'm not sure how much this example's design resembles what you're really doing, but I would consider keeping the Application, Timer, and Visualizer classes separate. I'm not sure I can give you a good reason why, but something about creating these things in a singe class worries me. In general I think the programming concept of dependency injection applies here: don't create your dependencies inside your code, create them outside and pass them as arguments to your code. Or in the case of the Application and Timer, connect them to the proper methods on your class. If you have a much larger application then it may make sense to have a class that wraps all of the Vispy canvas and data updating logic, another one just for the outer GUI window, and then another for a data generation class. Then in the bottom if main block of code create your application, your timer, your canvas wrapper, your main window (passing the canvas wrapper if needed), and connect them all together.
Other thoughts
This is a problem I want to make easier for vispy users and scientific python programmers in general so let me know what I'm missing here and we'll see what kind of ideas we can come up with. Feel free to comment and provide feedback on my pull request. Do you have other ideas for what the examples should be doing? I have plans for additional fancier examples in that PR, but if you have thoughts about what you'd like to see let me know.
this is a bit longer, the first part is just a description of the problem, the second one the question if my "fix" is correct.
I started with python programming. I created a program that communicates with an Arduino that reads the temperature of a furnace of our melting lab. The temperature is then used in a PID algorithm and an output is set to the Arduino. The communication is done via pyserial. So far, everthing works, including live plotting of the temperature signals, PID-variables and so on. The script includes a the main loop and 3 threads (serial communication, a datashifter that reads from serialport, the set temperature from the QWidget and the output of the PID algorithm. This values are used to create an array for displaying within pyqtgraph. Finally, the third thread shifts the data from the datashifter to the QWidget.
When using my Linux-Notebook, everything works fine, and the GUI never stops updating. In contrast, when using any Windows-Host, i have the problem that some pyqtgraphs stop to refresh. The behavior is strange, because i set all data at more or less the same time, with the same numpy array (just different columns) - some plots refresh longer (hours), some stop earlier (minutes). After searching more or less the hole internet ;-) I think that I found the problem: Its the passing of data from from a thread to the GUI. Some dummy code to explain what's going on:
DataUpdaterToGUI(QThread):
#sets the QWidget from main loop
def setGUI(self, gui):
self.gui = gui
def run()
while True:
with lock(): # RLock() Instance
copyArray = self.dataArray[:] # copy the array from the shifter
self.gui.plot1.copyArray(dataArray[:, 0], copyArray[:, 1])
self.gui.plot2.copyArray(dataArray[:, 0], copyArray[:, 2])
# self.gui.update()
# QApplication.instance().processEvents()
Neither calling self.gui.update() nor processEvents() has any influence on the outcome: The plots stop redrawing after a while (on windows).
Now i have a very simple example, and just want to make sure if I'm using the threading-stuff correctly. It works fine, but I have some questions:
Does the signal-slot approach copy the passed data?
Why is it not necessary to call the update() method of the QWidget?
Do I have to use any kind of locks when using signals?
class Main(QWidget):
def __init__(self):
super().__init__()
self.layout = QGridLayout(self)
self.graph = pg.PlotWidget()
self.graph.setYRange(0,1000)
self.plot = self.graph.plot()
self.layout.addWidget(self.graph,0,0)
self.show()
def make_connection(self, data_object):
data_object.signal.connect(self.grab_data)
#pyqtSlot(object)
def grab_data(self, data):
print(data)
self.plot.setData(data)
class Worker(QThread):
signal = pyqtSignal(object)
def __init__(self):
super().__init__()
def run(self):
self.data = [0, 1]
i = 2
while True:
self.data[1] = i
self.signal.emit(self.data)
time.sleep(0.01)
i += 1
if __name__ == "__main__":
app = QApplication(sys.argv)
widget = Main()
worker = Worker()
widget.make_connection(worker)
worker.start()
sys.exit(app.exec_())
Does the signal-slot approach copy the passed data? The signals are thread-safe and when transferring data they make a copy so the thread that precedes the data and the thread that consumes it (GUI Thread) will not have conflicts
Why is it not necessary to call the update() method of the QWidget? Actually pyqtgraph calls the update method, plot is a PlotDataItem, so if we check the source code of setData() method, it calls the updateItems() method, in that method the setData() method of the curve or scatter attribute is called (according to the type of graphics), in the case of curve its setData() method calls updateData(), and the updateData() method calls update, and in the case of the scatter its setData() method calls addpoint(), and addPoints() calls invalidate(), and this invalidate() method calls update().
Do I have to use any kind of locks when using signals? No, as the signals are thread-safe so Qt already has the guards set to avoid the collision.
I am making a gui in tkinter with live, embedded matplotlib graphs. I am using FigureCanvasTkAgg for the canvas, NavigationToolbar2Tk for the navigation bar, and FuncAnimation to handle periodic updates of the given source of data.
The callback tied to FuncAnimation resets the data on a given line (i.e. the return value from Axes.plot(...)) every invocation (i.e. Line2D.set_data(...)). The callback also redetermines and applies the appropriate x- and y-axis limits to fit the new data via
axis.relim()
axis.autoscale_view()
where axis is an instance of AxesSubplot.
Before the navigation bar is used, this works great; any new data added is appropriately reflected in the graph and the axes automatically re-scale to fit it, which was my goal.
The problem I am facing is that if any of the functions on the navigation bar are used (pan, zoom, etc.) the re-scaling fails to work any longer, meaning the graph may grow out of view and the user's only way to see new data is to manually pan over to it or to manually zoom out, which is undesirable.
Realistically, this functionality make sense since it would be annoying to, for example, try to zoom in a part of the plot only to have it zoom out immediately to refit the axes to new data, which is why I had intended to add a tkinter.Checkbutton to temporarily disable the re-scaling.
I've tried to look into the source for the navigation bar, and it seems to change state on the axes and canvas which I can only assume is the problem, but I have so far been unsuccessful at finding a way to "undo" these changes. If such a way exists, I would bind it to a tkinter.Button or something so the automatic re-scaling can be re-enabled.
How might I fix this problem?
Below is a minimal example that demonstrates this problem.
import math
import itertools
import tkinter as tk
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.animation import FuncAnimation
def xydata_generator(func, div):
for num in itertools.count():
num = num / div
yield num, func(num)
class Plot(tk.Frame):
def __init__(self, master, data_source, interval=100, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.data_source = data_source
self.figure = Figure((5, 5), 100)
self.canvas = FigureCanvasTkAgg(self.figure, self)
self.nav_bar = NavigationToolbar2Tk(self.canvas, self)
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
self.axis = self.figure.add_subplot(111)
self.x_data = []
self.y_data = []
self.line = self.axis.plot([], [])[0] # Axes.plot returns a list
# Set the data to a mutable type so we only need to append to it then force the line to invalidate its cache
self.line.set_data(self.x_data, self.y_data)
self.ani = FuncAnimation(self.figure, self.update_plot, interval=interval)
def update_plot(self, _):
x, y = next(self.data_source) # (realistically the data source wouldn't be restricted to be a generator)
# Because the Line2D object stores a reference to the two lists, we need only update the lists and signal
# that the line needs to be updated.
self.x_data.append(x)
self.y_data.append(y)
self.line.recache_always()
self._refit_artists()
def _refit_artists(self):
self.axis.relim()
self.axis.autoscale_view()
root = tk.Tk()
data = xydata_generator(math.sin, 5)
plot = Plot(root, data)
plot.pack(fill=tk.BOTH, expand=True)
root.mainloop()
Turns out to be pretty simple. To reset the axes so that the calls to Axes.relim() and Axes.autoscale_view() take effect, one simply needs to call Axes.set_autoscale_on(True). This must be repeated every time the functions on the navigation bar (pan, zoom, etc.) are used.
I have a simulation code and I would like to add diagnostic plotting to it with pyqtgraph.
The simulation is implemented in a Python class. In pseudo-code,
class Simulation:
[...]
'''
Defines self.data, the stuff that needs plotting
and self.done, which is False and becomese True once the simulation
is over.
'''
def evolve(self):
''' takes care of updating self.data'''
[...]
self.done = True
def get_data(self):
return self.data
and I would like to keep it independent from the plotting infrastructure.
What I'm trying to do is to create a plotting class, which so far I tried to implement with python threading, that reads
import pyqtgraph as pg
import threading
from time import sleep
from pyqtgraph.Qt import QtGui
class Plot(threading.Thread):
def __init__(self, source, target_fps=10):
threading.Thread.__init__(self, daemon=True)
self.source = source
self.wait_time = 1. / target_fps
self.win = pg.GraphicsWindow()
self.plot = self.win.addPlot()
self.curve = self.plot.plot()
self.curve.setData(self.source.get_data())
def run(self):
while not self.source.done:
self.curve.setData(self.source.get_data())
sleep(self.wait_time)
whose job is to poll the source class once in a while and update the plot. Then my __main__ would be something along the lines of
if __name__ == "__main__":
sim = Simulation() # initialise simulation code
plotter = Plot(sim) # <- By commenting these three lines
plotter.start() # <- I fall back to a working simulation
sim.evolve() #
plotter.join() # <- without plotting diagnostics
Of course the previous __main__ doesn't work because it is missing the Qt event loop required by pyqtgraph, something of the like of pg.QtGui.QApplication.exec_(), which carries the inconvenient side effect of blocking the execution of the rest of the code after it. If I try to put the exec_() inside Plot.run() I get a warning that the event loop must be run from within the main thread, and plots don't show up.
Is there a workaround that I can use to make the plotting class work and not block the execution of the simulation? I would like not to touch the Simulation class.
I am trying to write a Python GUI and I need to do a live plot. I currently have a program that receives data from a machine I am using and I want to be able to plot the values the machine outputs as I receive them. I have been researching and from what I have found so far, it doesn't seem to me like tkinter or any library can do this in a GUI. Does anyone know whether and how tkinter can do this or if there is another library that is capable of doing such a live plot?
Also, how would I go about writing the data that I gather to a file as I receive the data?
Thanks in advance for your help.
It looks like you get the data by polling, which means you don't need threads or multiple processes. Simply poll the device at your preferred interface and plot a single point.
Here's an example with some simulated data to illustrate the general idea. It updates the screen every 100ms.
import Tkinter as tk
import random
class ServoDrive(object):
# simulate values
def getVelocity(self): return random.randint(0,50)
def getTorque(self): return random.randint(50,100)
class Example(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
self.servo = ServoDrive()
self.canvas = tk.Canvas(self, background="black")
self.canvas.pack(side="top", fill="both", expand=True)
# create lines for velocity and torque
self.velocity_line = self.canvas.create_line(0,0,0,0, fill="red")
self.torque_line = self.canvas.create_line(0,0,0,0, fill="blue")
# start the update process
self.update_plot()
def update_plot(self):
v = self.servo.getVelocity()
t = self.servo.getTorque()
self.add_point(self.velocity_line, v)
self.add_point(self.torque_line, t)
self.canvas.xview_moveto(1.0)
self.after(100, self.update_plot)
def add_point(self, line, y):
coords = self.canvas.coords(line)
x = coords[-2] + 1
coords.append(x)
coords.append(y)
coords = coords[-200:] # keep # of points to a manageable size
self.canvas.coords(line, *coords)
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand=True)
root.mainloop()
As far as I understand it, it can be done with tkinter/Qt/wxpython etc. You just have to make use of mutlithreading and multiprocessing.
There may be a simpler way to do it with another module, but I am unaware of it.
I have been looking into something similar to this problem for a long time as well, and it appears that it is a constant issue among this community.
Here are some threads which talk about the issue:
How do I refresh a matplotlib plot in a Tkinter window?
How do I update a matplotlib figure while fitting a function?