Question: 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.

Answer: 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()