2

My goal is to do real-time plotting in matplotlib and embed these plots in a GTK3 GUI written with pygtk (or pygobject, can never remember which one). One way to do this efficiently is to use "blitting" and this example show how to do it by saving the background region, restoring it after every update, and redrawing the artist line. After quite a bit of smacking-head-on-keyboard I discovered that the default matplotlib examples that document how to do graphing with matplotlib in GTK use the GTK3Cairo backend renderer which was not sufficient (as it doesn't support blitting) and that I needed to switch to GTK3Agg. This worked successfully and I am able to imitate the above example while embedding it in a GTK ScrolledWindow widget. However the graphs seem to draw over one another at the bottom of each plot.

I've traced this down to a couple of issues. It looks like the saved background region is not being properly positioned and is slightly offset. If I modify the restore_region(background) function call (see below) and instead try to modify the bbox and xy values, I can shift the background region around but it does not solve the issue of the bottom of the graph drawing over itself (the offsets below are just arbitrary numbers).

x1, y1, x2, y2 = background.get_extents()
restore_region(background, bbox=(x1, y1-20, x2, y2), xy=(x1+9, y1+10))

Here is the example I am working on where I've tried to integrate the above linked example using saved background regions and blitting to increase plotting speed and decrease CPU load.

In the end the final usage won't be to update from timeout_add(), I'll be updating with data obtained from a separate processing thread.

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject

import numpy as np

import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas

class TestPlot(Gtk.ScrolledWindow):
    def __init__(self):
        Gtk.ScrolledWindow.__init__(self)

        self.x = np.arange(0, 2*np.pi, 0.1)
        self.y = np.sin(self.x)

        self.fig, self.axes = plt.subplots(nrows=6)
        self.canvas = FigureCanvas(self.fig)
        self.add_with_viewport(self.canvas)

        self.fig.canvas.draw()

        styles = ['r-', 'g-', 'y-', 'm-', 'k-', 'c-']
        def plot(ax, style):
            return ax.plot(self.x, self.y, style, animated=True)[0]
        self.lines = [plot(ax, style) for ax, style in zip(self.axes, styles)]

        self.backgrounds = [self.fig.canvas.copy_from_bbox(ax.bbox) for ax in self.axes]

        self.i = 1
        GObject.timeout_add(10, self.update_graph)

    def update_graph(self):
        try:
            items = enumerate(zip(self.lines, self.axes, self.backgrounds), start=1)
            for j, (line, ax, background) in items:
                line.set_ydata(np.sin(j*self.x + self.i/10.))
                self.fig.canvas.restore_region(background)
                ax.draw_artist(line)
                self.fig.canvas.blit(ax.bbox)
            self.i += 1
        except:
            pass
        return True

class TestWindow(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self, title="TestWindow")
        self.set_default_size(700, 500)
        self.graph = TestPlot()
        self.add(self.graph)

win = TestWindow()
win.connect("delete-event", Gtk.main_quit)
win.show_all()
Gtk.main()

This is what the example looks like: Example GIF

This is what the above code example produces for me: GIF of above code example

The graph lines are also not rendering correctly.

Any help would be greatly appreciated!

Edit 1:

I solved the issue of the graph redrawing over itself and the restore_background() call not erasing everything. I first set self.backgrounds = None and modified update_graph() to check first if the backgrounds have been saved and to save them if they haven't been. This gives the GTK main thread time to update and resize the windows and plots correctly and then it saves the backgrounds once that is done.

def update_graph(self):
    if self.backgrounds is None:
        self.backgrounds = [self.fig.canvas.copy_from_bbox(ax.bbox) for ax in self.axes]

    try:
        items = enumerate(zip(self.lines, self.axes, self.backgrounds), start=1)
        for j, (line, ax, background) in items:
            line.set_ydata(np.sin(j*self.x + self.i/10.))
            self.fig.canvas.restore_region(background)
            ax.draw_artist(line)
            self.fig.canvas.blit(ax.bbox)
        self.i += 1
    except:
        pass
    return True

I still have the issue that the graph border and ticks and not being redrawn if the update interval is faster than 130ms. If timeout_add() uses anything less than 130, the graph ticks and border are not drawn correctly. I'm assuming this is because it takes longer than 130ms to redraw the ticks so this may need to be saved as another set of "backgrounds".

Edit 2:

Another breakthrough! Solved the update interval issue with help from this answer. The code below is able to refresh with an interval of 2ms but I noticed it still has some issues if I try to resize the window. It also has the added benefit of almost no CPU load! To resolve this I kept the refresh rate at 10ms. The goal with this will be to provide asynchronous updates to the graph anyway instead of regular update intervals for the test shown below.

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject

import numpy as np

import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas

class TestPlot(Gtk.ScrolledWindow):
    def __init__(self):
        Gtk.ScrolledWindow.__init__(self)

        self.background = None
        self.x = np.arange(0, 2*np.pi, 0.1)
        self.y = np.sin(self.x)
        self.z = np.cos(self.x)

        self.fig = Figure()
        self.canvas = FigureCanvas(self.fig)
        self.ax = self.fig.add_subplot(111, axisbg="black")

        self.add_with_viewport(self.canvas)

        self.red_line,   = self.ax.plot(self.x, self.y, "r-", animated=True)
        self.green_line, = self.ax.plot(self.x, self.z, "lime", animated=True)

        self.i = 1
        self.canvas.mpl_connect("draw_event", self.on_draw)
        GObject.timeout_add(10, self.update_graph)

    def update_graph(self):
        try:
            self.red_line.set_ydata(np.sin(self.x + self.i/10.))
            self.green_line.set_ydata(np.cos(self.x + self.i/10.))
            self.fig.canvas.restore_region(self.background)
            self.ax.draw_artist(self.red_line)
            self.ax.draw_artist(self.green_line)
            self.fig.canvas.blit(self.ax.clipbox)
            self.i += 1
        except:
            pass
        return True

    def save_bg(self):
        self.background = self.fig.canvas.copy_from_bbox(self.ax.get_figure().bbox)

    def on_draw(self, *args):
        self.save_bg()
        return False

class TestWindow(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self, title="TestWindow")
        self.set_default_size(700, 500)
        self.graph = TestPlot()
        self.add(self.graph)

win = TestWindow()
win.connect("delete-event", Gtk.main_quit)
win.show_all()
Gtk.main()
radial
  • 21
  • 4

0 Answers0