Python Forum
Matplotlib failing in thread class
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Matplotlib failing in thread class
#1
Hello,

I am new to this forum and apologize if I'm posting in the wrong area or this type of question is not meant for this forum. I'm looking for some guidance regarding a fundamental issue I've localized to threaded functionality.

I'm working on a higher performance plotter for matplotlib which I think I've made substantial progress on. I want to spawn the plot off my GUI so to prevent it from blocking I figured I need to make it a daemon thread so users could still interact with the plot and the GUI. I generally use this approach when I need to launch a task and don't want it to block GUI functionality. I have to plotting working unthreaded, but when I put it in a thread class I can't inteeract (drag the plot window around for example). I feel like I'm missing something fundamental here so thats why I've been looking for a way to ask experts. I'm an EE not a software engineer so my education in this is less than optimal.

I've tried to create two Python modules that show and and isolate the issue. I've attached a zip file with two python files. Sanbox1.py works and seems to behave like I'd expect. Sandbox2.py is the same code basically added to the "Thread" class. My goal was to spawn this plotting functionality off on a separate thread. I can't figure out why Sandbox2.py works differently land doesn't let me drag the window around or interact with it. If I try it just crashes.

I found this documentation that I have been following and studying to try to understand this.
Example From Documentation

I couldn't figure out how to attach a zip file so the code is below:

Sandbox1.py - working well
import matplotlib.pyplot as plt
import numpy as np


class BlitManager:
    def __init__(self, canvas, animated_artists=()):
        """
        Parameters
        ----------
        canvas : FigureCanvasAgg
            The canvas to work with, this only works for sub-classes of the Agg
            canvas which have the `~FigureCanvasAgg.copy_from_bbox` and
            `~FigureCanvasAgg.restore_region` methods.

        animated_artists : Iterable[Artist]
            List of the artists to manage
        """
        self.canvas = canvas
        self._bg = None
        self._artists = []

        for a in animated_artists:
            self.add_artist(a)
        # grab the background on every draw
        self.cid = canvas.mpl_connect("draw_event", self.on_draw)

    def on_draw(self, event):
        """Callback to register with 'draw_event'."""
        cv = self.canvas
        if event is not None:
            if event.canvas != cv:
                raise RuntimeError
        self._bg = cv.copy_from_bbox(cv.figure.bbox)
        self._draw_animated()

    def add_artist(self, art):
        """
        Add an artist to be managed.

        Parameters
        ----------
        art : Artist

            The artist to be added.  Will be set to 'animated' (just
            to be safe).  *art* must be in the figure associated with
            the canvas this class is managing.

        """
        if art.figure != self.canvas.figure:
            raise RuntimeError
        art.set_animated(True)
        self._artists.append(art)

    def _draw_animated(self):
        """Draw all of the animated artists."""
        fig = self.canvas.figure
        for a in self._artists:
            fig.draw_artist(a)

    def update(self):
        """Update the screen with animated artists."""
        cv = self.canvas
        fig = cv.figure
        # paranoia in case we missed the draw event,
        if self._bg is None:
            self.on_draw(None)
        else:
            # restore the background
            cv.restore_region(self._bg)
            # draw all of the animated artists
            self._draw_animated()
            # update the GUI state
            cv.blit(fig.bbox)
        # let the GUI event loop process anything it has to do
        cv.flush_events()





class Top(object):
    def __init__(self):
        self.x = np.linspace(0, 2 * np.pi, 100)

        # make a new figure
        self.fig, self.ax = plt.subplots()
        # add a line
        (self.ln,) = self.ax.plot(self.x, np.sin(self.x), animated=True)
        # add a frame number
        self.fr_number = self.ax.annotate(
            "0",
            (0, 1),
            xycoords="axes fraction",
            xytext=(10, -10),
            textcoords="offset points",
            ha="left",
            va="top",
            animated=True,
        )
        self.bm = BlitManager(self.fig.canvas, [self.ln, self.fr_number])
        # make sure our window is on the screen and drawn
        plt.show(block=False)
        plt.pause(.1)



    def loop(self):
        for j in range(1000):
            # update the artists
            self.ln.set_ydata(np.sin(self.x + (j / 1000.) * np.pi))
            self.fr_number.set_text("frame: {j}".format(j=j))
            # tell the blitting manager to do it's thing
            self.bm.update()

T = Top()
T.loop()
Sandbox2.py - freezes upon interaction
import matplotlib.pyplot as plt
import numpy as np
import threading

class BlitManager:
    def __init__(self, canvas, animated_artists=()):
        """
        Parameters
        ----------
        canvas : FigureCanvasAgg
            The canvas to work with, this only works for sub-classes of the Agg
            canvas which have the `~FigureCanvasAgg.copy_from_bbox` and
            `~FigureCanvasAgg.restore_region` methods.

        animated_artists : Iterable[Artist]
            List of the artists to manage
        """
        self.canvas = canvas
        self._bg = None
        self._artists = []

        for a in animated_artists:
            self.add_artist(a)
        # grab the background on every draw
        self.cid = canvas.mpl_connect("draw_event", self.on_draw)

    def on_draw(self, event):
        """Callback to register with 'draw_event'."""
        cv = self.canvas
        if event is not None:
            if event.canvas != cv:
                raise RuntimeError
        self._bg = cv.copy_from_bbox(cv.figure.bbox)
        self._draw_animated()

    def add_artist(self, art):
        """
        Add an artist to be managed.

        Parameters
        ----------
        art : Artist

            The artist to be added.  Will be set to 'animated' (just
            to be safe).  *art* must be in the figure associated with
            the canvas this class is managing.

        """
        if art.figure != self.canvas.figure:
            raise RuntimeError
        art.set_animated(True)
        self._artists.append(art)

    def _draw_animated(self):
        """Draw all of the animated artists."""
        fig = self.canvas.figure
        for a in self._artists:
            fig.draw_artist(a)

    def update(self):
        """Update the screen with animated artists."""
        cv = self.canvas
        fig = cv.figure
        # paranoia in case we missed the draw event,
        if self._bg is None:
            self.on_draw(None)
        else:
            # restore the background
            cv.restore_region(self._bg)
            # draw all of the animated artists
            self._draw_animated()
            # update the GUI state
            cv.blit(fig.bbox)
        # let the GUI event loop process anything it has to do
        cv.flush_events()





class Top(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.x = np.linspace(0, 2 * np.pi, 100)

        # thread loop flag
        self.thread_event = threading.Event()
        self.thread_event.set()     # set thread by default

        # make a new figure
        self.fig, self.ax = plt.subplots()
        # add a line
        (self.ln,) = self.ax.plot(self.x, np.sin(self.x), animated=True)
        # add a frame number
        self.fr_number = self.ax.annotate(
            "0",
            (0, 1),
            xycoords="axes fraction",
            xytext=(10, -10),
            textcoords="offset points",
            ha="left",
            va="top",
            animated=True,
        )
        self.bm = BlitManager(self.fig.canvas, [self.ln, self.fr_number])
        # make sure our window is on the screen and drawn
        plt.show(block=False)
        plt.pause(.1)



    #def loop(self):
    def run(self):
        #for j in range(1000):
        j = 1
        while self.thread_event.is_set():
            # update the artists
            self.ln.set_ydata(np.sin(self.x + (j / 1000.) * np.pi))
            self.fr_number.set_text("frame: {j}".format(j=j))
            # tell the blitting manager to do it's thing
            self.bm.update()
            j = j + 1






T = Top()
# T.loop()
T.start()
T.isDaemon()
My versions are:
Windows 10
Python 2.7.17
matplotlib 2.2.5

Any advice is greatly appreciated! Thank you for your time!
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020