Search code examples
pythonmatplotlibgraphcolorspython-multithreading

Wrongly fill_between in matplotlib when using threads


This question actually builds further onto my question I posted last week. (How to partial fill_between in matplotlib, as in different colors for different values)

I'm trying to record the amount of data per second on a CANBus and plot this on a graph. I'm apologizing in advance for the lots of code, but the three methods I tried are somewhat similar.

This is the static situation, which works:

import matplotlib
matplotlib.use('Qt5Agg')
from matplotlib import pyplot as plt
import sys
from collections import deque
import logging
import time

logging.basicConfig(level=logging.INFO)

buffer_size = 120
lvl = buffer_size * [100]
llvl = buffer_size * [95]
t = [t for t in range(buffer_size)]

bitthrough = deque(buffer_size*[0], buffer_size)

y = [107, 108, 105, 109, 107, 106, 107, 109, 106, 106, 94, 93, 94, 93, 93, 94, 95, 106, 108, 109, 107, 107, 106, 108, 105, 108, 107, 106, 107, 97, 93, 96, 94, 96, 95, 94, 104, 107, 106, 108, 107, 107, 106, 107, 105, 107, 108, 105, 107, 100, 93, 94, 93, 95, 104, 107, 107, 108, 108, 107, 107, 107, 107, 104, 94, 96, 95, 96, 94, 95, 94, 100, 107, 107, 105, 107, 107, 109, 107, 108, 107, 105, 108, 108, 106, 97, 94, 94, 94, 94, 95, 94, 94, 94, 96, 108, 108, 107, 106, 107, 107, 108, 107, 106, 95, 95, 95, 94, 94, 96, 105, 108, 107, 106, 106, 108, 107, 108, 106, 107]

bitthrough = y

fig, ax = plt.subplots()

ax.set_ylim(0, 120)
ax.set_xlim(0, 120)

ln, = plt.plot([], color='black')
plt.ion()
plt.show()

while True:
    plt.pause(1)

    ln.set_xdata(range(buffer_size))
    ln.set_ydata(bitthrough)

    wh_green = [a <= b for a,b in zip(bitthrough, llvl)]
    wh_orange = [a > b and a <= c for a, b, c in zip(bitthrough, llvl, lvl)]
    wh_red = [a > b for a, b, in zip(bitthrough, lvl)]

    ax.fill_between(t, 0, bitthrough, where=wh_red, color='red', interpolate=True)
    ax.fill_between(t, 0, bitthrough, where=wh_orange, color='orange', interpolate=True)
    ax.fill_between(t, 0, bitthrough, where=wh_green, color='green', interpolate=True)
    fig.canvas.draw_idle()

This results in the following graph: (which is fine for me, now) static situation graph

But when I want to update the graph live using threads, things break. This is the code I have. canbus is a module I made to monitor a CANBus. PCAN.throughput() is a asyncio coroutine function that counts the number of messages in a second. After the second is passed, it returns with the amount of kilobytes it received on the bus. The terminal shows the content of bitthrough, which gets filled in a seperate thread.

from canbus import PCAN
import matplotlib
matplotlib.use('Qt5Agg')
from matplotlib import pyplot as plt
import sys
import asyncio
from collections import deque
import logging
import time
import threading
from matplotlib.figure import Figure

logging.basicConfig(level=logging.INFO)

buffer_size = 120
lvl = buffer_size * [100]
llvl = buffer_size * [95]
t = [t for t in range(buffer_size)]

loop = asyncio.get_event_loop()

cn = PCAN()
loop.run_until_complete(cn.poll_ids())
loop.run_until_complete(cn.get_names())
print(cn.names)

bitthrough = deque(buffer_size*[0], buffer_size)


def get_bt(loop):
    asyncio.set_event_loop(loop)
    while True:
        task = loop.run_until_complete(cn.throughput())
        bitthrough.append(task[0] / 33. * 100.)
        print(bitthrough)


thread = threading.Thread(target=get_bt, args=(loop,))
thread.daemon = True
thread.start()

fig, ax = plt.subplots()

ax.set_ylim(0, 120)
ax.set_xlim(0, 120)

ln, = plt.plot([], color='black')
plt.ion()
plt.show()

while True:
    plt.pause(1)
    ln.set_xdata(range(buffer_size))
    ln.set_ydata(bitthrough)

    wh_green = [a <= b for a,b in zip(bitthrough, llvl)]
    wh_orange = [a > b and a <= c for a, b, c in zip(bitthrough, llvl, lvl)]
    wh_red = [a > b for a, b, in zip(bitthrough, lvl)]

    ax.fill_between(t, 0, bitthrough, where=wh_red, color='red', interpolate=True)
    ax.fill_between(t, 0, bitthrough, where=wh_orange, color='orange', interpolate=True)
    ax.fill_between(t, 0, bitthrough, where=wh_green, color='green', interpolate=True)
    fig.canvas.draw_idle()

This code results in the following:

threading case graph

Notice how the color red keeps on the highest level it was before.

I also tried to implement it with the Qt backend of matplotlib. However, I just got an empty graph that couldn't get updated. (Without threads this method worked, but it slogged down my computer leaving me unable to do something else)

from canbus import PCAN
# import numpy as np
import matplotlib
matplotlib.use('Qt5Agg')
from matplotlib import pyplot as plt
from matplotlib.animation import FuncAnimation
import sys
import asyncio
from collections import deque
import logging
import time
import threading
import datetime
from matplotlib.backends.qt_compat import QtCore, QtWidgets
from matplotlib.backends.backend_qt5agg import (
        FigureCanvas, NavigationToolbar2QT as NavigationToolbar)

from matplotlib.figure import Figure

logging.basicConfig(level=logging.INFO)

buffer_size = 120
lvl = buffer_size * [100]
llvl = buffer_size * [95]
t = [t for t in range(buffer_size)]

loop = asyncio.get_event_loop()

cn = PCAN()
loop.run_until_complete(cn.poll_ids())
loop.run_until_complete(cn.get_names())
print(cn.names)

bitthrough = deque(buffer_size*[0], buffer_size)


def get_bt(loop):
    asyncio.set_event_loop(loop)
    while True:
        task = loop.run_until_complete(cn.throughput())
        bitthrough.append(task[0] / 33. * 100.)
        print(bitthrough)


thread = threading.Thread(target=get_bt, args=(loop,))
thread.daemon = True
thread.start()

class ApplicationWindow(QtWidgets.QMainWindow):

    def __init__(self):

        super().__init__()

        self._main = QtWidgets.QWidget()
        self.setCentralWidget(self._main)
        layout = QtWidgets.QVBoxLayout(self._main)

        dynamic_canvas = FigureCanvas(Figure(figsize=(5, 3)))
        layout.addWidget(dynamic_canvas)

        self._dynamic_ax = dynamic_canvas.figure.subplots()

        self._update_canvas()

    def _update_canvas(self):

        # p = loop.run_until_complete(cn.throughput())[0] / 500. * 100.
        # bitthrough.append(p)

        wh_green = [a <= b for a,b in zip(bitthrough, llvl)]
        wh_orange = [a > b and a <= c for a, b, c in zip(bitthrough, llvl, lvl)]
        wh_red = [a > b for a, b, in zip(bitthrough, lvl)]

        self._dynamic_ax.clear()
        self._dynamic_ax.fill_between(t, 0, bitthrough, where=wh_red, color='red', interpolate=True)
        self._dynamic_ax.fill_between(t, 0, bitthrough, where=wh_orange,color='orange', interpolate=True)
        self._dynamic_ax.fill_between(t, 0, bitthrough, where=wh_green, color='green', interpolate=True)
        self._dynamic_ax.plot(t, bitthrough, color="black")
        self._dynamic_ax.set_ylim(0, 120)
        logging.info("redrawing graph")
        self._dynamic_ax.figure.canvas.draw()


if __name__ == "__main__":

    qapp = QtWidgets.QApplication(sys.argv)
    app = ApplicationWindow()
    app.show()
    qapp.exec_()
    while True:
        app._update_canvas()
        time.sleep(1)

Which gave me this: qt matplotlib backend

Also not good, and I'm kinda lost right now.


Solution

  • I found the problem is my last solution (using Qt backend of matplotlib). I called

    while True:
        app._update_canvas()
        time.sleep(1)
    

    outside the Qt application. So the graph didn't get updated, hence, an empty graph. Moving app._update_canvas() to inside the class and calling it periodically with a QTimer plots the graph correctly.

    Added to the __init__ function of ApplicationWindow:

    self.timer = QtCore.QTimer(self)
    self.timer.timeout.connect(self._update_canvas)
    self.timer.start(1000)