Flickering windows for Matplotlib animation

221 views Asked by At

I'm trying to create a simulator in Python with Matplotlib as the GUI. I'd like to have a number of different figures, all individually animated - i.e. one showing the main simulation window, another showing graphs, etc. I'm using the Matplotlib set_data() function to animate the plots, rather than clearing and re-drawing everything each time, which is working well, however all of the windows are flickering, and I'd really like to avoid this if possible (I've looked at this question: Matplotlib Figure flickering when updating, but the answer didn't work for me unfortunately).

The best way to do this might be using the Matplotlib FuncAnimation class, however I'd like to drive the animation externally (e.g. by calling an update function as and when I want to update the plots), whereas FuncAnimation seems to 'have control' of the clock, which isn't appropriate for my use case. I've tried the 'blitting' approach described here: How do I re-draw only one set of axes on a figure in python?, but this doesn't seem to stop the flickering, although I've very new to animation so I'm probably not doing it correctly.

A few observations:

  • When there's just one window (figure) open, the flickering doesn't happen. E.g. if there are two Matplotlib windows flickering and then one is closed, the one left open will start running much more smoothly (and will no longer flicker).
  • A window with an animated figure will flicker even if the second window doesn't contain an animated figure - it seems to be related to there being multiple Matplotlib windows open.

Does anyone know how I can fix this? Here's a minimal working example of my problem:

import matplotlib.pyplot as plt

class View:
    def __init__(self):
        plt.ion()
        self.sim_fig, self.sim_ax = plt.subplots(num="Simulator")
        self.energy_fig, self.energy_ax = plt.subplots(num="Energy")

        # Set axis limits
        x_min, x_max = (-3, 6)
        y_min, y_max = (-6, 3)
        self.sim_ax.set_xlim(x_min, x_max)
        self.sim_ax.set_ylim(y_min, y_max)
        self.sim_ax.axis('equal')
        self.energy_ax.set_xlim(0, 200)
        self.energy_ax.set_ylim(-20, 20)

        self.times = []
        self.energies = []

        # Create plot objects for animation
        self.posn_pt, = self.sim_ax.plot([], [], 'bo')
        self.energy_data, = self.energy_ax.plot([], [], '.-')

        plt.show(block=False)
        
    def update(self, time, x, y, energy):
        # Allows non-constant updates between steps
        self.times.append(time)

        # Update dot position
        self.posn_pt.set_xdata(x)
        self.posn_pt.set_ydata(y)

        # Append energy
        self.energies.append(energy)
        self.energy_data.set_data(self.times, self.energies)

        plt.draw()


view = View()
from random import uniform
x = 0
y = 0
energy = 0
time = 0

# Simulator is a bit like this (but obviously much more complicated!)
# Time between steps is variable (might not be constant)
view.update(time, x, y, energy)
for i in range(200):
    x += uniform(-0.02, 0.05)
    y += uniform(-0.05, 0.02)
    energy += uniform(-1, 1)
    time += uniform(0, 1)
    view.update(time, x, y, energy)
    plt.pause(0.05)

input("Simulation finished")

... and with my attempt to 'blit'...

import matplotlib.pyplot as plt

class View:
    def __init__(self):
        plt.ion()
        self.sim_fig, self.sim_ax = plt.subplots(num="Simulator")
        self.sim_fig.canvas.draw()
        self.sim_background = self.sim_fig.canvas.copy_from_bbox(self.sim_ax.bbox)

        self.energy_fig, self.energy_ax = plt.subplots(num="Energy")
        self.energy_fig.canvas.draw()
        self.energy_background = self.energy_fig.canvas.copy_from_bbox(self.energy_ax.bbox)

        # Set axis limits
        x_min, x_max = (-3, 6)
        y_min, y_max = (-6, 3)
        self.sim_ax.set_xlim(x_min, x_max)
        self.sim_ax.set_ylim(y_min, y_max)
        self.sim_ax.axis('equal')
        self.energy_ax.set_xlim(0, 200)
        self.energy_ax.set_ylim(-20, 20)

        self.times = []
        self.energies = []

        # Create plot objects for animation
        self.posn_pt, = self.sim_ax.plot([], [], 'bo')
        self.energy_data, = self.energy_ax.plot([], [], '.-')

        # Part of the flickerless-attempt animation code
        self.sim_fig.canvas.draw()
        self.energy_fig.canvas.draw()

        plt.show(block=False)
        
    def update(self, time, x, y, energy):
        # For flickerless plotting
        self.sim_fig.canvas.restore_region(self.sim_background)
        self.energy_fig.canvas.restore_region(self.energy_background)

        # Allows non-constant updates between steps
        self.times.append(time)

        # Update dot position
        self.posn_pt.set_xdata(x)
        self.posn_pt.set_ydata(y)

        # Append energy
        self.energies.append(energy)
        self.energy_data.set_data(self.times, self.energies)

        self.sim_ax.draw_artist(self.posn_pt)
        self.energy_ax.draw_artist(self.energy_data)
        self.sim_fig.canvas.blit(self.sim_ax.bbox)
        self.energy_fig.canvas.blit(self.energy_ax.bbox)


view = View()
from random import uniform
x = 0
y = 0
energy = 0
time = 0

# Simulator is a bit like this (but obviously much more complicated!)
# Time between steps is variable (might not be constant)
view.update(time, x, y, energy)
for i in range(200):
    x += uniform(-0.02, 0.05)
    y += uniform(-0.05, 0.02)
    energy += uniform(-1, 1)
    time += uniform(0, 1)
    view.update(time, x, y, energy)
    plt.pause(0.05)

input("Simulation finished")

I'd also like to be able to dynamically rescale the axes, which some answers have said complicates things.

0

There are 0 answers