1

I am trying to animate multiple patches as efficiently as possible when reading data from a list?

The code below displays an animation of the scatter plot but not the patches. Each point in scatter plot contains various sizes of circles. This example would require 6 different circles to be animated at 2 subjects each time point. But what if there were 20 subjects that each had 3 circles around them.

What is the most efficient way to animate all 60 circles for each frame?

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import matplotlib as mpl

x_data = np.random.randint(80, size=(400, 4))
y_data = np.random.randint(80, size=(400, 4))

fig, ax = plt.subplots(figsize = (8,6))
ax.set_xlim(0,80)
ax.set_ylim(0,80)

scatter = ax.scatter(x_data[0], y_data[0], zorder = 5) #Scatter plot

Player_1 = x_data[0][0], y_data[0][0]
Player_2 = x_data[0][1], y_data[0][1]

Player_1_IR = mpl.patches.Circle(Player_1, radius = 2, color = 'black', lw = 1, alpha = 0.8, zorder = 4)
Player_1_MR = mpl.patches.Circle(Player_1, radius = 4, color = 'gray', lw = 1, alpha = 0.8, zorder = 3)
Player_1_OR = mpl.patches.Circle(Player_1, radius = 6, color = 'lightgrey', lw = 1, alpha = 0.8, zorder = 2)

Player_2_IR = mpl.patches.Circle(Player_2, radius = 2, color = 'black', lw = 1, alpha = 0.8, zorder = 4)
Player_2_MR = mpl.patches.Circle(Player_2, radius = 4, color = 'gray', lw = 1, alpha = 0.8, zorder = 3)
Player_2_OR = mpl.patches.Circle(Player_2, radius = 6, color = 'lightgrey', lw = 1, alpha = 0.8, zorder = 2)

ax.add_patch(Player_1_IR)
ax.add_patch(Player_1_MR)
ax.add_patch(Player_1_OR)

ax.add_patch(Player_2_IR)
ax.add_patch(Player_2_MR)
ax.add_patch(Player_2_OR)

def animate(i) : 
    scatter.set_offsets(np.c_[x_data[i,:], y_data[i,:]])

ani = animation.FuncAnimation(fig, animate, frames=len(x_data), 
                              interval = 700, blit = False)

plt.show()
Chopin
  • 96
  • 1
  • 10
  • 35
  • I'm seeing the word `player` a lot in your code. If you are trying to create a game, I don't think that matplotlib is the right platform -- have you looked at [pygame](https://www.pygame.org/docs/)? – Thomas Kühn Feb 13 '18 at 05:50
  • If this is really just about updating a lot of patches, I would collect them in a list and then update by iterating over that list. For instance, if you have your list of `Circle` patches, say `circle_list` and an according list of coordinates to update from, say `coord_list`, just do `for circ,coord in zip(circle_list,coord_list): circ.center=coord`. See also [this](https://stackoverflow.com/a/16528737/2454357). – Thomas Kühn Feb 13 '18 at 05:53
  • I should change to subject. But no I'm not. The XY Coordinates I'm using is from player tracking units measured in a sporting game. I'm just using those coordinates to visualise their movement. So when I say `Player` I mean a subjects XY coordinate – Chopin Feb 13 '18 at 05:54
  • I see, that clears it up then :) – Thomas Kühn Feb 13 '18 at 05:55
  • Thanks for all your help with this. I'll put @ThomasKühn in the acknowledgements. Probably co-author! – Chopin Feb 13 '18 at 05:57
  • Yeh, I'm not following how to update the list of coordinates – Chopin Feb 13 '18 at 06:09
  • Thanks @ThomasKühn – Chopin Feb 13 '18 at 08:56

1 Answers1

2

You can store all patches that you want to update in a list through which you then iterate through every iteration step. Note that the size of the Circle patches is in data units/coordinates while the scatter plot points are in points (one point = 1/72 inch), which means that the relative size between scatter points and circles depends on the figure size and axes limits and will change when you re-scale the figure.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import matplotlib as mpl

x_data = np.random.randint(80, size=(400, 20))
y_data = np.random.randint(80, size=(400, 20))

fig, ax = plt.subplots(figsize = (8,6))
ax.set_xlim(0,80)
ax.set_ylim(0,80)

scatter = ax.scatter(x_data[0], y_data[0], zorder = 5) #Scatter plot

##creating list of patches
players = []
for n in range(10):
    ##as there are always 3 circles, append all three patches as a list at once
    players.append([
        mpl.patches.Circle((x_data[0,n],y_data[0,n]), radius = 2, color = 'black', lw = 1, alpha = 0.8, zorder = 4),
        mpl.patches.Circle((x_data[0,n],y_data[0,n]), radius = 4, color = 'gray', lw = 1, alpha = 0.8, zorder = 3),
        mpl.patches.Circle((x_data[0,n],y_data[0,n]), radius = 6, color = 'lightgrey', lw = 1, alpha = 0.8, zorder = 2)
    ])

##adding patches to axes
for player in players:
    for circle in player:
        ax.add_patch(circle)


def animate(i):
    scatter.set_offsets(np.c_[x_data[i,:], y_data[i,:]])
    ##updating players:
    for n,player in enumerate(players):
        for circle in player:
            circle.center = (x_data[i,n],y_data[i,n])

ani = animation.FuncAnimation(fig, animate, frames=len(x_data), 
                              interval = 700, blit = False)

plt.show()

Old Answer (slightly different visual effect, but could be tuned to look the same):

If you really just want circles around your scatter points, you can actually forget about the Circle patches and just overlay several scatter plots with different marker sizes.

In the example below I only mark part of the scatter points with circles by slicing the array of random numbers. Also remember that in scatter plots the marker size is given as points square, so if you want to increase the circle radius from, say, 5 to 6, the given marker size should change from 25 to 36.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import matplotlib as mpl

x_data = np.random.randint(80, size=(400, 20))
y_data = np.random.randint(80, size=(400, 20))

fig, ax = plt.subplots(figsize = (8,6))
ax.set_xlim(0,80)
ax.set_ylim(0,80)

scatter = ax.scatter(x_data[0], y_data[0], zorder = 5) #Scatter plot
scatter_IR = ax.scatter(
    x_data[0,:10], y_data[0,:10], zorder = 4,
    facecolor='black', edgecolor = 'black',
    alpha = 0.8, s = 100
)
scatter_MR = ax.scatter(
    x_data[0,:10], y_data[0,:10], zorder = 3,
    facecolor='grey', edgecolor = 'grey',
    alpha = 0.8, s = 225
)
scatter_OR = ax.scatter(
    x_data[0,:10], y_data[0,:10], zorder = 2,
    facecolor='lightgrey', edgecolor = 'lightgrey',
    alpha = 0.8, s = 400
)

def animate(i) : 
    scatter.set_offsets(np.c_[x_data[i,:], y_data[i,:]])
    scatter_IR.set_offsets(np.c_[x_data[i,:10], y_data[i,:10]])
    scatter_MR.set_offsets(np.c_[x_data[i,:10], y_data[i,:10]])
    scatter_OR.set_offsets(np.c_[x_data[i,:10], y_data[i,:10]])


ani = animation.FuncAnimation(fig, animate, frames=len(x_data), 
                              interval = 700, blit = False)

plt.show()
Thomas Kühn
  • 9,412
  • 3
  • 47
  • 63
  • Didn't think of that. Good Idea! – Chopin Feb 13 '18 at 08:57
  • Just a quick one @ThomasKuhn. My dataset is slightly different. The `x_data` is a `list` of x-coordinates, labelled `x_data[0]`. So if I want the first row it's `x_data [0][0]`. When using the above code I get an error stating `TypeError: list indices must be integers or slices, not tuple`. How would I change the how the data is read so it's not a tuple? – Chopin Feb 15 '18 at 00:53
  • @JPA0888 You have two choices, either you convert your `x_data` into an array (`x_data=np.array(x_data)`) or you change the indexing in each place from numpy style to python style (`x_data[0,start:end]` --> `x_data[0][start:end]`). Either way will fix the problem. I personally would go with option number one. – Thomas Kühn Feb 15 '18 at 06:29