Search code examples
pythonpython-3.xmatplotlibmatplotlib-widget

Placing animated graph with controlable parameters in a class


import matplotlib.gridspec as gridspec
import numpy as np

from matplotlib import animation
from matplotlib import pyplot as plt
from matplotlib.widgets import Slider, CheckButtons

PI = np.pi

sliderDataList = [{'name': 'Left amplitude', 'min': 0.1, 'max': 8.0, 'init': 2, 'step': 0.01}]
checkboxDataList = [{'name': 'Left wave', 'init': True}]


class CollidingWaves:
    def __init__(self, timeFactor=5, x_range=4 * PI, x_offset=0, y_range=4, y_offset=0, sliderData=[],
                 checkboxData=[], tension=1, massDensity=1):
        self.x_range = x_range
        self.x_offset = x_offset
        self.y_range = y_range
        self.y_offset = y_offset
        self.sliderData = sliderData
        self.checkboxData = checkboxData
        self.tension = tension
        self.massDensity = massDensity
        self.timeFactor = timeFactor
        self.showWave = []
        self.amplitude = 0

        self.fig = plt.figure()

        self.mainGrid = gridspec.GridSpec(2, 1)
        self.graphCell = plt.subplot(self.mainGrid[0, :])
        self.graphCell.set(xlim=(-self.x_range - self.x_offset, self.x_range - self.x_offset),
                           ylim=(-self.y_range - self.y_offset, self.y_range - self.y_offset))

        self.x_data = np.linspace(-self.x_range - self.x_offset, self.x_range - self.x_offset, 512)
        self.y_data = []

        self.lines = [plt.plot([], [])[0] for _ in range(2)]
        self.patches = self.lines

        self.controlCell = self.mainGrid[1, :]
        self.controlGrid = gridspec.GridSpecFromSubplotSpec(1, 7, self.controlCell)

        self.checkboxCell = self.controlGrid[0, 0]
        self.checkboxGrid = gridspec.GridSpecFromSubplotSpec(1, 1, self.checkboxCell)
        self.checkboxes = []
        self.checkboxAx = plt.subplot(self.checkboxGrid[0, 0:1])
        self.checkbox = CheckButtons(self.checkboxAx, tuple(x["name"] for x in self.checkboxData),
                                     tuple(x["init"] for x in self.checkboxData))
        self.checkboxes.append(self.checkbox)

        self.sliderCell = self.controlGrid[0, 2:6]
        self.sliderGrid = gridspec.GridSpecFromSubplotSpec(len(self.sliderData), 1, self.sliderCell)
        self.sliders = []
        for i in range(0, len(self.sliderData)):
            self.sliderAx = plt.subplot(self.sliderGrid[i, 0])
            self.slider = Slider(self.sliderAx, self.sliderData[i]["name"], self.sliderData[i]["min"],
                                 self.sliderData[i]["max"], valinit=self.sliderData[i]["init"],
                                 valstep=self.sliderData[i]["step"])
            self.sliders.append(self.slider)

        for slider in self.sliders:
            slider.on_changed(self.update)
        for checkbox in self.checkboxes:
            checkbox.on_clicked(self.update)

    def update(self):
        self.amplitude = self.sliders[0].val
        self.showWave = self.checkboxes[0].val

    def init(self):
        for line in self.lines:
            line.set_data([], [])
        return self.patches

    def animate(self, i):
        self.y_data[0] = [1] * 512
        self.y_data[1] = [2] * 512
        self.lines[0].set_data(self.x_data, self.y_data[0])
        self.lines[1].set_data(self.x_data, self.y_data[1])

        return self.patches

    def start(self):
        animation.FuncAnimation(self.fig, self.animate, init_func=self.init, frames=600, repeat=True, interval=20, blit=True)
        plt.show()


graph = CollidingWaves(sliderData=sliderDataList, checkboxData=checkboxDataList)
graph.start()

The idea of the snipped above is to have an animated graph and a set of widgets that control it's parameters. Changing the parameters should change the graph that is being displayed.

That being said, the code above does none of that. It's a animated graph that doesn't change and two widgets that change variables within the object. However, the program does not work as expected.

First of all, graph is not displayed at all. I don't understand why. Secondly, changing state of any of the two widgets throws a TypeError:

Traceback (most recent call last):
  File "C:\Programs\Python37\lib\site-packages\matplotlib\cbook\__init__.py", line 215, in process
    func(*args, **kwargs)
  File "C:\Programs\Python37\lib\site-packages\matplotlib\widgets.py", line 417, in _update
    self.set_val(val)
  File "C:\Programs\Python37\lib\site-packages\matplotlib\widgets.py", line 438, in set_val
    func(val)
TypeError: update() takes 1 positional argument but 2 were given

What am I doing wrong here?


Solution

  • Seems there are only four issues here:

    • update is called with an event as argument. You need to make sure the it actually takes this argument, even if you don't use it.
    • The checkbox does not have a val attribute. You get the status of the checkbox via .get_status.
    • The y_data is assigned two elements. Hence it needs to have two elements from the start.
    • The animation needs to stay in memory. Hence you would assign it to a variable.

    In total, this would work:

    import matplotlib.gridspec as gridspec
    import numpy as np
    
    from matplotlib import animation
    from matplotlib import pyplot as plt
    from matplotlib.widgets import Slider, CheckButtons
    
    PI = np.pi
    
    sliderDataList = [{'name': 'Left amplitude', 'min': 0.1, 'max': 8.0, 'init': 2, 'step': 0.01}]
    checkboxDataList = [{'name': 'Left wave', 'init': True}]
    
    
    class CollidingWaves:
        def __init__(self, timeFactor=5, x_range=4 * PI, x_offset=0, y_range=4, y_offset=0, sliderData=[],
                     checkboxData=[], tension=1, massDensity=1):
            self.x_range = x_range
            self.x_offset = x_offset
            self.y_range = y_range
            self.y_offset = y_offset
            self.sliderData = sliderData
            self.checkboxData = checkboxData
            self.tension = tension
            self.massDensity = massDensity
            self.timeFactor = timeFactor
            self.showWave = []
            self.amplitude = 0
    
            self.fig = plt.figure()
    
            self.mainGrid = gridspec.GridSpec(2, 1)
            self.ax = plt.subplot(self.mainGrid[0, :])
            self.ax.set(xlim=(-self.x_range - self.x_offset, self.x_range - self.x_offset),
                               ylim=(-self.y_range - self.y_offset, self.y_range - self.y_offset))
    
            self.x_data = np.linspace(-self.x_range - self.x_offset, self.x_range - self.x_offset, 512)
            self.y_data = [[],[]]
    
            self.lines = [self.ax.plot([], [])[0] for _ in range(2)]
            self.patches = self.lines
    
            self.controlCell = self.mainGrid[1, :]
            self.controlGrid = gridspec.GridSpecFromSubplotSpec(1, 7, self.controlCell)
    
            self.checkboxCell = self.controlGrid[0, 0]
            self.checkboxGrid = gridspec.GridSpecFromSubplotSpec(1, 1, self.checkboxCell)
            self.checkboxes = []
            self.checkboxAx = plt.subplot(self.checkboxGrid[0, 0:1])
            self.checkbox = CheckButtons(self.checkboxAx, tuple(x["name"] for x in self.checkboxData),
                                         tuple(x["init"] for x in self.checkboxData))
            self.checkboxes.append(self.checkbox)
    
            self.sliderCell = self.controlGrid[0, 2:6]
            self.sliderGrid = gridspec.GridSpecFromSubplotSpec(len(self.sliderData), 1, self.sliderCell)
            self.sliders = []
            for i in range(0, len(self.sliderData)):
                self.sliderAx = plt.subplot(self.sliderGrid[i, 0])
                self.slider = Slider(self.sliderAx, self.sliderData[i]["name"], self.sliderData[i]["min"],
                                     self.sliderData[i]["max"], valinit=self.sliderData[i]["init"],
                                     valstep=self.sliderData[i]["step"])
                self.sliders.append(self.slider)
    
            for slider in self.sliders:
                slider.on_changed(self.update)
            for checkbox in self.checkboxes:
                checkbox.on_clicked(self.update)
    
        def update(self, event=None):
            self.amplitude = self.sliders[0].val
            self.showWave = self.checkboxes[0].get_status()
    
        def init(self):
            for line in self.lines:
                line.set_data([], [])
            return self.patches
    
        def animate(self, i):
            self.y_data[0] = [1] * 512
            self.y_data[1] = [2] * 512
            self.lines[0].set_data(self.x_data, self.y_data[0])
            self.lines[1].set_data(self.x_data, self.y_data[1])
    
            return self.patches
    
        def start(self):
            self.ani = animation.FuncAnimation(self.fig, self.animate, init_func=self.init, frames=600, repeat=True, interval=20, blit=True)
            plt.show()
    
    
    graph = CollidingWaves(sliderData=sliderDataList, checkboxData=checkboxDataList)
    graph.start()