7
\$\begingroup\$

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()
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

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.  
"""
\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.