Real-time update of a Matplotlib graph

Real-time update of a Matplotlib graph


In this section, we will present a simple application—a CPU usage monitor, where we will update the Matplotlib graph in real-time (once every second over a period of 30 seconds).

As there are several indicators of CPU usage in a modern operating system, we decided to restrict our graph to the four main ones:

  • user: The time consumed by processes executed by the users of the machine

  • nice: The time consumed by processes executed by users but with a lower priority

  • system: The time consumed by system tasks

  • idle: The time consumed waiting for something to execute

The ignored indicators contribute for just a minimal part of the CPU usage, so their exclusion doesn't disturb the validity of the example.

Of those four indicators, what we will plot is the percentage of each of them against the total CPU usage.

Here we start:

import sys
from PyQt4 import QtGui

These are the modules for command-line parameters and for Python bindings of the QtGui submodule.

from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg \
import FigureCanvasQTAgg as FigureCanvas

The classic Matplotlib import for the required objects.

import psutil as p

Python is full of modules that do almost everything we can think of, so for our example, we leverage an existing module—psutils.

psutil is a multiplatform module (available for Windows, Linux, and Mac OS X) that exports a common interface to access system information such as processes, memory, CPU, and so on. We will use its functionalities to obtain the CPU usage.

MAXITERS = 30

This is the total number of iterations we want to perform. Since we perform an iteration every second, the total number of iterations equals 30 seconds of CPU usage monitoring.

class CPUMonitor(FigureCanvas):

As usual, we define a class for the Matplotlib graph elaboration.

def __init__(self):
self.before = self.prepare_cpu_usage()

In the initialization method, we save the current CPU usage information. The algorithm we will use to update the graph needs a previous set of values.

self.fig = Figure()
self.ax = self.fig.add_subplot(111)

The basic Matplotlib Figure and Axes initialization.

FigureCanvas.__init__(self, self.fig)

Initialization for the canvas, referring to the Figure object defined earlier.

self.ax.set_xlim(0, 30)
self.ax.set_ylim(0, 100)
self.ax.set_autoscale_on(False)

We set the limits for the X-axis (to have a 30 seconds interval) and for the Y-axis (the range of percentage usage), and then disable the autoscale feature. This will allow us to have a fixed dimension Axes, where the plot can be redrawn without resizing the figure.

self.user, self.nice, self.sys, self.idle = [], [], [], []
self.l_user, = self.ax.plot([],self.user, label='User %')
self.l_nice, = self.ax.plot([],self.nice, label='Nice %')
self.l_sys, = self.ax.plot([],self.sys, label='Sys %')
self.l_idle, = self.ax.plot([],self.idle, label='Idle %')

This draws a placeholder line for the four datasets that we will use. This is important because we now have the references to the four line objects, and we can dynamically update their information without generating a new object at every iteration.

self.ax.legend()

Here, we add a legend.

self.fig.canvas.draw()

The preceding line of code forces a draw of the canvas.

self.cnt = 0

We initialize the iterations counter to 0.

self.timerEvent(None)

We make an explicit call to the method that we will use to update the graph dynamically: this is a little trick used to speed up the visualization of the plot.

self.timer = self.startTimer(1000)

With this command, we start a timer object and save the reference to self.timer. QTimer is a class that triggers an event every n milliseconds (the interval is specified on instantiation).

In this case, our timer will generate an event every second, and we will use this event to update our graph.

The main loop (started by exec_()) processes the timer events (along with all the others) and delivers them to this widget.

def prepare_cpu_usage(self):

This function will take care of preparing the CPU usage information we need.

t = p.cpu_times()

We use the psutil cpu_times() method to retrieve the current CPU usage.

if hasattr(t, 'nice'):
return [t.user, t.nice, t.system, t.idle]
else:
# special case for Windows, without 'nice' value
return [t.user, 0, t.system, t.idle]

We check if the nice attribute is available. On a Unix-like system, that attribute is present, so we can return the whole set of data. On Windows, where there is no distinction between user and nice processes, the attribute is missing, so we set its value to 0 while still returning the other indicators. The net result is that we have a cross-platform code able to run on Windows and on Unix-like systems.

def get_cpu_usage(self):

We define another function to take the values from prepare_cpu_usage() and compute the information needed for plotting.

now = self.prepare_cpu_usage()

We take the current CPU usage values.

delta = [now[i]-self.before[i] for i in range(len(now))]

Then we compute the deltas from the previous measurement to the current one. Delta is the Greek letter for d (from difference), commonly used when dealing with differences.

Values returned from psutil cpu_times() are counters. They are monotonically increased from the moment the machine boots until it is turned off. What we're interested in is the amount of CPU taken by each of the four categories that we plot during the measurement interval. These commands do this evaluation.

total = sum(delta)

We compute the sum of the deltas.

self.before = now

We replace the self.before value with now, so at the next iteration, the current values will be used.

return [(100.0*dt)/total for dt in delta]

and we return the CPU usage percentage for our four categories. dt/total represents the fraction of the total time used by that category and multiplying it by 100.0 generates a percentage value.

def timerEvent(self, evt):

As said, events are an important part of Qt, and the main loop receives and dispatches them to the right widgets. A common way by which a widget processes events is by reimplementing event handlers.

In our example, QTimer object generates (at a regular rate of one event per second) a QTimerEvent that is sent to our widget (since it's the widget that started the timer).

To modify the widget behavior following the reception of an event, we need to define our own event handler, which in the case of a timer is called timerEvent().

result = self.get_cpu_usage()

We get the current percentage values for CPU usage.

self.user.append(result[0])
self.nice.append(result[1])
self.sys.append( result[2])
self.idle.append(result[3])

We add them to the relevant datasets.

self.l_user.set_data(range(len(self.user)), self.user)
self.l_nice.set_data(range(len(self.nice)), self.nice)
self.l_sys.set_data( range(len(self.sys)), self.sys)
self.l_idle.set_data(range(len(self.idle)), self.idle)

Now, we replot the lines with the updated information. We have added one item to each indicator's list, and now we are updating the line objects to reflect the new data. Updating the lines instead of creating a completely new plot (or removing the old lines and adding new ones) is faster and does not create any annoying visual effect on the window.

self.fig.canvas.draw()

We force a redraw of the canvas to actually show the changed lines.

if self.cnt == MAXITERS:

then we have to check if we've performed all the iterations or not.

self.killTimer(self.timer)

Once we have completed all the iterations, we stop the timer by calling killTimer() and passing the timer reference that we had created and saved when creating startTimer().

else:
self.cnt += 1

Alternatively, we simply increment the counter and wait for the next timer event to occur.

app = QtGui.QApplication(sys.argv)

we now start the main part of the application, and as the first thing, we create our wrapper QApplication instance.

widget = CPUMonitor()

Here, we create our CPUMonitor widget.

widget.setWindowTitle("30 Seconds of CPU Usage Updated in RealTime")

we set the window title.

widget.show()

We show the widget.

sys.exit(app.exec_())

and at the end, we start the main loop.

Here is the full example code:

# for command-line arguments
import sys
# Python Qt4 bindings for GUI objects
from PyQt4 import QtGui
# Matplotlib Figure object
from matplotlib.figure import Figure
# import the Qt4Agg FigureCanvas object, that binds Figure to
# Qt4Agg backend. It also inherits from QWidget
from matplotlib.backends.backend_qt4agg \
import FigureCanvasQTAgg as FigureCanvas
# used to obtain CPU usage information
import psutil as p
# Total number of iterations
MAXITERS = 30
class CPUMonitor(FigureCanvas):
"""Matplotlib Figure widget to display CPU utilization"""
def __init__(self):
# save the current CPU info (used by updating algorithm)
self.before = self.prepare_cpu_usage()
# first image setup
self.fig = Figure()
self.ax = self.fig.add_subplot(111)
# initialization of the canvas
FigureCanvas.__init__(self, self.fig)
# set specific limits for X and Y axes
self.ax.set_xlim(0, 30)
self.ax.set_ylim(0, 100)
# and disable figure-wide autoscale
self.ax.set_autoscale_on(False)
# generates first "empty" plots
self.user, self.nice, self.sys, self.idle =[], [], [], []
self.l_user, = self.ax.plot([],self.user, label='User %')
self.l_nice, = self.ax.plot([],self.nice, label='Nice %')
self.l_sys, = self.ax.plot([],self.sys, label='Sys %')
self.l_idle, = self.ax.plot([],self.idle, label='Idle %')
# add legend to plot
self.ax.legend()
# force a redraw of the Figure
self.fig.canvas.draw()
# initialize the iteration counter
self.cnt = 0
# call the update method (to speed-up visualization)
self.timerEvent(None)
# start timer, trigger event every 1000 millisecs (=1sec)
self.timer = self.startTimer(1000)
def prepare_cpu_usage(self):
"""helper function to return CPU usage info"""
# get the CPU times using psutil module
t = p.cpu_times()
# return only the values we're interested in
if hasattr(t, 'nice'):
return [t.user, t.nice, t.system, t.idle]
else:
# special case for Windows, without 'nice' value
return [t.user, 0, t.system, t.idle]
def get_cpu_usage(self):
"""Compute CPU usage comparing previous and current measurements"""
# take the current CPU usage information
now = self.prepare_cpu_usage()
# compute delta between current and previous measurements
delta = [now[i]-self.before[i] for i in range(len(now))]
# compute the total (needed for percentages calculation)
total = sum(delta)
# save the current measurement to before object
self.before = now
# return the percentage of CPU usage for our 4 categories
return [(100.0*dt)/total for dt in delta]
def timerEvent(self, evt):
"""Custom timerEvent code, called at timer event receive"""
# get the cpu percentage usage
result = self.get_cpu_usage()
# append new data to the datasets
self.user.append(result[0])
self.nice.append(result[1])
self.sys.append( result[2])
self.idle.append(result[3])
# update lines data using the lists with new data
self.l_user.set_data(range(len(self.user)), self.user)
self.l_nice.set_data(range(len(self.nice)), self.nice)
self.l_sys.set_data( range(len(self.sys)), self.sys)
self.l_idle.set_data(range(len(self.idle)), self.idle)
# force a redraw of the Figure
self.fig.canvas.draw()
# if we've done all the iterations
if self.cnt == MAXITERS:
# stop the timer
self.killTimer(self.timer)
else:
# else, we increment the counter
self.cnt += 1
# create the GUI application
app = QtGui.QApplication(sys.argv)
# Create our Matplotlib widget
widget = CPUMonitor()
# set the window title
widget.setWindowTitle("30 Seconds of CPU Usage Updated in RealTime")
# show the widget
widget.show()
# start the Qt main loop execution, exiting from this script
# with the same return code of Qt application
sys.exit(app.exec_())

Here is a screenshot which was taken while running the preceding application:

At the top of the window, we can see the green line for the CPU usage of nice processes, down below there is the blue line for user CPU usage, and very near the 0 we have system time (in red) and idle time (in cyan) lines, barely visible. In Chapter 7, we will present a similar technique to update a plot in real time, but with a much higher throughput.