Creating Animated Plots with Matplotlib

Matplotlib has functionality to created animations and can be used to create dynamic visualizations. In this post, I will explain the concepts and techniques for creating animated charts using Python and Matplotlib.

I find this technique very helpful in creating animations showing how certain algorithms work. This post also contains Python implementations of two common geometry simplification algorithms and they will used to create animations showing each step of the algorithm. Since both of these implementations use a recursive function, the technique shown in the post can be extended to visualize other recursive functions using matplotlib. You will learn how to create animated plots like below.

The complete notebook containing the code and explanation can be accessed from here.

Animation Basics

The matplotlib.animation module provides a FuncAnimation class to create animated plots. This allows you to make an animation by repeatedly calling a function and saving the output as a frame in the animation.

Before we dive into more complex example. it is helpful to understand the basics of matplotlib animation. Let’s define 3 positions and we will create an animation of a point moving between them.

points = [(0.1, 0.5), (0.5, 0.5), (0.9, 0.5)]

We need to define a function that takes the frame number and generates a plot from it. Here we define a function animation that takes the frame index and creates a plot from the point at the same index in the points list. So at frame 0, it will display the first point, frame 1 the second point and so on.

%matplotlib inline
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

fig, ax = plt.subplots(1, 1)
fig.set_size_inches(5,5)

def animate(i):
    ax.clear()
    # Get the point from the points list at index i
    point = points[i]
    # Plot that point using the x and y coordinates
    ax.plot(point[0], point[1], color='green', 
            label='original', marker='o')
    # Set the x and y axis to display a fixed range
    ax.set_xlim([0, 1])
    ax.set_ylim([0, 1])
ani = FuncAnimation(fig, animate, frames=len(points),
                    interval=500, repeat=False)
plt.close()

The animation is now contained in the ani object. We can call save() and save the result as an animated GIF. We need to specify a writer that supports the output format.

from matplotlib.animation import PillowWriter
# Save the animation as an animated GIF
ani.save("simple_animation.gif", dpi=300,
         writer=PillowWriter(fps=1))

Animating Plots

Now that you understand the basics, it’s time to apply this technique to animate plots. I will show 2 different approaches for animating matplotlib plots. I find it very helpful in visualizing iterative algorithms. We will take examples of two popular geometry simplification algorithms and visualize how they work.

  1. Visvalingam and Whyatt algorithm
  2. Douglas-Peucker algorithm

Note:

The implementations for both Visvalingam and Whyatt and Douglas-Peucker algorithms in the post are not necessarily the fastest or the most optimized. They are meant for educational purposes and therefore are written in a way that is easy to understand.

Let’s take a line segment defined by a list of (x,y) coordinates as below.

point_list = [(0,3), (1,0), (2,1), (3, 2), (5, 3),
              (7,4), (8,4), (10,3), (11,1), (12,1),
              (15,2), (17, 3), (18, 3), (20, 2)]

We will use the shapely library to create a LineString object and plot it.

import matplotlib.pyplot as plt
from shapely.geometry import LineString

original = LineString(point_list)
fig, ax = plt.subplots(1, 1)
fig.set_size_inches(20,5)
ax.plot(*original.xy, color='red', 
        label='input', marker='o')
ax.set_title('Original Line Segment', fontsize=20)
ax.legend()
plt.savefig('line.png', bbox_inches='tight', dpi=300)
plt.show()

Visualizing Visvalingam and Whyatt Algorithm

This algorithm simplifies a line segment by successively removing vertices till the required number of vertices remain. It calculates the areas of triangle formed by three consecutive points and then removes the vertex with the smallest area. Learn more.

Below is a python implementation of this algorithm. We use the shapely library to calculate polygon areas. This implementation uses a recursive approach to achieve the result.

import math
from shapely.geometry import Polygon

def visvalingam_whyatt_recursive(point_list, required_points):
    # Copy the original list since we will be modifying it
    points = point_list.copy()
    
    if len(points) == required_points:
        return points
    
    # Calculate traingle areas of each point
    areas = []
    for index, point in enumerate(points):
        if index == 0 or index == len(points)-1:
            areas.append(math.inf)
        else:
            p1 = points[index-1]
            p2 = point
            p3 = points[index+1]
            polygon = Polygon([p1, p2, p3])
            areas.append(polygon.area)
    min_area = min(areas)
    remove_index = areas.index(min_area)
    # Remove the vertex with smallest area
    points.pop(remove_index)
    if len(points) == required_points:
        return points
    else:
        # Call the function recursively with updated point list
        return visvalingam_whyatt_recursive(
            points, required_points)

Let’s test the function and simplify the line to retain only 50% of the original vertices. We can plot both the original and simplified segment to see the result.

required_points = int(len(point_list)/2)
simplified_list = visvalingam_whyatt_recursive(
    point_list, required_points)

fig, ax = plt.subplots(1, 1)
fig.set_size_inches(20,5)

original = LineString(point_list)
simplified = LineString(simplified_list)
ax.plot(*original.xy, color='red',
        label='original', marker='o')
ax.plot(*simplified.xy, color='blue',
        linestyle='dashed', label='simplified', marker='o')
ax.set_title('Visvalingam and Whyatt Simplification',
             fontsize=20)
ax.legend()
plt.savefig('visvalingam_whyatt.png',
            bbox_inches='tight', dpi=300)
plt.show()

Animating the Algorithm

This algorithm removes 1 point at a time. We can visualize how the algorithm works by plotting the results of simplifying the original segment with successively smaller number of points. The animate() function gets input as the frame number i and we use it to calculate how many points to retain in each iteration. The resulting animation shows how the algorithm chooses the vertex with the smallest area first and removes it – continuing to do the same till only the start and end points remain.

fig, ax = plt.subplots(1, 1)
fig.set_size_inches(20,5)
fig.tight_layout(rect=[0, 0.03, 1, 0.95])

def animate(i):
    ax.clear()
    simplified_list = visvalingam_whyatt_recursive(
        point_list, len(point_list)-i)
    original = LineString(point_list)
    simplified = LineString(simplified_list)
    ax.plot(*original.xy, color='red',
            label='original', marker='o')
    ax.plot(*simplified.xy, color='blue',
            linestyle='dashed', label='simplified', marker='o')
    ax.set_title(
        'Visvalingam and Whyatt Algorithm - \
        Eliminated {} Points'.format(i), fontsize=20) 
    ax.legend()

ani = FuncAnimation(fig, animate, frames=len(point_list)-1, 
                    interval=500, repeat=False)
plt.close()

from matplotlib.animation import PillowWriter
# Save the animation as an animated GIF
ani.save("visvalingam_whyatt.gif",
         dpi=300, writer=PillowWriter(fps=1))

Visualizing Douglas-Peucker Algorithm

The Douglas-Peucker algorithm successively removes vertices from a segment using the distance between the original segment and simplified segment. It removes points whose distance is less than the specified threshold e Learn more.

Below is a python implementation of this algorithm. We use the shapely library to calculate distance between a point and a line segment. This implementation uses a recursive approach to achieve the result. The implementation is adapted from https://github.com/fhirschmann/rdp

from shapely.geometry import Point, LineString

def douglas_peuker_recursive(point_list, e):
    dmax = 0
    index = -1
  
    for i in range(1, len(point_list)):
        point = Point(point_list[i])
        line = LineString([point_list[0], point_list[-1]])
        d = point.distance(line)
        if d > dmax:
            index = i
            dmax = d
    if dmax > e:
        r1 = douglas_peuker_recursive(point_list[:index+1], e)
        r2 = douglas_peuker_recursive(point_list[index:], e)
        return r1[:-1] + r2
    else:
        return [point_list[0], point_list[-1]]

We can test the function with e=1 and plot the results.

e = 1
simplified_list = douglas_peuker_recursive(point_list, e)

fig, ax = plt.subplots(1, 1)
fig.set_size_inches(20,5)
fig.tight_layout(rect=[0, 0.03, 1, 0.95])

original = LineString(point_list)
simplified = LineString(simplified_list)
ax.plot(*original.xy, color='red',
        label='original', marker='o')
ax.plot(*simplified.xy, color='blue', 
        linestyle='dashed', label='simplified', marker='o')
ax.set_title('Douglas-Peucker Simplification', fontsize=20)
ax.legend()
plt.savefig('douglas_peucker.png', 
            bbox_inches='tight', dpi=300)
plt.show()

Animating the Algorithm

The Douglas-Peucker algorithm is harder to visualize because each step is done recursively and you need to capture the result of it and plot it. Here I present another strategy to animate each step. We can capture the intermediate results in a separate list and use that list with the animate() function. To capture the output of each invocation of the function, we can use a decorator. Since our function is already defined, we can directly call the track function instead of the @track syntax for decorating the douglas_peuker_recursive() function.

Remember that the recursive function may be called multiple times till it before it meets the condition to actually remove a point. So we save the input and output both to our temporary list.

steps_list = []

# Decorator function to save the input/output from a function
def track(func):
    def inner(*args, **kwargs):
        # Save the input to the function
        steps_list.append(
            {'type': 'input', 'line': args[0]})
        output = func(*args, **kwargs)
        # Save the output from the function
        steps_list.append({
            'type': 'output', 'line': output
        })
        return output
    return inner

# Decorate the function
douglas_peuker_recursive = track(douglas_peuker_recursive)

# Run the function
simplified_list = douglas_peuker_recursive(point_list, e)

We have now captured each step of the algorithm in the steps_list. We can now write an animation function to display each step from it.

fig, ax = plt.subplots(1, 1)
fig.set_size_inches(20,5)
fig.tight_layout(rect=[0, 0.03, 1, 0.95])

def animate(frame):
    ax.clear()
    step = steps_list[frame]
    original = LineString(point_list)
    ax.plot(*original.xy, color='red',
            label='original', marker='o')
    if step['type'] == 'input':
        part = LineString(step['line'])
        ax.plot(*part.xy, color='yellow',
                linestyle='dashed', label='_part', marker='x')
    elif step['type'] ==  'output':
        simplified = LineString(step['line'])
        ax.plot(*simplified.xy, color='blue',
                label='simplified', marker='o')
        
    ax.set_title('Douglas-Peucker - Step {}'.format(frame),
                 fontsize=20) 
ani = FuncAnimation(fig, animate, frames=len(steps_list),
                    interval=500, repeat=False)
plt.close()

from matplotlib.animation import PillowWriter
# Save the animation as an animated GIF
ani.save("douglas_peucker.gif",
         dpi=300, writer=PillowWriter(fps=1))

The resulting animation is shown below. You can now see each step of the algorithm as it splits the line segments and removes points outside of the specified threshold.

Leave a Reply