#python #dynamic #graph
#python #динамический #График
Вопрос:
Мне нужна ваша помощь, чтобы написать скрипт на Python, который будет принимать динамически измененные данные, источник данных здесь не имеет значения, и отображать график на экране.
Я знаю, как использовать matplotlib, но проблема с matplotlib в том, что я могу отобразить график только один раз, в конце скрипта. Мне нужно иметь возможность не только отображать график один раз, но и обновлять его на лету, каждый раз при изменении данных.
Я обнаружил, что для этого можно использовать wxPython с matplotlib, но для меня это немного сложно сделать, потому что я вообще не знаком с wxPython.
Поэтому я буду очень рад, если кто-нибудь покажет мне простой пример, как использовать wxPython с matplotlib для отображения и обновления простого графика. Или, если это какой-то другой способ сделать это, мне это тоже подойдет.
PS:
Хорошо, поскольку никто не ответил, посмотрели справку matplotlib, замеченную @janislaw, и написали некоторый код. Это какой-то фиктивный пример:
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()
В этой реализации есть проблемы, например, скрипт останавливается, если вы пытаетесь переместить окно. Но в принципе его можно использовать.
Комментарии:
1. Я просто пытался сделать это сегодня и отказался от matplotlib. Я только что остановился на отправке всех данных через сокет в скрипт обработки, который выполняет весь рисунок, но это, вероятно, не тот ответ, на который вы надеялись.
2. matplotlib легко встраивается в любой графический интерфейс, который вам нравится, и не обязательно должен быть статичным. В документах есть примеры — см. раздел Пользовательские интерфейсы. Существуют также traits / traitsgui / chaco, возможно, более подходящие для такого рода заданий, но требующие смены парадигмы ссылка
Ответ №1:
Вот класс, который я написал, который решает эту проблему. Он принимает фигуру matplotlib, которую вы передаете ему, и помещает ее в окно графического интерфейса. Он находится в собственном потоке, так что он остается отзывчивым, даже когда ваша программа занята.
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()
В вашем коде вам нужно инициализировать плоттер следующим образом:
import pylab
fig = matplotlib.pyplot.figure()
Plotter(fig)
Затем вы можете построить его следующим образом:
fig.gca().clear()
fig.gca().plot([1,2,3],[4,5,6])
fig.canvas.draw()
Комментарии:
1. Я не смог заставить ваше решение работать, поскольку я новичок в Tkinter, я не уверен, что не так, однако то, что я выяснил до сих пор, заключается в том, что mainloop не может быть в потоке.
Ответ №2:
В качестве альтернативы matplotlib библиотека Chaco предоставляет отличные возможности построения графиков и в некотором смысле лучше подходит для построения графиков в реальном времени.
Посмотрите некоторые скриншоты здесь, и в частности, посмотрите эти примеры:
У Chaco есть серверные части для qt и wx, поэтому большую часть времени он довольно хорошо обрабатывает базовые детали для вас.
Комментарии:
1. Обновлено. Тем не менее, после этого ответа в экосистеме Python появилось много новых библиотек: Bokeh , Altair , Holoviews и другие.
Ответ №3:
Вместо matplotlib.pyplot.show()
вы можете просто использовать matplotlib.pyplot.show(block=False)
. Этот вызов не заблокирует дальнейшее выполнение программы.
Ответ №4:
пример динамического графика, секрет в том, чтобы делать паузу при построении графика, здесь я использую 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
Ответ №5:
У меня возникла необходимость создать график, который обновляется со временем. Наиболее удобным решением, которое я придумал, было каждый раз создавать новый график. Проблема заключалась в том, что скрипт не будет выполнен после создания первого графика, если окно не будет закрыто вручную. Этой проблемы удалось избежать, включив интерактивный режим, как показано ниже
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
Здесь следует отметить два важных момента
- идентификатор рисунка — если идентификатор рисунка изменен, каждый раз будет создаваться новый график, но если он тот же, то соответствующий график будет обновляться.
- функция паузы — останавливает выполнение кода на указанный период времени. Если это не применено, график обновится почти немедленно
Ответ №6:
Я создал класс, который рисует виджет tkinter с помощью графика matplotlib. График обновляется динамически (более или менее в реальном времени).
- Протестировано в python 3.10, matplotlib 3.6.0 и tkinter 8.6.
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
Ниже приведен пример пользовательского масштаба tkinter, используемого для обновления данных, которые отображаются на графике tkinter.
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()