This article assumes the reader already knows how to plot geographic data using the Bokeh library in Python. For an excellent and thorough explanation, please see this article written by Colin Patrick Reid.
Your data is processed and you’ve successfully adapted Colin’s code to create your own beautiful visualization in Bokeh, but you want to convey more information than is possible with a static image. You’re in the right place!
Initial Setup We will start with this already processed data that combines bike rental data with historic daily average Chicago temperatures and the code below:
Which generates the following static image:

Add AnimationTo create an animated graph, we need to add a periodic callback function that updates the data after a specified amount of time has passed. In order to do this, the code needs to refer to the JavaScript document that is automatically created from your Python script. Therefore, instead of using show(p) to display our code, we need to update to c_urdoc().addroot(column(p)). Note we need to make sure we import curdoc from bokeh.plotting and column from bokeh.layouts. This change requires that the script be ran from the command line, which will be explained further below.
Each row in this dataset represents the number of bikes rented from a given bike station, averaged over days by average temperature (rounded to the nearest 5°F). Each update is to refresh the data to the next 5°F bucket, from 30°F to 80°F. Data for the graph is stored in a dictionary, containing x and y coordinates and circle size. Each callback updates the dictionary for the new circle sizes representing the next temperature bucket. To make the updates, store the Bokeh circle object in a variable, then update _variable_name.datasource.data with the updated data dictionary.
Smoothing and PacingThe data is grouped into 5°F buckets to make sure there is a sufficient number of data points for each temperature and to make the general trend easier to interpret. If we transition directly from one temperature bucket to the next, the animation would be very choppy, making it difficult to gain insight from the graph. To smooth out the animation, each frame interpolates the number of bike rentals as well as the temperature (rounded to nearest degree) between the previous and next bucket.
I found that 20 frames per bucket resulted in smooth animation. This equates to 4 frames per degree (20 frames per 5°F = 4 frames per 1°F). Refreshing the data every 80 milliseconds results in each temperature shown for 0.32 seconds (0.08s 4 frames), for a total runtime of 16 seconds (0.32 50 temperatures).
Additional Code:
Create the GIFAdding the callback function and calling the graph via the curdoc method is sufficient to create an animated graph that will run on localhost in your browser, but if you want to create a file of your animation (perhaps to include in a PowerPoint presentation), then you will want to create a .gif file.
To create a gif, save each frame of the animation to a .png file via the _exportpng method from bokeh.io (see code above). Then use the imread function from the imageio library to create a gif from the png files:
Note I recommend finalizing your code for the Bokeh server (including smoothing and pacing) before saving any png files. It takes a lot longer to save all the png files than it does to generate the server, so error checking the server is a much faster process than error checking the resulting gif.
Bokeh Server vs GIF Runtime Observant readers may have noticed that I calculated the gif to be 16 seconds long, but the animation at the beginning of this article actually runs for 20 seconds. Why the discrepancy? The Bokeh server created from this code does run for 16 seconds, but I did not specify the framerate in the code that creates the gif from the png files. The default is 10 frames per second (fps), compared to 12.5 fps used in the Bokeh server code (1 frame/80 ms callback period), which explains the gif’s 20 second runtime (12.5 fps in gif / 10 fps in Bokeh server * 16 seconds).
Keep this in mind if the Bokeh server has a different pace than the resulting gif. You can specify your desired framerate in the mimsave function via the fps parameter, or alternatively set the periodic callback refresh rate to 100 ms (which is equivalent to the mimsave default of 10 fps).
Run your CodeThe static image can be ran directly from your IDE, but as mentioned earlier, the curdoc method needs to be ran from the command line, which enables Bokeh to create a new plot each time your browser connects to the server. To run the code, run the following code from the command prompt:
bokeh serve — show AnimatedGraph.py
Note I use the Anaconda distribution of Python, so I actually use the Anaconda Prompt to run this command. Some additional configuration may be necessary to execute this command from your computer.
ResultsWas the extra effort of animating the graph worth it? I think it was, because it allowed me to find insight from the data I may otherwise not have and illustrate it in a very easily digestible way.
You probably noticed that demand at the labeled lakeshore locations exploded once the temperature reached approximately 55°F. This makes perfect sense as people are much more likely to visit the beach when the weather is warm, but I am not sure if I would have realized this without the animated visualization. I may have thought these destinations were simply more popular overall, like the locations west of the south branch of the river (which Chicago readers will recognize as the commuter train arrivals from the suburbs) that have a linear relationship with temperature.
From this insight, I was able to show that demand at these lakeshore locations increases quadratically with temperature, while the other locations increase linearly:


This is helpful information, since it demonstrates that estimating demand for the beach locations should probably use a different model than the other locations, which could improve the restocking strategy.