Updated On : Nov-04,2022 Time Investment : ~30 mins

Simple Guide to Create Animation using Bokeh

Nowadays, It's quite common to use animation to depict changes in some measure over time on social media platforms and various dashboards.

You might have seen an animation of bar charts on Twitter or LinkedIn showing things like world GDP over time, an approval rating of presidents over time, COVID deaths / cases over time, etc. The stock market has a moving candlestick chart for every second price change.

Python is the most preferred library to analyze data and it has a bunch of libraries that let us create interactive visualizations as well as animations.

Bokeh is one such library that has a rich set of features and lets us create aesthetically pleasing interactive charts.

What Can You Learn From This Article?

As a part of this tutorial, we have explained how to create animation using Python data visualization library bokeh. Tutorial explains simple animation like moving candlestick chart, line chart, bar chart, etc. Tutorial uses widgets available from bokeh to control animation. We'll be running animation as an independent server.

If you are new to Bokeh, we recommend you go through the link below, which covers basic charts and will get you started with library fast.

One more tutorial that we recommend can be beneficial for this tutorial is our simple guide on using bokeh widgets. Please feel free to explore it in your free time.

Below, we have listed important sections of tutorial to give an overview of the material covered.

Important Sections Of Tutorial

  1. Bar Chart Animation
  2. Candlestick Chart Animation
  3. Line Chart Animation
  4. Bubble Chart Animation

Below, we have imported Python library bokeh and printed version of it that we have used in our tutorial.

import bokeh

print("Bokeh Version : {}".format(bokeh.__version__))
Bokeh Version : 2.4.3

Example 1: Bar Chart Animation

As a part of our first example, we have explained how we can create bar chart animation using Bokeh. We'll be using gapminder dataset available from bokeh sample data for our purpose. The dataset has population details of countries of world from 1964 to 2013.

We have created a simple animation that shows top 10 countries by population as a bar chart. The bar height changes as year changes.

Below, we have imported dataset and displayed few rows to give an overview of the content.

from bokeh.sampledata.gapminder import population, fertility, life_expectancy

population.head()
1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 ... 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013
Country
Afghanistan 10474903.0 10697983.0 10927724.0 11163656.0 11411022.0 11676990.0 11964906.0 12273101.0 12593688.0 12915499.0 ... 26693486.0 27614718.0 28420974.0 29145841.0 29839994.0 30577756.0 31411743.0 32358260.0 33397058.0 34499915.0
Albania 1817098.0 1869942.0 1922993.0 1976140.0 2029314.0 2082474.0 2135599.0 2188650.0 2241623.0 2294578.0 ... 3124861.0 3141800.0 3156607.0 3169665.0 3181397.0 3192723.0 3204284.0 3215988.0 3227373.0 3238316.0
Algeria 11654905.0 11923002.0 12229853.0 12572629.0 12945462.0 13338918.0 13746185.0 14165889.0 14600659.0 15052371.0 ... 32396048.0 32888449.0 33391954.0 33906605.0 34428028.0 34950168.0 35468208.0 35980193.0 36485828.0 36983924.0
American Samoa 22672.0 23480.0 24283.0 25087.0 25869.0 26608.0 27288.0 27907.0 28470.0 28983.0 ... 61871.0 62962.0 64045.0 65130.0 66217.0 67312.0 68420.0 69543.0 70680.0 71834.0
Andorra 17438.0 18529.0 19640.0 20772.0 21931.0 23127.0 24364.0 25656.0 26997.0 28357.0 ... 75292.0 77888.0 79874.0 81390.0 82577.0 83677.0 84864.0 86165.0 87518.0 88909.0

5 rows × 50 columns

Initially, we have created a bar chart showing the top 10 countries by population for year 1964. We have created a horizontal bar chart for our purpose. We have filtered original dataframe to create a dataframe of top 10 countries by population which we have used to create a bar chart.

After creating chart, we have created two widgets that we'll use for our purpose.

  1. Button
  2. Slider

The button widget will be used to play and pause animation.

The slider widget can be used to explore population during any particular year.

After defining widgets, we have created callbacks that will be called each time value of widget changes.

The callback linked to button widget simply adds a callback to current document using add_periodic_callback(). This method calls callback named update_chart() every 200 milliseconds. Based on current value of button, it adds (Play) and removes callback (Pause).

Once we click button, this callback will be executed every 200 milliseconds. These function simply updates current year which we have stored as a global variable and assigns it to slider value. As slider value changes, the callback linked to it'll be called which updates chart.

The callback linked to slider creates an intermediate dataframe of the top 10 countries by population based on current slider year and then assigns this dataframe to chart which updates chart.

After defining callbacks, we have registered them with widgets.

At last, we have created a GUI by calling add_root() method on currdoc(). We have organized widgets and chart in a column using column() function.

## bokeh_animation_ex1.py

from bokeh.sampledata.gapminder import population, fertility, life_expectancy
from bokeh.models import Button, Slider
from bokeh.plotting import figure, column, row
from bokeh.models.ranges import FactorRange
from bokeh.io import curdoc

import time

## Prepare Data
def convert_population(population):
    if population > 1e9:
        pop = "{:.2f} Bn".format(population / 1e9)
    else:
        pop = "{:.2f} Mn".format(population / 1e6)
    return pop

top_10 = population[["1964"]].dropna().sort_values(by=["1964"], ascending=False).reset_index()
top_10 = top_10.rename(columns={"1964": "Population"}).head(10).reset_index()
top_10["index"] = top_10["index"] + 1
top_10["index"] = top_10["index"].values[::-1]
top_10 = top_10.sort_values(by="Population")
top_10["Population_Text"] = top_10["Population"].apply(convert_population)


## Create Chart
fig = figure(width=700, height=500, title="Top 10 Countries by Population (1964-2013)",
             x_range=(0, 1.6e9))

bars = fig.hbar(y="index", right="Population", color="dodgerblue",
                height=0.85, source=top_10)
text = fig.text(x="Population", y="index", text="Population_Text", x_offset=5,
                text_baseline="middle", source=top_10)

x_range = list(map(int, [0, 200e6, 400e6, 600e6, 800e6, 1e9, 1.2e9, 1.4e9, 1.6e9]))
x_range_ticks = ["1 Mn", "200 Mn", "400 Mn", "600 Mn", "800 Mn", "1.00 Bn", "1.20 Bn", "1.40 Bn", "1.6 Bn"]
#fig.xaxis.ticker = x_range
fig.xaxis.major_label_overrides = dict(zip(x_range, x_range_ticks))

fig.yaxis.ticker = list(range(1,11))
fig.yaxis.major_label_overrides = dict(zip(list(range(1,11)), top_10.Country.values))

## Define Widgets
slider = Slider(start=1964, end=2013, value=1964, step=1, title="Year:")
btn = Button(label="Play")

## Define Callbacks
curr_year = 1964
def update_chart():
    global curr_year
    curr_year += 1
    slider.value = curr_year
    if curr_year == 2013:
        curr_year = 1964

callback = None
def execute_animation():
    global callback
    if btn.label == "Play":
        btn.label = "Pause"
        callback = curdoc().add_periodic_callback(update_chart, 100)
    else:
        btn.label = "Play"
        curdoc().remove_periodic_callback(callback)


def modify_chart(attr, old, new):
    idx = "{}".format(slider.value)
    top_10 = population[[idx]].dropna().sort_values(by=[idx], ascending=False).reset_index()
    top_10 = top_10.rename(columns={idx: "Population"}).head(10).reset_index()
    top_10["index"] = top_10["index"] + 1
    top_10["index"] = top_10["index"].values[::-1]
    top_10 = top_10.sort_values(by="Population")
    top_10["Population_Text"] = top_10["Population"].apply(convert_population)

    bars.data_source.data = top_10
    text.data_source.data = top_10

    fig.yaxis.major_label_overrides = dict(zip(list(range(1,10)), top_10.Country.values))


## Register Callbacks
btn.on_click(execute_animation)
slider.on_change("value", modify_chart)

## GUI
curdoc().add_root(column(btn, slider, fig))

We have saved code for our animation to a file named bokeh_animation_ex1.py. We can execute file using below command from command line. It'll start bokeh server at port 5006 and open animation in browser. We can access animation using URL (http://localhost:5006/bokeh_animation_ex1) as well.

bokeh serve --show bokeh_animation_ex1.py

Bokeh Animation

Example 2: Candlestick Chart Animation

As a part of our second example, we have explained how to create candlestick chart animation using Python data viz bokeh. We have used Microsoft stock data available from bokeh which has data from 2000-2013.

The example starts by creating candlestick chart for first 22 days. It then adds new candle every 200 milliseconds.

This time, we have used only button widget to start and pause animation. The callback registered with button simply adds / removes a callback function to /from current doc using add_periodic_callback() function.

The function registered using add_periodic_callback() simply adds one day's OHLC data to chart on each call.

The animation displays 3 months of data at a time in chart and then removes one entry from start and adds a new one at the end.

If you want to learn how to create candlestick charts using bokeh then please feel free to check below link.

from bokeh.sampledata.stocks import MSFT
from bokeh.models import Button, Slider, DatetimeTickFormatter
from bokeh.plotting import figure, column, row
from bokeh.models.ranges import FactorRange
from bokeh.io import curdoc

import time
import pandas as pd

## Prepare Data
msft_df = pd.DataFrame(MSFT)
msft_df["date"] = pd.to_datetime(msft_df["date"])

## Create Candlestick Charts
days = 22*3 ## 3 Months

inc = msft_df[:days].close > msft_df[:days].open
dec = msft_df[:days].open > msft_df[:days].close

w = 12*60*60*1000

fig = figure(x_axis_type="datetime", plot_width=900, plot_height=500,
             #y_range=(20,120),
             title = "Microsoft Candlestick Chart Animation (2000-2013)")

segments = fig.segment("date", "high", "date", "low", color="black", source=msft_df[:days])

green_patterns = fig.vbar("date", w, "open", "close", fill_color="lawngreen", line_width=0,
                          source=msft_df[:days][inc])

red_patterns = fig.vbar("date", w, "open", "close", fill_color="tomato", line_width=0,
                        source=msft_df[:days][dec])

fig.xaxis.axis_label="Date"
fig.yaxis.axis_label="Price ($)"

fig.xaxis.formatter = DatetimeTickFormatter(days="%m-%d-%Y")

## Define Widgets
btn = Button(label="Play")

## Define Callbacks
curr_cnt = days
def update_chart():
    global curr_cnt
    curr_cnt += 1
    if curr_cnt == len(msft_df):
        curr_cnt = days

    inc = msft_df[curr_cnt-days:curr_cnt].close > msft_df[curr_cnt-days:curr_cnt].open
    dec = msft_df[curr_cnt-days:curr_cnt].open > msft_df[curr_cnt-days:curr_cnt].close

    segments.data_source.data = msft_df[curr_cnt-days:curr_cnt]
    green_patterns.data_source.data = msft_df[curr_cnt-days:curr_cnt][inc]
    red_patterns.data_source.data = msft_df[curr_cnt-days:curr_cnt][dec]

callback = None
def execute_animation():
    global callback
    if btn.label == "Play":
        btn.label = "Pause"
        callback = curdoc().add_periodic_callback(update_chart, 200)
    else:
        btn.label = "Play"
        curdoc().remove_periodic_callback(callback)


## Register Callbacks
btn.on_click(execute_animation)

## GUI
curdoc().add_root(column(btn, fig))
bokeh serve --show bokeh_animation_ex2.py

Bokeh Animation

Example 3: Line Chart Animation

As a part of our third example, we have explained how to create line animation using Python data viz bokeh. We'll reuse Microsoft OHLC dataset available from bokeh for our purpose. The animation displays 4 lines for open, high, low, and close prices of Microsoft stock.

The example starts by creating a line chart for first 90 days. It then adds one new day's OHLC data and removes one old entry.

We have used button widget this time as well to start and stop animation.

from bokeh.sampledata.stocks import MSFT
from bokeh.models import Button, Slider, DatetimeTickFormatter, Legend
from bokeh.plotting import figure, column, row
from bokeh.models.ranges import FactorRange
from bokeh.io import curdoc

import time
from functools import partial
import pandas as pd

## Prepare Data
msft_df = pd.DataFrame(MSFT)
msft_df["date"] = pd.to_datetime(msft_df["date"])

## Create Chart
days = 90


fig = figure(x_axis_type="datetime", plot_width=900, plot_height=500,
             title = "Microsoft Candlestick Chart")

line1 = fig.line(x="date", y="open", color="dodgerblue", source=msft_df[:days])
line2 = fig.line(x="date", y="high", color="lime", source=msft_df[:days])
line3 = fig.line(x="date", y="low", color="tomato", source=msft_df[:days])
line4 = fig.line(x="date", y="close", color="orange", source=msft_df[:days])

fig.xaxis.axis_label="Date"
fig.yaxis.axis_label="Price ($)"

fig.xaxis.formatter = DatetimeTickFormatter(days="%m-%d-%Y")

legend = Legend(items=[
    ("Open",   [line1]),
    ("High",   [line2]),
    ("Low",   [line3]),
    ("Close",   [line4]),
], location=(0, 100))

fig.add_layout(legend, 'right')

## Define Widgets
btn = Button(label="Play")

## Define Callbacks
curr_cnt = days
def update_chart():
    global curr_cnt
    curr_cnt += 1
    if curr_cnt == len(msft_df):
        curr_cnt = days

    line1.data_source.data = msft_df[curr_cnt-days:curr_cnt]
    line2.data_source.data = msft_df[curr_cnt-days:curr_cnt]
    line3.data_source.data = msft_df[curr_cnt-days:curr_cnt]
    line4.data_source.data = msft_df[curr_cnt-days:curr_cnt]

callback = None
def execute_animation():
    global callback
    if btn.label == "Play":
        btn.label = "Pause"
        callback = curdoc().add_periodic_callback(update_chart, 200)
    else:
        btn.label = "Play"
        curdoc().remove_periodic_callback(callback)


## Register Callbacks
btn.on_click(execute_animation)

## GUI
curdoc().add_root(column(btn, fig))
bokeh serve --show bokeh_animation_ex3.py

Bokeh Animation

Example 4: Bubble / Scatter Chart Animation

As a part of our fourth example, we have explained how to create bubble chart animation using bokeh. We have reused our Gapminder population dataset for this example as well. The animation shows changes in population of 10 selected countries over a period starting from 1964 to 2013.

The example starts by creating a bubble chart showing population of 10 selected countries during year 1964. The x-axis is used for year and y-axis displays country names. The size of country bubble changes over time as population increases.

Apart from bubble animation, we have added few annotations to chart to look it more interesting.

We have added a label at top right which displays current year during animation. We have added three line annotations to chart showing 500 Mn, 1 Bn and 1.3 Bn marks so we can know countries crossing them.

If you are interested in learning various annotations available from bokeh then do check our small tutorial that explains them in detail.

## bokeh_animation_ex4.py

from bokeh.sampledata.gapminder import population
from bokeh.models import Button, Slider, DatetimeTickFormatter, Legend, Label, Span
from bokeh.plotting import figure, column, row
from bokeh.models.ranges import FactorRange
from bokeh.palettes import Category20_10
from bokeh.transform import factor_cmap
from bokeh.io import curdoc

import time
import numpy as np
import pandas as pd

def create_tick_label(pop):
    if pop >= 1e9:
        return "{:.2f} Bn".format(pop/1e9)
    else:
        return "{:.2f} Mn".format(pop/1e6)

## Prepare Data
countries = ["China", "United States", "India", "Germany", "United Kingdom", "Russia", "France",
             "Australia", "Brazil", "Pakistan"]
countries = sorted(countries)
population = population[population.index.isin(countries)]


## Create Candlestick Charts
fig = figure(plot_width=1200, plot_height=600,
             y_range= countries, x_range=(0, 1.5e9),
             title = "Population Over Time (1964-2013)")

initial_df = population[["1964"]].reset_index().rename(columns={"1964": "Population"})
initial_df["size"] = initial_df["Population"] / 5e6

points = fig.scatter(x="Population", y="Country", size="size", alpha=0.8,
                     color=factor_cmap("Country", Category20_10, countries),
                     source=initial_df)

label = Label(x=1.05e9, y=8.8, text="Year: 1964", text_font_size="30px", text_color="tomato")
fig.add_layout(label)

## Span Lines & Labels
## Half billion line
fig.add_layout(Span(location=0.5e9, dimension='height', line_color='green', line_dash='dashed', line_width=4, name="Half Billion"))
fig.add_layout(Label(x=0.51e9, y=9.7, text="Half Billion", text_font_size="12px", text_color="green", text_font_style="bold"))
## 1 billion line
fig.add_layout(Span(location=1e9, dimension='height', line_color='orange', line_dash='dashed', line_width=4, name="1 Billion"))
fig.add_layout(Label(x=1.01e9, y=9.7, text="1 Billion", text_font_size="12px", text_color="orange", text_font_style="bold"))
## 1.3 billion line
fig.add_layout(Span(location=1.3e9, dimension='height', line_color='tomato', line_dash='dashed', line_width=4, name="1.3 Billion"))
fig.add_layout(Label(x=1.31e9, y=9.7, text="1.3 Billion", text_font_size="12px", text_color="tomato", text_font_style="bold"))

fig.xaxis.axis_label="Population"
fig.yaxis.axis_label="Countries"

fig.xaxis.ticker = list(map(int, np.arange(0,1.6e9,0.2e9)))
fig.xaxis.major_label_overrides = dict(list(zip(map(int,np.arange(0,1.6e9,0.2e9)), [create_tick_label(pop) for pop in np.arange(0,1.6e9,0.2e9)])))

## Define Widgets
btn = Button(label="Play")

## Define Callbacks
curr_year = 1964
def update_chart():
    global curr_year
    curr_year += 1
    if curr_year == 2014:
        curr_year = 1964

    intermediate_df = population[[str(curr_year)]].reset_index().rename(columns={str(curr_year): "Population"})
    intermediate_df["size"] = intermediate_df["Population"] / 5e6
    points.data_source.data = intermediate_df

    label.text = "Year: {}".format(curr_year)

callback = None
def execute_animation():
    global callback
    if btn.label == "Play":
        btn.label = "Pause"
        callback = curdoc().add_periodic_callback(update_chart, 200)
    else:
        btn.label = "Play"
        curdoc().remove_periodic_callback(callback)


## Register Callbacks
btn.on_click(execute_animation)

## GUI
curdoc().add_root(column(btn, fig))
bokeh serve --show bokeh_animation_ex4.py

Bokeh Animation

This ends our small tutorial explaining how we can create animation using Bokeh.

References

Useful Bokeh Resources

Bokeh Charts

Bokeh Chart Styling, Theming, Annotating, and Layout Guide

Maps using Bokeh

Dashboards

Candlestick Charts using Bokeh

Python Data Visualization Libraries

Sunny Solanki  Sunny Solanki

Share Views Stuck Somewhere? Need Help with Coding? Have Doubts About the Topic/Code?

When going through coding examples, it's quite common to have doubts and errors.

If you have doubts about some code examples or are stuck somewhere when trying our code, send us an email at coderzcolumn07@gmail.com. We'll help you or point you in the direction where you can find a solution to your problem.

You can even send us a mail if you are trying something new and need guidance regarding coding. We'll try to respond as soon as possible.

Share Views Want to Share Your Views? Have Any Suggestions?

If you want to

  • provide some suggestions on topic
  • share your views
  • include some details in tutorial
  • suggest some new topics on which we should create tutorials/blogs
Please feel free to contact us at coderzcolumn07@gmail.com. We appreciate and value your feedbacks. You can also support us with a small contribution by clicking DONATE.