使用Python创建动态更新的图表

6
我需要用Python写一个脚本,它可以动态地获取数据并在屏幕上显示图形。
我知道如何使用matplotlib,但是matplotlib的问题是我只能在脚本结束时显示图形。我不仅需要能够一次性显示图形,还需要在数据变化时实时更新图形。
我发现可以使用wxPython和matplotlib来实现这一点,但对于我来说有点复杂,因为我对wxPython完全不熟悉。
所以如果有人能给我展示一个简单的例子,如何使用wxPython和matplotlib来显示和更新简单的图形,我会非常高兴。或者,如果有其他方法可以实现这个功能,对我来说也很好。
更新:
PS:由于没有人回答,我看了一下matplotlib的帮助,并写了一些代码。这是一个简单的示例:

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()

这个实现有问题,比如如果你尝试移动窗口,脚本会停止。但基本上它是可以使用的。

我今天刚试图做这件事,但放弃了matplotlib。我只是决定将所有数据通过套接字发送到一个处理脚本中进行绘制,但这可能不是你希望得到的答案。 - Nathan
4
matplotlib很容易嵌入到任何你喜欢的GUI中,并且不需要是静态的。文档中有示例-请参阅用户界面部分。还有traits/traitsgui/chaco,也许更适合这种工作,但需要进行范式转换。[链接] - janislaw
6个回答

2

这里是我编写的一个处理此问题的类。它接收您传递给它的 matplotlib 图形,并将其放置在 GUI 窗口中。它在自己的线程中运行,因此即使程序繁忙,它也可以保持响应。

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()

我无法让你的解决方案工作,因为我对Tkinter不熟悉,不确定哪里出了问题。但是,我已经发现主循环不能在线程中运行。 - Jehandad

2
作为matplotlib的替代选择,Chaco库提供了不错的绘图功能,并且在某些方面更适合实时绘图。
在此处查看一些截图here,特别是查看这些示例: Chaco有qt和wx的后端,因此大多数情况下它会为您处理底层细节。

更新。话虽如此,自这个答案以来,Python生态系统中出现了许多新的库:BokehAltairHoloviews等等。 - Isaiah Norton

0

你可以使用matplotlib.pyplot.show(block=False)代替matplotlib.pyplot.show()。这个调用不会阻塞程序继续执行。


0
我创建了一个类,它可以绘制带有 matplotlib 绘图的 tkinter 小部件。该绘图会动态更新(几乎实时)。
  • 在 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()


上面的示例会产生以下窗口。 {{link1:在此输入图像描述}}。

0
动态绘图的示例,秘诀是在绘制时暂停,这里我使用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

0

我有需要创建一个随着时间更新的图表。最方便的解决方案是每次创建一个新的图表。问题在于,除非手动关闭窗口,否则 脚本不会再执行 创建第一个图表之后。 通过如下方式打开交互模式,避免了这个问题。

    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

这里有两个重要的注意点

  1. 图表的id - 如果更改了图表的id,每次都会创建一个新的图表,但如果它相同,则相关的图表将被更新。
  2. 暂停函数 - 这将停止代码在指定的时间段内执行。如果不应用此功能,图表将几乎立即刷新。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接