How to make a gif map using Python, Geopandas and Matplotlib

No need for Photoshop: make an animated chart using only Python and the command line.

Ben Dexter Cooley
Towards Data Science

--

Update: I added a normalise function to keep the colorbar legend range the same for each map. Code updated below and in Jupyter Notebook on Github. See notes on weird rendering further down.

As a language, Python is enormously flexible. Which makes it possible to make lots of different visualisations in, at times, only a few lines of code. But with all the different charting websites and software available now, why bother writing code? Can’t we just use a GUI to upload a csv file, adjust the range, hit export png and call it a day?

Yes. You certainly can. And sometimes, this is the best choice if you need a quick, one-time chart or map. But the real power of using Python comes into play when you need to make lots of maps — lots and lots of maps.

In this tutorial, I’ll build off of my previous write-up on how to make a choropleth map using Geopandas and Matplotlib. We’ll be using this as a starting point, so if you want to catch up check out the post here or view my Jupyter notebook on Github.

This time I’ll be going through how to create an animated gif choropleth that shows both geographic data (cause it’s a map) and change over time. Best of all, this tutorial will from start to finish using only Python and a few command line tools. No need to look for the right menu/dropdown/window in Photoshop.

As I mentioned in my previous tutorial, this may not be the best workflow for all use cases. But if speed, reproducibility and consistency are your priorities, I think this a good way to go.

Why gif map?

In the past few years, gif charts seem to have become hugely popular on social media. Publishers like the Financial Times and the Economist have spent more time streamlining and perfecting their data viz production for formats that receive more engagement on platforms like Twitter and Instagram. And perhaps more importantly, gif charts allow for a new layer of storytelling on top of what the static chart is showing.

This can be used a couple of different ways:

  • to walk the user through the most important points of the same chart via annotations or highlighting
  • to show two different charts by means of comparison
  • to show the same chart change over time (this is what I’ll be covering)

The list is by no means exhaustive, and the use cases are continuing to expand. In summary: moving charts are both cool and useful (when used correctly).

Let’s get mapping.

How to make a gif map

Without having to recap every single step of the previous tutorial, here’s what you should have to start:

  1. load in shapefile
  2. load csv of data to visualise
  3. join the two dataframes
  4. plot the map and start to style it.

Now, we will use the power of for() loops in Python to crank out the same map using multiple different columns. Because we want to show change over time, we will need to make sure our data contains multiple years as variables. For ease of labelling, let’s make sure that each column header of data is a year number.

In order to iterate through the columns, we will need a list of strings to call the name of each column. Let’s create a list variable containing the year of each column (formatted as a string). Let’s also set an output path so that all of our maps are saved to one folder.

# save all the maps in the charts folder
output_path = 'charts/maps'

# counter for the for loop
i = 0

# list of years (which are the column names at the moment)
list_of_years = ['200807','200907','201007','201107','201207','201307','201407','201507','201607']

Finally, before we create the maps we want to set a consistent global variable for the vmin and max values. This sets the value for the colour range. If you don’t set this beforehand, Matplotlib will change the range of the choropleth each time the for loop iterates, so it will be harder to see how values have increased or decreased over time.

# set the min and max range for the choropleth map
vmin, vmax = 200, 1200

Writing the for loop

Once you start using them, for() loops are fairly simple. The syntax of our for() loop will say this:

  • for each year in the list_of_years list, run the following code.
  • And when all years in our list have gone through the code, stop the loop.

Here’s what that looks like in the code using the basemap gist from the previous tutorial:

# start the for loop to create one map per year
for year in list_of_years:

# create map, UDPATE: added plt.Normalize to keep the legend range the same for all maps
fig = merged1.plot(column=year, cmap='Blues', figsize=(10,10), linewidth=0.8, edgecolor='0.8', vmin=vmin, vmax=vmax,
legend=True, norm=plt.Normalize(vmin=vmin, vmax=vmax))

# remove axis of chart
fig.axis('off')

# add a title
fig.set_title('Violent crimes in London', \
fontdict={'fontsize': '25',
'fontweight' : '3'})

# this will save the figure as a high-res png in the output path. you can also save as svg if you prefer.
filepath = os.path.join(output_path, only_year+'_violence.jpg')
chart = fig.get_figure()
chart.savefig(filepath, dpi=300)

If you run this code and open the output path folder you just set, you should see lots of maps, each with a slightly different shade to them representing the data from the different years. But this could be confusing to the viewer: how will they know the year increments once the map animates?

We could set a simple date range at the bottom (2007–2015), but for() loops give us a better solution. Since we already have the years of each column saved as strings in a variable, we can add a different annotation to each map (which corresponds to the year of the data).

According to our for() loop, the variable ‘year’ will be the column year for each time the for loop runs. Using this logic, we can insert ‘year’ as a variable for the fig.annotate() argument.

Now each time the for loop runs, a different year will be inserted onto the map as an annotation. Using the same logic, we can also set the filename to start with each year so it’s easy to find which map corresponds to each year. Adding this code inside your for() loop will add a year annotation.

# create an annotation for the year by grabbing the first 4 digits
only_year = year[:4]
# position the annotation to the bottom left
fig.annotate(only_year,
xy=(0.1, .225), xycoords='figure fraction',
horizontalalignment='left', verticalalignment='top',
fontsize=35)

Rerun the code, and your maps should be replaced with new maps, each with a year annotation in the bottom left corner. If you flip through the maps in from top to bottom, you can start to get a sense of what your gif will look like.

Make the gif

For the final step (gif-making), we will leave Python and pop open the Terminal (or whatever command line editor you use). Navigate to the directory where you saved all the files in the terminal.

Sadly I couldn’t find a way to get ImageMagick to batch convert my png maps into jpg format. And I couldn’t seem to get matplotlib to save my charts as .jpg by default (got an error described on this Stack Overflow thread). So I found a workaround using a Mac program called sips (tutorial and explainer here). But essentially, it’s a command line tool to do batch conversions of files. To non-Mac users, I’m sure there’s a similar solution out there as well!

Copy/paste this snippet into your terminal to convert all the pngs to jpgs.

for i in *.png; do sips -s format jpeg -s formatOptions 70 "${i}" --out "${i%png}jpg"; done

There are numerous ways to make gifs, but I’m using ImageMagick for a few reasons:

  • it’s relatively easy to install and setup
  • it allows you to set transition time, crop size, and file format all in one line of code
  • it’s super fast

The documentation for ImageMagick is pretty thorough. If you haven’t already installed it, check out their installation and docs page.

To check to see if you installed ImageMagick properly:

  1. In the Terminal, type:
convert -version

2. If ImageMagick is already installed, a message will be displayed with the version and copyright notices. Once you have ImageMagick installed on your system, navigate to the directory that has all the maps we just made. Now we need to run one line of code to create our gif. Here’s what it does in a nutshell:

  • convert: take all these files and change them
  • -delay 60: set the time that passes between each image before going to the next
  • -loop 0: set an infinite loop
  • <insert all filenames that will be converted>
  • my_map.gif

And here’s the code to use in Terminal:

convert -delay 60 -loop 0 2008_violence.jpg 2009_violence.jpg 2010_violence.jpg 2011_violence.jpg 2012_violence.jpg 2013_violence.jpg 2014_violence.jpg 2015_violence.jpg 2016_violence.jpg new_map_normal.gif

Now check out the new file you created “new_map_normal.gif” and you should see something like this:

The gif above still renders as changing color range here on Medium, but the code outputs the correct file and the legend is correct when you view the file on other sites ¯\_(ツ)_/¯. Download it and view in a different programme to see for yourself (and anyone who knows why this might be, let me know!).

That’s it! We have a gif map.

Now of course, this starts to raise a set of new questions: why do the colours get darker in recent years? Has violent crime risen significantly? Gif maps in this format have the limitation of being hard to read the actual figures behind each borough.

For that, more complex combo charts may be more useful rather than including a legend to the right (one to tackle for next time!). However, as a method for giving a teaser to the story behind your data, using a gif map can be a good entry point to deeper analysis. Plus, you can turn any kind of chart that has time series data into a gif chart — I just chose maps cause they’re cool.

As always you can find all the code needed to replicate this tutorial (Jupyter notebook, shapefiles and data) on Github. If you have any questions, find me on twitter.

Thanks for reading! I also publish a weekly newsletter about the intersection of data visualisation, data science and technology. Each edition contains articles to read, visualisations to explore, datasets to analyse and more tutorials to learn from. You can signup here.

--

--

Visualization Software Engineer @ Pattern (Broad Institute). Designer, developer, data artist. Portfolio: bendoesdataviz.com | Art: bdexter.com