jonboy
jonboy

Reputation: 372

Animate lines within circle - matplotlib

I'm trying to animate lines using matplotlib. I can animate a circle and some scatter points but I'm trying to add lines using a changing angle. Using below, I determine the orientation of X2, Y2 at each point in time to determine the direction. I then want to plot to lines that display this direction. They essentially split the circle up into 4 equal segments. I've inserted each frame individually below. I've entered a compass as a reference.

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import animation

fig, ax = plt.subplots(figsize = (8,8))
ax.set_xlim(-20,20)
ax.set_ylim(-20,20)
ax.grid(False)

df = pd.DataFrame({   
    'Time' : [1,1,1,1,2,2,2,2,3,3,3,3],             
    'id' : ['A','B','C','D','A','B','C','D','A','B','C','D'],                 
    'X1' : [1,8,0,-5,1,1,-6,0,1,8,0,-5],
    'Y1' : [-5,2,-5,2,5,-5,-2,2,-5,2,-5,2],
    'X2' : [0,0,0,0,-1,-1,-1,-1,0,0,0,0],
    'Y2' : [0,0,0,0,1,1,1,1,1,1,1,1],    
    'Rotation' : [0,0,0,0,-0.78,-0.78,-0.78,-0.78,1.57,1.57,1.57,1.57],
    'Angle' : [0,0,0,0,-45,-45,-45,-45,90,90,90,90],                    
    })

points_x = np.array(df.groupby(['Time'])['X1'].apply(list))
points_y = np.array(df.groupby(['Time'])['Y1'].apply(list))

# scatter points
points = ax.scatter(points_x[2], points_y[2], c = 'blue', marker = '*')

moving_x = np.array(df.groupby(['Time'])['X2'].apply(list))
moving_y = np.array(df.groupby(['Time'])['Y2'].apply(list))

# scatter moving
moving_point = ax.scatter(moving_x[2], moving_y[2], c = 'black', marker = 'x')

# Array of immediate congestion radius coordinates
radius = df.drop_duplicates(subset = ['Time','X2', 'Y2'])[['X2', 'Y2']].values

# Plot immediate congestion radius
circle = plt.Circle(radius[2], 10, color = 'black', fill = False)

# Add radius to plot
ax.add_patch(circle)

t = df['Angle'][0]
line1, = ax.plot([0, 0],[0,t], color = 'b', linewidth = 1)

def animate(i) :

    circle.center = (radius[i,0], radius[i,1])

    line1.set_data([i, i],[0,t])

    points.set_offsets(np.c_[points_x[0+i], points_y[0+i]])
    moving_point.set_offsets(np.c_[moving_x[0+i], moving_y[0+i]])


ani = animation.FuncAnimation(fig, animate,  np.arange(0,2), blit = False)

If I split the frames up individually, I'm hoping they display the following:

Frame 1: enter image description here

Frame 2:

enter image description here

Frame 3:

enter image description here

Edit 2:

enter image description here

Upvotes: 2

Views: 682

Answers (2)

meTchaikovsky
meTchaikovsky

Reputation: 7676

In order to handle datasets beyond the toy dataset in your post, I put everything for creating an animation into the function animate. Since I'm not familiar with the dataset you are working with (for example I don't know what Rotation does), the script below is the best I can do.

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import animation

df = pd.DataFrame({   
    'Time' : [1,1,1,1,2,2,2,2,3,3,3,3],             
    'id' : ['A','B','C','D','A','B','C','D','A','B','C','D'],                 
    'X1' : [1,8,0,-5,1,1,-6,0,1,8,0,-5],
    'Y1' : [-5,2,-5,2,5,-5,-2,2,-5,2,-5,2],
    'X2' : [0,0,0,0,-1,-1,-1,-1,0,0,0,0],
    'Y2' : [0,0,0,0,1,1,1,1,1,1,1,1],    
    'Rotation' : [0,0,0,0,-0.78,-0.78,-0.78,-0.78,1.57,1.57,1.57,1.57],
    'Angle' : [0,0,0,0,-45,-45,-45,-45,90,90,90,90],                    
    })

# the scattered points in the compass
points_x = np.array(df.groupby(['Time'])['X1'].apply(list))
points_y = np.array(df.groupby(['Time'])['Y1'].apply(list))

# the center of the compass
moving_x = np.array(df.groupby(['Time'])['X2'].apply(list))
moving_y = np.array(df.groupby(['Time'])['Y2'].apply(list))

radius = df.drop_duplicates(subset = ['Time','X2', 'Y2'])[['X2', 'Y2']].values
angles = df['Angle'].unique()

rot_mat = lambda theta:np.array([
    [np.cos(np.deg2rad(theta)),-np.sin(np.deg2rad(theta))],
    [np.sin(np.deg2rad(theta)),np.cos(np.deg2rad(theta))]
])

fig, ax = plt.subplots(figsize = (8,8))

def animate(i) :
    
    ax.clear()
    ax.set_xlim(-20,20)
    ax.set_ylim(-20,20)
        
    points = ax.scatter(points_x[i]+radius[i][0], points_y[i]+radius[i][1], c = 'blue', marker = '*')
    moving_point = ax.scatter(moving_x[i],moving_y[i], c = 'black', marker = 'x')
    
    circle = plt.Circle(radius[i], 10, color = 'black', fill = False)
    ax.add_patch(circle)

    ends_one = np.array([[-7.07106781,7.07106781],[7.07106781,-7.07106781]])
    ends_two = np.array([[-7.07106781,-7.07106781],[7.07106781,7.07106781]])
    ends_one = ends_one @ rot_mat(angles[i]) + radius[i]
    ends_two = ends_two @ rot_mat(angles[i]) + radius[i]
    line1, = ax.plot([], [], ls='--', color='black',lw=1, zorder=10,animated=True)
    line2, = ax.plot([], [], ls='--', color='black',lw=1, zorder=10,animated=True)
    line1.set_data([ends_one[0][0],ends_one[1][0]],[ends_one[0][1],ends_one[1][1]])
    line2.set_data([ends_two[0][0],ends_two[1][0]],[ends_two[0][1],ends_two[1][1]])
    
    tags = ['N','E','S','W']
    tag_pos = np.array([[0,8.5],[8.5,0],[0,-8.5],[-8.5,0]])
    tag_pos = tag_pos @ rot_mat(angles[i])
    for tag,pos in zip(tags,tag_pos):
        ax.annotate(tag,xy=pos+radius[i], xycoords='data',
                    fontsize=10,horizontalalignment='right', verticalalignment='bottom')

ani = animation.FuncAnimation(fig, animate, np.arange(0,3), blit = False)
ani.save('test.gif', writer='pillow', fps=3)

As you can see, coordinates can be passed iteratively into the function animate to generate frames of a compass. After you run this script, you will get a gif file test.gif, which can be previewed

test.gif


Remove text of past frames

There are two ways you can do this

1. If you are using a function to create animation

put ax.clear() at the beginning of the function, for example

import matplotlib.pyplot as plt
from matplotlib import animation

rot_mat = lambda theta:np.array([
    [np.cos(np.deg2rad(theta)),-np.sin(np.deg2rad(theta))],
    [np.sin(np.deg2rad(theta)),np.cos(np.deg2rad(theta))]
])

fig, ax = plt.subplots(figsize = (8,8))

centers = np.linspace(1,36,36).reshape(18,2)*0.2
angles = np.linspace(45,90,18)

def animate(i) :
    
    ax.clear()
    ax.set_xlim(-20,20)
    ax.set_ylim(-20,20)
            
    circle = plt.Circle(centers[i], 10, color = 'black', fill = True)
    circle.set_facecolor('violet')
    ax.add_patch(circle)

    tags = ['N','E','S','W']
    tag_pos = np.array([[0,8.5],[8.5,0],[0,-8.5],[-8.5,0]])
    tag_pos = tag_pos @ rot_mat(angles[i])
    for tag,pos in zip(tags,tag_pos):
        ax.annotate(tag,xy=pos+centers[i], xycoords='data',
                    fontsize=10,horizontalalignment='right', verticalalignment='bottom')

ani = animation.FuncAnimation(fig, animate, np.arange(0,18), blit = False)
ani.save('test.gif', writer='pillow', fps=18)

This will create a gif like this

use a function to create

2. If you are not using a function to create animation

You need to manually remove the text, for example

import matplotlib.pyplot as plt
from matplotlib import animation

rot_mat = lambda theta:np.array([
    [np.cos(np.deg2rad(theta)),-np.sin(np.deg2rad(theta))],
    [np.sin(np.deg2rad(theta)),np.cos(np.deg2rad(theta))]
])

centers = np.linspace(1,36,36).reshape(18,2)*0.2
angles = np.linspace(45,90,18)

fig, ax = plt.subplots(figsize = (8,8))
ax.set_xlim(-20,20)
ax.set_ylim(-20,20)
    
circle = plt.Circle(centers[0], 10, color = 'black', fill = True)
circle.set_facecolor('violet')
ax.add_patch(circle)

annotates = []
tags = ['N','E','S','W']
tag_pos = np.array([[0,8.5],[8.5,0],[0,-8.5],[-8.5,0]])
tag_pos = tag_pos @ rot_mat(angles[0])
for tag,pos in zip(tags,tag_pos):
    ann = ax.annotate(tag,xy=pos+centers[0], xycoords='data',
                      fontsize=10,horizontalalignment='right', verticalalignment='bottom')
    annotates.append(ann)

for i in range(18):
    
    circle.center = centers[i]
    # remove the previous text
    for item in annotates: item.remove()
    annotates = []
    
    tag_pos = tag_pos @ rot_mat(angles[i])
    for tag,pos in zip(tags,tag_pos):
        ann = ax.annotate(tag,xy=pos+centers[i], xycoords='data',
                          fontsize=10,horizontalalignment='right', verticalalignment='bottom')
        annotates.append(ann)

ani = animation.FuncAnimation(fig, animate, np.arange(0,18), blit = False)
ani.save('test.gif', writer='pillow', fps=18)

as you can see, with this line of code for item in annotates: item.remove(), the text of a previous frame are removed. Therefore, by running this script, you will also get

the new one

Upvotes: 1

Sameeresque
Sameeresque

Reputation: 2602

A new point (x′,y′) is obtained by rotating an existing point (x,y), θ radians around the point (cx, cy) by performing translation, rotation, and a translation operation using the following formula:

x′ = (  (x - cx) * cos(θ) + (y - cy) * sin(θ) ) + cx
y′ = ( -(x - cx) * sin(θ) + (y - cy) * cos(θ) ) + cy

linescircle

Programmatically shown here:

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import math
from matplotlib import animation

fig, ax = plt.subplots(figsize = (8,8))
ax.set_xlim(-20,20)
ax.set_ylim(-20,20)
ax.grid(False)

df = pd.DataFrame({   
    'Time' : [1,1,1,1,2,2,2,2,3,3,3,3],             
    'id' : ['A','B','C','D','A','B','C','D','A','B','C','D'],                 
    'X1' : [1,8,0,-5,1,1,-6,0,1,8,0,-5],
    'Y1' : [-5,2,-5,2,5,-5,-2,2,-5,2,-5,2],
    'X2' : [0,0,0,0,-1,-1,-1,-1,0,0,0,0],
    'Y2' : [0,0,0,0,1,1,1,1,1,1,1,1],    
    'Rotation' : [0,0,0,0,-0.78,-0.78,-0.78,-0.78,1.57,1.57,1.57,1.57],
    'Angle' : [0,0,0,0,-45,-45,-45,-45,90,90,90,90],                    
    })

points_x = np.array(df.groupby(['Time'])['X1'].apply(list))
points_y = np.array(df.groupby(['Time'])['Y1'].apply(list))

# scatter points
points = ax.scatter(points_x[2], points_y[2], c = 'blue', marker = '*')

moving_x = np.array(df.groupby(['Time'])['X2'].apply(list))
moving_y = np.array(df.groupby(['Time'])['Y2'].apply(list))

# scatter moving
moving_point = ax.scatter(moving_x[2], moving_y[2], c = 'black', marker = 'x')

# Array of immediate congestion radius coordinates
radius = df.drop_duplicates(subset = ['Time','X2', 'Y2'])[['X2', 'Y2']].values

# Plot immediate congestion radius
circle = plt.Circle(radius[2], 10, color = 'black', fill = False)

# Add radius to plot
ax.add_patch(circle)

t = df['Angle'][0]
angles=np.array(df.groupby(['Time'])['Angle'].apply(list))
line1, = ax.plot([],[], color = 'k', linewidth = 1)
line2, = ax.plot([],[], color = 'k', linewidth = 1)

def animate(i) :

    circle.center = (radius[i,0], radius[i,1])

    #set the initial coordinates for line 1
    xs1L1=-10.0/2**0.5
    ys1L1=10.0/2**0.5
    xs2L1=10.0/2**0.5
    ys2L1=-10.0/2**0.5

    #set the initial coordinates for line 2 which is perpendicular to line 1
    xs1L2=-xs1L1
    ys1L2=ys1L1
    xs2L2=-xs1L2
    ys2L2=ys2L1

    # the center of circle 
    cx=radius[i,0]
    cy=radius[i,1]

    # Convert angle from degrees to radians
    theta=math.radians(angles[i][0])

    # rotating line 1 and allowing for translation of line center
    x1L1=(  (xs1L1+radius[i,0] - cx) * math.cos(theta) + (ys1L1+radius[i,1] - cy) * math.sin(theta) ) + cx
    x2L1=(  (xs2L1+radius[i,0] - cx) * math.cos(theta) + (ys2L1+radius[i,1]- cy) * math.sin(theta) ) + cx
    y1L1=( -(xs1L1+radius[i,0] - cx) * math.sin(theta) + (ys1L1+radius[i,1] - cy) * math.cos(theta) ) + cy
    y2L1=( -(xs2L1+radius[i,0] - cx) * math.sin(theta) + (ys2L1+radius[i,1] - cy) * math.cos(theta) ) + cy
    
    line1.set_data([x1L1,x2L1],[y1L1,y2L1])

    # rotating line 2 and allowing for translation of line center
    x1L2=(  (xs1L2+radius[i,0] - cx) * math.cos(theta) + (ys1L2+radius[i,1] - cy) * math.sin(theta) ) + cx
    x2L2=(  (xs2L2+radius[i,0] - cx) * math.cos(theta) + (ys2L2+radius[i,1]- cy) * math.sin(theta) ) + cx
    y1L2=( -(xs1L2+radius[i,0] - cx) * math.sin(theta) + (ys1L2+radius[i,1] - cy) * math.cos(theta) ) + cy
    y2L2=( -(xs2L2+radius[i,0] - cx) * math.sin(theta) + (ys2L2+radius[i,1] - cy) * math.cos(theta) ) + cy
    
    line2.set_data([x1L2,x2L2],[y1L2,y2L2])

    
    points.set_offsets(np.c_[points_x[0+i], points_y[0+i]])
    moving_point.set_offsets(np.c_[moving_x[0+i], moving_y[0+i]])

    return circle, line1, line2, points, moving_point

ani = animation.FuncAnimation(fig, animate,  np.arange(0,3), blit = True)
plt.show()

Upvotes: 1

Related Questions