I would like to share a matplotlib
real-time monitor plot that I needed for another application. The basic trick is to reverse the time, so that t=0 is now and t=20 is twenty seconds in the past and to plot the signal from 0 at the right towards the left. The core method is update
that reverses time using the deque
appendleft
and applying a reverse axis by self.ax_monitor.set_xlim(self.monitor_time_window_s, 0)
, i.e., from high to low, rather than the typical low to high.
The following code is a running example
(requires matplotlib
and numpy
):
from collections import deque
import time
import numpy as np
import matplotlib.pyplot as plt
fps = 24
seconds_per_frame = 1 / fps
class Monitor():
''' class to monitor value versus time
'''
def __init__(self, figsize=(5, 2), monitor_window=10, update_interval=0.1,
ymin=-1, ymax=1, ticks=None):
self.monitor_time_window_s = monitor_window
self.update_monitor_interval_s = update_interval
self.fig_monitor, self.ax_monitor = plt.subplots(
1, 1, figsize=figsize)
self.ax_monitor.set_ylim(ymin, ymax)
if ticks is None:
ticks = [ymin, 0, ymax]
self.ax_monitor.set_yticks(ticks)
self.ax_monitor.tick_params(axis='y', which='major', labelsize=6)
self.ax_monitor.tick_params(axis='x', which='major', labelsize=6)
self.ax_monitor.grid(True)
self.fig_monitor.tight_layout()
self.monitor_plot, = self.ax_monitor.plot(
[0], [0], color='black', linewidth=0.5)
self.ax_monitor.set_xlim(self.monitor_time_window_s, 0)
@property
def update_time_s(self):
return self.update_monitor_interval_s
@property
def monitor_fig(self):
return self.fig_monitor
@property
def monitor_ax(self):
return self.ax_monitor
def set_fig_title(self, title='Monitor Figure'):
self.fig_monitor.canvas.manager.set_window_title(title)
def set_ax_title(self, title='Monitor'):
self.ax_monitor.set_title(title)
self.fig_monitor.tight_layout()
def blit(self):
self.fig_monitor.canvas.draw()
self.fig_monitor.canvas.flush_events()
def update(self, _time, value):
''' method to plot value in time_window_graphs, time is
past time, so t=0 is now and t=20 is 20 seconds ago
'''
# reset when time is zero
if _time < self.update_monitor_interval_s:
self.monitor_values = []
self.time_values_reversed = deque([])
self.time_reversed = []
# build the time_reversed list (in reversed order to represent
# time passed) and trim the monitor_values so they keep the
# a finite length
self.monitor_values.append(value)
if _time < self.monitor_time_window_s + self.update_monitor_interval_s:
self.time_values_reversed.appendleft(_time)
self.time_reversed = list(self.time_values_reversed)
else:
self.monitor_values.pop(0)
self.monitor_plot.set_data(self.time_reversed, self.monitor_values)
def run_monitor(event, monitor):
def current_time():
return time.time()
_time = 0
actual_start_time = current_time()
while True:
value = np.sin(_time) - 0.5 + np.random.random_sample()
if _time % monitor.update_time_s < seconds_per_frame:
monitor.update(_time, value)
monitor.blit()
_time += seconds_per_frame
# wait for actual time to catch up with model time _time
running_time = current_time() - actual_start_time
while running_time < _time:
running_time = current_time() - actual_start_time
def main():
monitor = Monitor(monitor_window=20, ymin=-1.6, ymax=1.6,
ticks=[-1.5, -1.0, -0.5, 0.0, +0.5, 1.0, 1.5])
monitor.set_fig_title('Press any key to start ...')
monitor.set_ax_title()
monitor.monitor_fig.canvas.mpl_connect(
'key_press_event', lambda event: run_monitor(event, monitor))
plt.show()
if __name__ == "__main__":
main()
1 Answer 1
UX
The GUI looks great, and the message at the top of the graph is helpful.
However, exiting out of the GUI is a little awkward. When I click on
the "x", the GUI window closes, but I still need to Ctrl-C in my shell to end
the program. Perhaps using matplotlib
close
would be cleaner.
Naming
The PEP 8 style guide recommends using upper case for constants:
fps
would be FPS
(or spell it out as FRAMES_PER_SECOND
)
seconds_per_frame
would be SECONDS_PER_FRAME
There are many functions and variables with "time" in their name.
However, the variable named _time
is vague. If it represents the past
time, perhaps past_time
would be a better name.
Unused code
The event
variable is not used in this function, and it should be removed:
def run_monitor(event, monitor):
DRY
In the run_monitor
function, this line is repeated twice:
running_time = current_time() - actual_start_time
You could eliminate the running_time
variable and use this loop instead:
while True:
if current_time() - actual_start_time < _time:
break
Documentation
You could add a docstring to the top of the code to summarize its purpose:
"""
Real-time monitor plot.
The basic trick is to reverse the time, so that t=0 is now and t=20
is twenty seconds in the past and to plot the signal from 0 at
the right towards the left.
"""