Reputation: 2074
I need to write a script in Python that will take dynamically changed data, the source of data is not matter here, and display graph on the screen.
I know how to use matplotlib, but the problem with matplotlib is that I can display graph only once, at the end of the script. I need to be able not only to display graph one time, but also update it on the fly, each time when data changes.
I found that it is possible to use wxPython with matplotlib to do this, but it is little bit complicate to do this for me, because I am not familiar with wxPython at all.
So I will be very happy if someone will show me simple example how to use wxPython with matplotlib to show and update simple graph. Or, if it is some other way to do this, it will be good to me too.
PS: since no one answered and looked at matplotlib help noticed by @janislaw and wrote some code. This is some dummy example:
import time
import matplotlib.pyplot as plt
def data_gen():
a=data_gen.a
if a>10:
data_gen.a=1
data_gen.a=data_gen.a+1
return range (a,a+10)
def run(*args):
background = fig.canvas.copy_from_bbox(ax.bbox)
while 1:
time.sleep(0.1)
# restore the clean slate background
fig.canvas.restore_region(background)
# update the data
ydata = data_gen()
xdata=range(len(ydata))
line.set_data(xdata, ydata)
# just draw the animated artist
ax.draw_artist(line)
# just redraw the axes rectangle
fig.canvas.blit(ax.bbox)
data_gen.a=1
fig = plt.figure()
ax = fig.add_subplot(111)
line, = ax.plot([], [], animated=True)
ax.set_ylim(0, 20)
ax.set_xlim(0, 10)
ax.grid()
manager = plt.get_current_fig_manager()
manager.window.after(100, run)
plt.show()
This implementation have problems, like script stops if you trying to move the window. But basically it can be used.
Upvotes: 6
Views: 38440
Reputation: 301
I have created a class that draws a tkinter widget with a matplotlib plot. The plot is updated dynamically (more or less in realtime).
from matplotlib import pyplot as plt
from matplotlib import animation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from tkinter import *
class MatplotlibPlot:
def __init__(
self, master, datas: list[dict], update_interval_ms: int = 200, padding: int = 5,
fig_config: callable = None, axes_config: callable = None
):
"""
Creates a Matplotlib plot in a Tkinter environment. The plot is dynamic, i.e., the plot data is periodically
drawn and the canvas updates.
@param master: The master widget where the pot will be rendered.
@param datas: A list containing dictionaries of data. Each dictionary must have a `x` key, which holds the xx
data, and `y` key, which holds the yy data. The other keys are optional and are used as kwargs of
`Axes.plot()` function. Each list entry, i.e., each dict, is drawn as a separate line.
@param fig_config: A function that is called after the figure creation. This function can be used to configure
the figure. The function signature is `fig_config(fig: pyplot.Figure) -> None`. The example bellow allows
the configuration of the figure title and Dots Per Inch (DPI).
``` python
my_vars = [{"x": [], "y": [], "label": "Label"}, ]
window = Tk()
def my_fig_config(fig: pyplot.Figure) -> None:
fig.suptitle("Superior Title")
fig.set_dpi(200)
MatplotlibPlot(master=window, datas=my_vars, fig_config=my_fig_config)
window.mainloop()
```
@param axes_config: A function that is called after the axes creation. This function can be used to configure
the axes. The function signature is `axes_config(axes: pyplot.Axes) -> None`. The example bellow allows
the configuration of the axes xx and yy label, the axes title and also enables the axes legend.
``` python
my_vars = [{"x": [], "y": [], "label": "Label"}, ]
window = Tk()
def my_axes_config(axes: pyplot.Axes) -> None:
axes.set_xlabel("XX Axis")
axes.set_ylabel("YY Axis")
axes.set_title("Axes Title")
axes.legend()
MatplotlibPlot(master=window, datas=my_vars, axes_config=my_axes_config)
window.mainloop()
```
@param update_interval_ms: The plot update interval in milliseconds (ms). Defaults to 200 ms.
@param padding: The padding, in pixels (px), to be used between widgets. Defaults to 5 px.
"""
# Creates the figure
fig = plt.Figure()
# Calls the config function if passed
if fig_config:
fig_config(fig)
# Creates Tk a canvas
canvas = FigureCanvasTkAgg(figure=fig, master=master)
# Allocates the canvas
canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=True, padx=padding, pady=padding)
# Creates the toolbar
NavigationToolbar2Tk(canvas=canvas, window=master, pack_toolbar=True)
# Creates an axes
axes = fig.add_subplot(1, 1, 1)
# For each data entry populate the axes with the initial data values. Also, configures the lines with the
# extra key-word arguments.
for data in datas:
axes.plot(data["x"], data["y"])
_kwargs = data.copy()
_kwargs.pop("x")
_kwargs.pop("y")
axes.lines[-1].set(**_kwargs)
# Calls the config function if passed
if axes_config:
axes_config(axes)
# Creates a function animation which calls self.update_plot function.
self.animation = animation.FuncAnimation(
fig=fig,
func=self.update_plot,
fargs=(canvas, axes, datas),
interval=update_interval_ms,
repeat=False,
blit=True
)
# noinspection PyMethodMayBeStatic
def update_plot(self, _, canvas, axes, datas):
# Variables used to update xx and yy axes limits.
update_canvas = False
xx_max, xx_min = axes.get_xlim()
yy_max, yy_min = axes.get_ylim()
# For each data entry update its correspondent axes line
for line, data in zip(axes.lines, datas):
line.set_data(data["x"], data["y"])
_kwargs = data.copy()
_kwargs.pop("x")
_kwargs.pop("y")
line.set(**_kwargs)
# If there are more than two points in the data then update xx and yy limits.
if len(data["x"]) > 1:
if min(data["x"]) < xx_min:
xx_min = min(data["x"])
update_canvas = True
if max(data["x"]) > xx_max:
xx_max = max(data["x"])
update_canvas = True
if min(data["y"]) < yy_min:
yy_min = min(data["y"])
update_canvas = True
if max(data["y"]) > yy_max:
yy_max = max(data["y"])
update_canvas = True
# If limits need to be updates redraw canvas
if update_canvas:
axes.set_xlim(xx_min, xx_max)
axes.set_ylim(yy_min, yy_max)
canvas.draw()
# return the lines
return axes.lines
Below is an example of a custom tkinter scale used to update data which is drawn in the tkinter plot.
from matplotlib import pyplot as plt
from matplotlib import animation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from tkinter import *
class MatplotlibPlot:
def __init__(
self, master, datas: list[dict], update_interval_ms: int = 200, padding: int = 5,
fig_config: callable = None, axes_config: callable = None
):
"""
Creates a Matplotlib plot in a Tkinter environment. The plot is dynamic, i.e., the plot data is periodically
drawn and the canvas updates.
@param master: The master widget where the pot will be rendered.
@param datas: A list containing dictionaries of data. Each dictionary must have a `x` key, which holds the xx
data, and `y` key, which holds the yy data. The other keys are optional and are used as kwargs of
`Axes.plot()` function. Each list entry, i.e., each dict, is drawn as a separate line.
@param fig_config: A function that is called after the figure creation. This function can be used to configure
the figure. The function signature is `fig_config(fig: pyplot.Figure) -> None`. The example bellow allows
the configuration of the figure title and Dots Per Inch (DPI).
``` python
my_vars = [{"x": [], "y": [], "label": "Label"}, ]
window = Tk()
def my_fig_config(fig: pyplot.Figure) -> None:
fig.suptitle("Superior Title")
fig.set_dpi(200)
MatplotlibPlot(master=window, datas=my_vars, fig_config=my_fig_config)
window.mainloop()
```
@param axes_config: A function that is called after the axes creation. This function can be used to configure
the axes. The function signature is `axes_config(axes: pyplot.Axes) -> None`. The example bellow allows
the configuration of the axes xx and yy label, the axes title and also enables the axes legend.
``` python
my_vars = [{"x": [], "y": [], "label": "Label"}, ]
window = Tk()
def my_axes_config(axes: pyplot.Axes) -> None:
axes.set_xlabel("XX Axis")
axes.set_ylabel("YY Axis")
axes.set_title("Axes Title")
axes.legend()
MatplotlibPlot(master=window, datas=my_vars, axes_config=my_axes_config)
window.mainloop()
```
@param update_interval_ms: The plot update interval in milliseconds (ms). Defaults to 200 ms.
@param padding: The padding, in pixels (px), to be used between widgets. Defaults to 5 px.
"""
# Creates the figure
fig = plt.Figure()
# Calls the config function if passed
if fig_config:
fig_config(fig)
# Creates Tk a canvas
canvas = FigureCanvasTkAgg(figure=fig, master=master)
# Allocates the canvas
canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=True, padx=padding, pady=padding)
# Creates the toolbar
NavigationToolbar2Tk(canvas=canvas, window=master, pack_toolbar=True)
# Creates an axes
axes = fig.add_subplot(1, 1, 1)
# For each data entry populate the axes with the initial data values. Also, configures the lines with the
# extra key-word arguments.
for data in datas:
axes.plot(data["x"], data["y"])
_kwargs = data.copy()
_kwargs.pop("x")
_kwargs.pop("y")
axes.lines[-1].set(**_kwargs)
# Calls the config function if passed
if axes_config:
axes_config(axes)
# Creates a function animation which calls self.update_plot function.
self.animation = animation.FuncAnimation(
fig=fig,
func=self.update_plot,
fargs=(canvas, axes, datas),
interval=update_interval_ms,
repeat=False,
blit=True
)
# noinspection PyMethodMayBeStatic
def update_plot(self, _, canvas, axes, datas):
# Variables used to update xx and yy axes limits.
update_canvas = False
xx_max, xx_min = axes.get_xlim()
yy_max, yy_min = axes.get_ylim()
# For each data entry update its correspondent axes line
for line, data in zip(axes.lines, datas):
line.set_data(data["x"], data["y"])
_kwargs = data.copy()
_kwargs.pop("x")
_kwargs.pop("y")
line.set(**_kwargs)
# If there are more than two points in the data then update xx and yy limits.
if len(data["x"]) > 1:
if min(data["x"]) < xx_min:
xx_min = min(data["x"])
update_canvas = True
if max(data["x"]) > xx_max:
xx_max = max(data["x"])
update_canvas = True
if min(data["y"]) < yy_min:
yy_min = min(data["y"])
update_canvas = True
if max(data["y"]) > yy_max:
yy_max = max(data["y"])
update_canvas = True
# If limits need to be updates redraw canvas
if update_canvas:
axes.set_xlim(xx_min, xx_max)
axes.set_ylim(yy_min, yy_max)
canvas.draw()
# return the lines
return axes.lines
class CustomScaler:
def __init__(self, master, init: int = None, start: int = 0, stop: int = 100,
padding: int = 5, callback: callable = None):
"""
Creates a scaler with an increment and decrement button and a text entry.
@param master: The master Tkinter widget.
@param init: The scaler initial value.
@param start: The scaler minimum value.
@param stop: The scaler maximum value.
@param padding: The widget padding.
@param callback: A callback function that is called each time that the scaler changes its value. The function
signature is `callback(var_name: str, var_index: int, var_mode: str) -> None`.
"""
self.start = start
self.stop = stop
if init:
self.value = IntVar(master=master, value=init, name="scaler_value")
else:
self.value = IntVar(master=master, value=(self.stop - self.start) // 2, name="scaler_value")
if callback:
self.value.trace_add("write", callback=callback)
Scale(master=master, from_=self.start, to=self.stop, orient=HORIZONTAL, variable=self.value) \
.pack(side=TOP, expand=True, fill=BOTH, padx=padding, pady=padding)
Button(master=master, text="◀", command=self.decrement, repeatdelay=500, repeatinterval=5) \
.pack(side=LEFT, fill=Y, padx=padding, pady=padding)
Button(master=master, text="▶", command=self.increment, repeatdelay=500, repeatinterval=5) \
.pack(side=RIGHT, fill=Y, padx=padding, pady=padding)
Entry(master=master, justify=CENTER, textvariable=self.value) \
.pack(fill=X, expand=False, padx=padding, pady=padding)
def decrement(self):
_value = self.value.get()
if _value <= self.start:
return
self.value.set(_value - 1)
def increment(self):
_value = self.value.get()
if _value >= self.stop:
return
self.value.set(_value + 1)
def scaler_changed(my_vars: list[dict], scaler: CustomScaler) -> None:
my_vars[0]["x"].append(len(my_vars[0]["x"]))
my_vars[0]["y"].append(scaler.value.get())
def my_axes_config(axes: plt.Axes) -> None:
axes.set_xlabel("Sample")
axes.set_ylabel("Value")
axes.set_title("Scaler Values")
def main():
my_vars = [{"x": [], "y": []}, ]
window = Tk()
window.rowconfigure(0, weight=10)
window.rowconfigure(1, weight=90)
frame_scaler = Frame(master=window)
frame_scaler.grid(row=0, column=0)
scaler = CustomScaler(
master=frame_scaler, start=0, stop=100, callback=lambda n, i, m: scaler_changed(my_vars, scaler)
)
frame_plot = Frame(master=window)
frame_plot.grid(row=1, column=0)
MatplotlibPlot(master=frame_plot, datas=my_vars, axes_config=my_axes_config, update_interval_ms=10)
window.mainloop()
if __name__ == "__main__":
main()
The example above produces the following window.
Upvotes: 0
Reputation: 4366
As an alternative to matplotlib, the Chaco library provides nice graphing capabilities and is in some ways better-suited for live plotting.
See some screenshots here, and in particular, see these examples:
Chaco has backends for qt and wx, so it handles the underlying details for you rather nicely most of the time.
Upvotes: 2
Reputation: 591
I had the need to create a graph that updates with time. The most convenient solution I came up was to create a new graph each time. The issue was that the script won't be executed after the first graph is created, unless the window is closed manually. That issue was avoided by turning the interactive mode on as shown below
for i in range(0,100):
fig1 = plt.figure(num=1,clear=True) # a figure is created with the id of 1
createFigure(fig=fig1,id=1) # calls a function built by me which would insert data such that figure is 3d scatterplot
plt.ion() # this turns the interactive mode on
plt.show() # create the graph
plt.pause(2) # pause the script for 2 seconds , the number of seconds here determine the time after that graph refreshes
There are two important points to note here
Upvotes: 0
Reputation: 1
example of dynamic plot , the secret is to do a pause while plotting , here i use networkx:
G.add_node(i,)
G.add_edge(vertic[0],vertic[1],weight=0.2)
print "ok"
#pos=nx.random_layout(G)
#pos = nx.spring_layout(G)
#pos = nx.circular_layout(G)
pos = nx.fruchterman_reingold_layout(G)
nx.draw_networkx_nodes(G,pos,node_size=40)
nx.draw_networkx_edges(G,pos,width=1.0)
plt.axis('off') # supprimer les axes
plt.pause(0.0001)
plt.show() # display
Upvotes: 0
Reputation: 6613
Instead of matplotlib.pyplot.show()
you can just use matplotlib.pyplot.show(block=False)
. This call will not block the program to execute further.
Upvotes: 0
Reputation: 1297
Here is a class I wrote that handles this issue. It takes a matplotlib figure that you pass to it and places it in a GUI window. Its in its own thread so that it stays responsive even when your program is busy.
import Tkinter
import threading
import matplotlib
import matplotlib.backends.backend_tkagg
class Plotter():
def __init__(self,fig):
self.root = Tkinter.Tk()
self.root.state("zoomed")
self.fig = fig
t = threading.Thread(target=self.PlottingThread,args=(fig,))
t.start()
def PlottingThread(self,fig):
canvas = matplotlib.backends.backend_tkagg.FigureCanvasTkAgg(fig, master=self.root)
canvas.show()
canvas.get_tk_widget().pack(side=Tkinter.TOP, fill=Tkinter.BOTH, expand=1)
toolbar = matplotlib.backends.backend_tkagg.NavigationToolbar2TkAgg(canvas, self.root)
toolbar.update()
canvas._tkcanvas.pack(side=Tkinter.TOP, fill=Tkinter.BOTH, expand=1)
self.root.mainloop()
In your code, you need to initialize the plotter like this:
import pylab
fig = matplotlib.pyplot.figure()
Plotter(fig)
Then you can plot to it like this:
fig.gca().clear()
fig.gca().plot([1,2,3],[4,5,6])
fig.canvas.draw()
Upvotes: 2