Using this dataset of % change by state, I have merged it with a cartographic boundary map of US states from the Census department: https://www2.census.gov/geo/tiger/GENZ2018/shp/cb_2018_us_state_500k.zip
df.head()
Year 2017 2018 2019 2020 2021 2022 2023
State
Alabama 0.00 0.00 0.00 0.00 0.00 0.00 0.00
Arizona 0.24 0.00 0.03 -0.15 0.56 -0.36 0.21
Arkansas 0.35 -0.06 -0.03 0.03 -0.00 -0.13 -0.02
California 0.13 0.07 -0.03 0.04 0.21 -0.10 0.03
Colorado 0.81 -0.18 -0.01 -0.05 0.10 -0.03 -0.51
I would like to cycle through the columns (years) in a FuncAnimation
after the boundaries have been plotted, and I am not quite sure how to go about it. The lifecycle of a plot in official reference manual cites relevant examples, but all deal with built-in figures, and not shape files.
Here is a related answer that seems exactly like what I'm missing, but deals with only (x, y)
line graph: How to keep shifting the X axis and show the more recent data using matplotlib.animation in Python?
How do I extrapolate column outside of calling shape.plot()
?
code:
shape = gpd.read_file(shapefile)
years = dfc.columns # dfc = % change df
tspan = len(dfc.columns)
""" merge map with dataframe on state name column """
shape = pd.merge(
left=shape,
right=dfc,
left_on='NAME',
right_on='State',
how='right'
)
""" init pyplot 'OO method' """
fig, ax = plt.subplots(figsize=(10, 5))
""" draw shape boundary """
ax = shape.boundary.plot(
ax=ax,
edgecolor='black',
linewidth=0.3,
)
""" plot shape """
ax = shape.plot(
ax=ax,
column=year, # what I need access to
legend=True, cmap='RdBu_r',
legend_kwds={'shrink': 0.3, 'orientation': 'horizontal', 'format': '%.0f'})
""" cycle through columns -- not operable yet """
def animate(year):
ax.clear()
ax.shape.column(year)
animation = FuncAnimation(states, animate, frames=(dfc.columns[0], dfc.columns[tspan] + 1, 1), repeat=True, interval=1000)
I really haven't found anything online dealing with these cartographic boundary maps specifically
I have tried the most obvious things I could think of:
Putting the entire shape.plot()
method into animate()
I tried a for
loop cycling the years, which resulted in 7 distinct maps. Each iteration lost the attributes I set in shape.boundary.plot()
Edit:
Since I've converted the original procedural example into the OO format, I am starting to have new questions about what might be done.
If ax = shape.plot(ax=ax)
, is there some kind of getter/setter, for previously defined attributes? e.g. ax.set_attr = column=year
(will scour manual immediately after I finish this)
Is there a way to define the map's boundary lines, shown here with shape.plot()
and shape.boundary.plot()
, using the fig
, instead of ax
(ax = shape.plot()
)?
Barring that, could we have shape.plot()
and shape.boundary.plot()
persist to the first subplot axs[0]
and have columns of data shown using subsequent overlapping subplots axs[n == year]
?
Any iterative process I've seen so far has lost the boundary attributes, so that's been a big sticking point for me.
In the following animation, only states in data
are plotted since how='right'
is used for pd.merge
.
Tested in python v3.12.3
, geopandas v0.14.4
, matplotlib v3.8.4
.
import geopandas as gpd
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation, PillowWriter
# Sample data
data = {
'State': ['Alabama', 'Arizona', 'Arkansas', 'California', 'Colorado'],
'2017': [0.00, 0.24, 0.35, 0.13, 0.81],
'2018': [0.00, 0.00, -0.06, 0.07, -0.18],
'2019': [0.00, 0.03, -0.03, -0.03, -0.01],
'2020': [0.00, -0.15, 0.03, 0.04, -0.05],
'2021': [0.00, 0.56, -0.00, 0.21, 0.10],
'2022': [0.00, -0.36, -0.13, -0.10, -0.03],
'2023': [0.00, 0.21, -0.02, 0.03, -0.51],
}
df = pd.DataFrame(data)
# Load the shapefile
shape = gpd.read_file('cb_2018_us_state_500k.shp')
# Merge the shape data with the dataframe
shape = pd.merge(
left=shape,
right=df,
left_on='NAME',
right_on='State',
how='right'
)
# Initialize the plot
fig, ax = plt.subplots(figsize=(10, 5))
# Set fixed axis limits
xlim = (shape.total_bounds[0], shape.total_bounds[2])
ylim = (shape.total_bounds[1], shape.total_bounds[3])
ax.set_xlim(xlim)
ax.set_ylim(ylim)
# Plot initial boundaries
boundary = shape.boundary.plot(ax=ax, edgecolor='black', linewidth=0.3)
# Initialize the colorbar variable with a fixed normalization
norm = plt.Normalize(vmin=df.iloc[:, 1:].min().min(), vmax=df.iloc[:, 1:].max().max())
sm = plt.cm.ScalarMappable(cmap='RdBu_r', norm=norm)
sm.set_array([]) # Only needed for adding the colorbar
colorbar = fig.colorbar(sm, ax=ax, orientation='horizontal', shrink=0.5, format='%.2f')
# Function to update the plot for each year
def animate(year):
ax.clear()
# Set the fixed axis limits
ax.set_xlim(xlim)
ax.set_ylim(ylim)
# Plot initial boundaries
boundary = shape.boundary.plot(ax=ax, edgecolor='black', linewidth=0.3)
# Plot the data for the current year
shape.plot(
ax=ax, column=year, legend=False, cmap='RdBu_r', norm=norm
)
# Add year annotation at the top
ax.annotate(f'Year: {year}', xy=(0.5, 1.05), xycoords='axes fraction', fontsize=12, ha='center')
# Create the animation
years = df.columns[1:] # Skip the 'State' column
animation = FuncAnimation(fig, animate, frames=years, repeat=False, interval=1000)
# Save the animation as a GIF
writer = PillowWriter(fps=1)
animation.save('us_states_animation.gif', writer=writer)
# Show the plot
plt.show()
Note: Segmentation of the colorbar is an artifact of the .gif format and is not present when running the animation.
Save the file as a .mp4, which doesn't display segmentation in the colorbar. Download FFmped from FFmpeg download page, extract the archive, and add the bin
folder path to the Path
variable in 'System Variables'
.
from matplotlib.animation import FuncAnimation, FFMpegWriter
import matplotlib as mpl
# Set the path to the ffmpeg executable
mpl.rcParams['animation.ffmpeg_path'] = r'C:\FFmpeg\bin\ffmpeg.exe' # Replace this with the correct path to your ffmpeg executable
...
# Save the animation as an MP4
writer = FFMpegWriter(fps=1, metadata=dict(artist='Me'), bitrate=1800)
animation.save('us_states_animation.mp4', writer=writer)
# Show the plot
plt.show()