Most data analysts and scientists using Python are familiar with plotting libraries such as matplotlib, Seaborn, and Plotly. These libraries are very useful for doing data exploration, as well as visualizing and generating graphics for reports. However, what if you want to generate all these charts and graphics and let your users view them on Web browsers? Also, it would be useful if the users can interact with your charts dynamically and drill down into the details they want to see. For this, you can use Bokeh.

In this article, I'll walk you through the basics of Bokeh: how to install it, how to create basic charts, how to deploy them on Web servers, and more. So let the fun begin!

What Is Bokeh?

Bokeh is a Python library for creating interactive visualizations for Web browsers. Using Bokeh, you can create dashboards - a visual display of all your key data. What's more, Bokeh powers your dashboards on Web browsers using JavaScript, all without you needing to write any JavaScript code.

Dashboards provide all your important information in a single page and are usually used for presenting information such as KPIs and sales results.

Installing Bokeh

For this article, I'll be using Anaconda for my Python installation. You can download Anaconda from https://www.anaconda.com/products/individual. Once Anaconda is installed, the next step is to install the Bokeh library.

To install the Bokeh library, simply use the pip command at the Anaconda Prompt/Terminal:

$ pip install bokeh

Creating Basic Glyphs

In Bokeh, a plot is a container that holds all the various objects (such as renderers, glyphs, or annotations) of a visualization.

Glyphs are the basic visual building blocks of Bokeh plots. The simplest way to get started is to create a simple chart using the various glyphs methods.

In Bokeh, glyphs are the geometrical shapes (lines, circles, rectangles, etc.) in a chart.

In Jupyter Notebook, type the following code in a new cell:

from bokeh.plotting import figure, output_file, show

import random

count = 10
x = range(count)
y = random.sample(range(0, 101), count)

p = figure()    # figure is a type of plot

# using various glyph methods to create scatter
# plots of different marker shapes
p.circle  (x, y, size=30, color='red', legend_label='circle')
p.line    (x, y, width=2, color='blue', legend_label='line')
p.triangle(x, y, size=10, color='gold', legend_label='triangle')

output_file('my_first_graph.html')    # name the output file

show(p)    #show the graph

The figure() function returns a plot object, which allows you to create various types of charts using the various glyphs methods. The circle(), line(), and triangle() glyph methods creates scatter plots with various marker shapes.

In the above, you also specified the filename of the page as my_first_graph.html, using the output_file() function; if you don't specify the output filename, a random file will be generated every time you run the code (and hence a new tab is created on your browser). When you run the cell containing the above code, you'll see the scatter plot containing the various markers as shown in Figure 1 in a new tab page on your Web browser.

Figure 1: Basic plots created using Bokeh
Figure 1: Basic plots created using Bokeh

Observe the toolbar displayed on the right side of the plot (see Figure 2).

Figure 2: The items in the toolbar
Figure 2: The items in the toolbar

The toolbar contains the following tools:

  • Bokeh: Link to the Bokeh page
  • Pan: Drag the chart to move it around
  • Box Zoom: Use your mouse to select part of the plot to zoom in
  • Wheel Zoom: Use the wheel on your mouse to zoom in and out of the plot
  • Save: Save and download a copy of your chart in PNG format
  • Reset: Restore the plot to its original state
  • Help: Link to the page on Bokeh plot tools

The toolbar is customizable and in a later section, I'll show you how to hide/add tools in the toolbar.

Vertical Bars

Bar charts can be drawn easily in Bokeh using the vbar() method. The following example shows how to display a bar chart by supplying data through a ColumnDataSource object:

from bokeh.plotting import figure, output_file, show
from bokeh.models import ColumnDataSource
import pandas as pd
import random

# create a Pandas dataframe
df = pd.DataFrame(dict(
    x = [1, 2, 3, 4, 5],
    y = random.sample(range(1,11), 5), 
))

# create a ColumnDataSource obj using a dataframe
source = ColumnDataSource(data=df)

p = figure(plot_width=400, plot_height=400)

p.vbar(x = 'x',
    top = 'y',
    source = source,
    width = 0.5,
    bottom = 0,
    color = 'lightgreen')

output_file('vbar.html')
show(p)

In the previous example, you provided the data to be plotted using lists. Although this is perfectly fine, it's better to use a ColumnDataSource object as the supplier of data to your plots. In fact, when you pass data to your Bokeh plots using lists, Bokeh automatically creates a ColumnDataSource object behind the scenes. Using a ColumnDataSource, you can enable more advanced capabilities for your Bokeh plots, such as sharing data between plots, filtering data, etc.

The ColumnDataSource object provides the data to the glyphs of your plot. It provides advanced capabilities for your Bokeh plots, such as sharing data between plots, filtering data, etc.

In the above example, the ColumnDataSource object is passed in through the source parameter of the vbar() method:

p.vbar(x = 'x',              # x-axis
       top = 'y',            # y-axis
       source = source,
       width = 0.5,          # width of bar
       bottom = 0,           # starts from bottom
       color = 'lightgreen')

Printing the source.data property shows the following (formatted for clarity):

print(source.data)
# {
#     'index': array([0, 1, 2, 3, 4]),
#     'x': array([1, 2, 3, 4, 5]),
#     'y': array([ 7, 10,  5,  9,  2])
#}

The value “x” in the vbar() method refers to the value of the “x” key in the value of source.data, and “y” refers to the value of the “y” key. Figure 3 shows the vertical bar chart generated by the vbar() method.

Figure 3: The plot created using the vbar() method
Figure 3: The plot created using the vbar() method

What happens if the values for the x-axis are categorical values, such as a list of fruits? Consider the following:

df = pd.DataFrame(dict(
    fruits = ['Apple','Orange','Pineapple', 'Pear','Kiwi'],
    sales = random.sample(range(1,11), 5),
))

In this case, in order to plot the chart correctly, you need to specify the x_range parameter in the figure() function and pass it the categorical values, like this:

from bokeh.plotting import figure, output_file, show
from bokeh.models import ColumnDataSource
import pandas as pd
import random

df = pd.DataFrame(dict(
    fruits = ['Apple','Orange','Pineapple', 'Pear','Kiwi'],
    sales = random.sample(range(1,11), 5),
))

source = ColumnDataSource(data=df)

p = figure(plot_width=400,
           plot_height=400,
           x_range=source.data['fruits'])

p.vbar(x = 'fruits',
       top = 'sales',
       source = source,
       width = 0.5,
       bottom = 0,
       color = 'lightgreen')

output_file('vbar.html')
show(p)

Figure 4 shows the updated vertical bar plot.

Figure 4: The updated vertical bar plot
Figure 4: The updated vertical bar plot

Displaying Legends and Colors

A chart needs a legend to be useful. Bokeh provides a collection of palettes for color mapping (https://docs.bokeh.org/en/latest/docs/reference/palettes.html). Consider the palette named Spectral (see Figure 5).

Figure 5: The various colors in the Spectral palette
Figure 5: The various colors in the Spectral palette

You can import the Spectral palette and examine its content:

from bokeh.palettes import Spectral
Spectral
# {3: ('#99d594', '#ffffbf', '#fc8d59'),
#  4: ('#2b83ba', '#abdda4', '#fdae61', '#d7191c'),
#  5: ('#2b83ba', '#abdda4', '#ffffbf', '#fdae61', '#d7191c'),
#  ...

Each row in the palette contains a collection (tuples) of colors. To use the colors in the palette, use the factor_cmap() function to create a dictionary of colors, like the following example:

from bokeh.plotting import figure, output_file, show
from bokeh.models import ColumnDataSource
import pandas as pd
import random

from bokeh.palettes import Spectral
from bokeh.transform import factor_cmap

df = pd.DataFrame(dict(
    fruits = ['Apple','Orange','Pineapple','Pear','Kiwi'],
    sales = random.sample(range(1,11), 5),
))

source = ColumnDataSource(data=df)

p = figure(plot_width=400,
           plot_height=400,
           x_range=source.data['fruits'])

p.vbar(source = source,
       x = 'fruits',
       top = 'sales',
       width = 0.5,
       bottom = 0,
       color = 'lightgreen',
       legend_field = 'fruits',
       line_color = 'black',
       fill_color = factor_cmap('fruits',
           palette=Spectral[6],
           factors=source.data['fruits'])
)

output_file('vbar.html')
show(p)

Figure 6 shows the vertical bar chart displayed in colors defined in the Spectral palette as well as with the legend.

Figure 6: Displaying the vertical bar plot using the Spectral palette
Figure 6: Displaying the vertical bar plot using the Spectral palette

Horizontal Bars

In addition to vertical bar chart, horizontal bar chart is also sometimes useful. The following code snippet shows how to display a horizontal bar chart using the hbar() method:

from bokeh.plotting import figure, output_file,show
from bokeh.models import ColumnDataSource
import pandas as pd
import random

from bokeh.palettes import Spectral
from bokeh.transform import factor_cmap

df = pd.DataFrame(dict(
    fruits = ['Apple','Orange','Pineapple','Pear','Kiwi'],
    sales = random.sample(range(1,11), 5),
))

source = ColumnDataSource(data=df)

p = figure(plot_width=400,
           plot_height=400,
           y_range=source.data['fruits'])

p.hbar(source = source,
       y = 'fruits',
       height = 0.5,
       left = 0,
       right = 'sales',
       color = 'lightgreen',
       legend_field = 'fruits',
       line_color = 'black',
       fill_color = factor_cmap('fruits',
           palette=Spectral[6],
           factors=source.data['fruits'])
)

output_file('hbar.html')
show(p)

Figure 7 shows the output using the colors from the Spectral palette.

Figure 7: Plotting using the hbar() method
Figure 7: Plotting using the hbar() method

Stacked Bars

A Stacked bar chart is a variation of bar chart that allows you to compare numeric values between values of a categorical variable. Suppose you have a dataframe containing the following rows and columns:

    teams  males  females
0  Team A      9        8
1  Team B      3        4
2  Team C      4       10
3  Team D      1        3
4  Team E      6        1

It would be useful to use a stacked bar to show the number of males and females in each team. The following code snippet shows how to use the vbar_stack() method to display a stack bar showing the composition of genders in each team:

from bokeh.plotting import figure, output_file, show
from bokeh.models import ColumnDataSource
import pandas as pd
import random

from bokeh.palettes import Category20

df = pd.DataFrame(dict(
    teams = ['Team A','Team B','Team C','Team D','Team E'],
    males = random.sample(range(1,11), 5),
    females = random.sample(range(1,11), 5),
))

source = ColumnDataSource(data=df)

p = figure(plot_width=400,
           plot_height=400,
           x_range=source.data['teams'])
           
v = p.vbar_stack(['males', 'females'],
                 source = source,
                 x = 'teams',
                 width=0.8,
                 color=Category20[8][5:7],
)

output_file('vbar.html')
show(p)

Figure 8 shows the output for the stacked bars.

Figure 8: Plotting a stacked bar plot
Figure 8: Plotting a stacked bar plot

What the stacked bars lack is a legend to indicate what the red and green bars represent. So let's add a legend to the plot using the Legend class:

...
p = figure(plot_width=600,
          plot_height=400,
          x_range=source.data['teams'])

v = p.vbar_stack(['males', 'females'],
                 source = source,
                 x = 'teams',
                 width=0.8,
                 color=Category20[8][5:7],
)

from bokeh.models import Legend
legend = Legend(items=[
    ("males",   [v[0]]),
    ("females", [v[1]]),
], location=(0, -30))

p.add_layout(legend, 'right')

output_file('vbar.html')
show(p)

You can specify the position of the legend and where to add it to the plot. Figure 9 shows the stacked bars with the legend.

Figure 9: The stacked bar plot with the legend
Figure 9: The stacked bar plot with the legend

Pie Charts

A pie chart is another popular way to illustrate numerical proportion. In Bokeh, you can use the wedge() method of the plot object to display a pie chart. In the following code snippet, you have a dataframe containing a list of fruits and their sales:

      fruits  sales
0      Apple     10
1     Orange      8
2  Pineapple      5
3       Pear      4
4       Kiwi      7
5     Banana      2
6     Papaya      9
7     Durian      1
8      Guava      3

To display a pie chart showing the sales of the various fruits, you need to calculate the angle of each slice:

df['angles'] = \
    df['sales'] / df['sales'].sum() * \ 2*math.pi

In addition, you can also assign specific color to each slice:

df['colors'] = Category20c[len(df['fruits'])]

The following code snippet outputs the pie chart as shown in Figure 10:

from bokeh.plotting import figure, output_file, show
from bokeh.models import ColumnDataSource
from bokeh.palettes import Category20c
from bokeh.transform import cumsum
import pandas as pd
import random
import math

df = pd.DataFrame(dict(
    fruits =
        ['Apple','Orange','Pineapple',
         'Pear','Kiwi','Banana',
         'Papaya','Durian','Guava'],
    sales = random.sample(range(1,11), 9),
))

df['colors'] = Category20c[len(df['fruits'])]
df['angles'] = \
    df['sales'] / df['sales'].sum() * \ 2*math.pi

source = ColumnDataSource(data=df)

p = figure(plot_width=700,
           plot_height=500,
           x_range=source.data['fruits'])

p.wedge(x=len(df['fruits'])/2,
        y=0,
        radius=3.0,
        start_angle=cumsum('angles', include_zero=True),
        end_angle=cumsum('angles'),
        line_color="white",
        fill_color='colors',
        legend_field='fruits',
        source=source)

output_file('piechart.html')
show(p)
Figure 10: Plotting a pie plot using the wedge() method
Figure 10: Plotting a pie plot using the wedge() method

Notice that for the pie chart, it doesn't really make sense for the axes to be visible, and so you can turn them off using the axis.visible property:

p.wedge(x = len(df['fruits'])/2,
        ...)

p.axis.visible = False

output_file('piechart.html')
show(p)

Figure 11 shows the pie chart without the axes.

Figure 11: Turning off the axes for the pie plot
Figure 11: Turning off the axes for the pie plot

A pie chart without label on it may not be very useful, so let's now add text to each slice of the pie plot using the LabelSet class:

from bokeh.plotting import figure, output_file, show
from bokeh.models import ColumnDataSource, LabelSet
import pandas as pd
import random

from bokeh.palettes import Category20c

import math
from bokeh.transform import cumsum

df = pd.DataFrame(dict(
    fruits = ['Apple','Orange','Pineapple',
              'Pear','Kiwi','Banana',
              'Papaya','Durian','Guava'],
    sales = random.sample(range(1,11), 9),
))

df['colors'] = Category20c[len(df['fruits'])]
df['angles'] = \
    df['sales'] / df['sales'].sum() * \ 2*math.pi
df['label'] = (df['fruits'] + "-" +
               df['sales'].astype(
               str)).str.pad(30, side='left')

source = ColumnDataSource(data=df)

p = figure(plot_width=700,
           plot_height=500,
           x_range=source.data['fruits'])

p.wedge(x = len(df['fruits'])/2,
        y = 0,
        radius = 3.0,
        start_angle = cumsum('angles',
                      include_zero=True),
        end_angle = cumsum('angles'),
        line_color = 'white',
        fill_color = 'colors',
        legend_field = 'fruits',
        source = source)

p.axis.visible = False

labels = LabelSet(x = len(df['fruits'])/2,
                  y = 0,
                  text = 'label',
                  angle = cumsum('angles', include_zero=True),
                  source = source,
                  render_mode = 'canvas')

p.add_layout(labels)

output_file('piechart.html')
show(p)

Figure 12 shows the much-improved pie plot with each slice labeled with the name and the sales.

Figure 12: The pie plot with each slice clearly labeled
Figure 12: The pie plot with each slice clearly labeled

Dashboard Layouts

Now that you have learned the basics of Bokeh plots, let's not forget why you want to use Bokeh in the first place. If you just want to plot some simple charts, you could jolly well use matplotlib or Seaborn. Using Bokeh, you could layout multiple charts in a single page.

Bokeh provides several layout options:

  • Row
  • Column
  • Gridplot

Listing 1 shows how you can use the row layout to display three charts.

Listing 1: Using the row layout

from bokeh.plotting import figure, output_file, show
from bokeh.models import ColumnDataSource
from bokeh.layouts import row, column, gridplot

import pandas as pd
import numpy as np
import math

x = np.arange(0, math.pi*2, 0.05)

df = pd.DataFrame(dict(
    x = x,
    sin = np.sin(x),
    cos = np.cos(x),
    tan = np.tan(x)
))

source = ColumnDataSource(data=df)

#-------------------------------
#  Sine Wave
#-------------------------------
p1 = figure(title = "Sine wave",
x_axis_label = 'x',
y_axis_label = 'y',
plot_width=300,
plot_height=300)
p1.line('x', 'sin',
source=source,
legend_label = "sine",
line_width = 2)

#-------------------------------
#  Cosine Wave
#-------------------------------
p2 = figure(title = "Cosine wave",
            x_axis_label = 'x',
            y_axis_label = 'y',
            plot_width=300,
            plot_height=300)
p2.line('x', 'cos',
        source=source,
        legend_label = "cos",
        line_width = 2)

#-------------------------------
#  Tangent Wave
#-------------------------------
p3 = figure(title = "Tangent wave",
            x_axis_label = 'x',
            y_axis_label = 'y',
            plot_width=300,
            plot_height=300)
p3.line('x', 'tan',
        source=source,
        legend_label = "tan",
        line_width = 2)

#-------------------------------
#  Laying out in row
#-------------------------------
plot = row([p1, p2, p3], sizing_mode='stretch_both')

output_file("sine_cosine_tangent.html")
show(plot)

You make use of the sizing_mode parameter to configure how the charts are displayed when the display changes in size, as you can see in this snippet:

#-------------------------------
#       Laying out in row
#-------------------------------
plot = row([p1, p2, p3], sizing_mode='stretch_both')

Figure 13 shows the output of the three charts: Sine wave, Cosine wave, and Tangent wave.

Figure 13: Laying out the plots in row format
Figure 13: Laying out the plots in row format

You can also use the column layout:

#-------------------------------
#      Laying out in column
#-------------------------------
plot = column([p1, p2, p3], sizing_mode='stretch_both')

Figure 14 shows the charts displayed vertically in a column layout.

Figure 14: Laying out the plots in column format
Figure 14: Laying out the plots in column format

Finally, you can also display the charts in a gridplot:

#-------------------------------
#     Laying out in gridplot
#-------------------------------
plot = gridplot([p1, p2, p3], ncols=2, sizing_mode='stretch_both')

Based on the value of the ncols parameter, Bokeh automatically lays out your plot from left to right, top to bottom, as shown in Figure 15.

Figure 15: Laying out the plots in gridplot format
Figure 15: Laying out the plots in gridplot format

Observe that the Tangent chart is displayed on the left side on the second row. If you want to explicitly specify how the charts are displayed, you can leave out the ncols parameter and pass in your plots objects as lists:

plot = gridplot([[p1, p2], [None, p3]], sizing_mode='stretch_both')

Figure 16 shows the Tangent curve displayed on the right side of the second column.

Figure 16: Controlling how each plot is shown in a gridplot
Figure 16: Controlling how each plot is shown in a gridplot

Using the Bokeh Server

If you observe carefully, so far, all the charts generated by Bokeh run from the file system of your computer: file:///Volumes/SSD/Dropbox/Articles/CoDe/Bokeh/source/sine_cosine_tangent.html. Although this is useful if you're the only one viewing the charts, it's a problem if you need to publish the charts to a wider audience. This is where the Bokeh server comes in. The purpose of the Bokeh server is to make it easy for Python developers to create interactive Web applications that can connect front-end UI events to real, running Python code.

To convert an existing Bokeh to run using the Bokeh server, you just need to import the curdoc() function, and then add the plot object to the root of the current document:

from bokeh.io import curdoc

curdoc().add_root(plot)
curdoc().title = "Using the Bokeh Server"

The curdoc() function returns the current document. Think of it as a collection of Bokeh plots.

Listing 2 shows the previous example being converted to run using the Bokeh server.

Listing 2: Saving the file as layouts.py to run it using the Bokeh server

from bokeh.plotting import figure, output_file, show
from bokeh.models import ColumnDataSource
from bokeh.layouts import row, column, gridplot

import pandas as pd
import numpy as np
import math

from bokeh.io import curdoc

x = np.arange(0, math.pi*2, 0.05)

df = pd.DataFrame(dict(
    x = x,
    sin = np.sin(x),
    cos = np.cos(x),
    tan = np.tan(x)
))

source = ColumnDataSource(data=df)

#-------------------------------
#   Sine Wave
#-------------------------------
p1 = figure(title = "Sine wave",
            x_axis_label = 'x',
            y_axis_label = 'y',
            plot_width=300,
            plot_height=300
)
p1.line('x', 'sin',
        source=source,
        legend_label = "sine",
        line_width = 2)

#-------------------------------
#   Cosine Wave
#-------------------------------
p2 = figure(title = "Cosine wave",
            x_axis_label = 'x',
            y_axis_label = 'y',
            plot_width=300,
            plot_height=300
)
p2.line('x', 'cos',
        source=source,
        legend_label = "cos",
        line_width = 2)

#-------------------------------
#   Tangent Wave
#-------------------------------
p3 = figure(title = "Tangent wave",
            x_axis_label = 'x',
            y_axis_label = 'y',
            plot_width=300,
            plot_height=300
)
p3.line('x', 'tan',
        source=source,
        legend_label = "tan",
        line_width = 2)

#-------------------------------
#   Laying out in gridplot
#-------------------------------
plot = gridplot([p1, p2, p3], ncols=2, sizing_mode='stretch_both')

# output_file("sine_cosine_tangent.html")
# show(plot)

curdoc().add_root(plot)
curdoc().title = "Using the Bokeh Server"

To run the program using the Bokeh server, you need to save it as a Python file (layouts.py) and run it on the Anaconda Prompt/Terminal:

$ bokeh serve --show layouts.py

Figure 17 shows that the Bokeh server publishes the page using a Web server listening at port 5006.

Figure       17: Hosting the dashboard on a Web server and viewing it on a Web browser
Figure 17: Hosting the dashboard on a Web server and viewing it on a Web browser

Bokeh Widgets

You've learned that you can publish your charts using the Bokeh server so that your users can now view your charts easily through a Web browser. To create a front-end user interface for your users to interact with the charts, Bokeh provides widgets - interactive controls that can be added to your Bokeh applications. Bokeh widgets include:

  • Button
  • CheckboxButtonGroup
  • CheckboxGroup
  • ColorPicker
  • DataTable
  • DatePicker
  • DateRangeSlider
  • Div
  • Dropdown
  • FileInput
  • MultiChoice
  • MultiSelect
  • Paragraph
  • PasswordInput
  • PreText
  • RadioButtonGroup
  • RadioGroup
  • RangeSlider
  • Select
  • Slider
  • Spinner
  • Tabs
  • TextAreaInput
  • TextInput
  • Toggle

Bokeh widgets are interactive controls that can be added to your Bokeh applications.

Due to space constraints, I'll only illustrate a few of the above widgets.

Select Widget

The Select widget allows the user to make a single selection from a list of items. Listing 3 shows a complete example of the use of the Select widget. In particular:

  • The on_change() function allows you to set the event handler for the Select's value event.
  • When the user selects an item from the Select widget, you make use of the event handler to update the ColumnDataSource object so that the chart can be updated dynamically.
  • The event handler for the Select widget's value event takes in three parameters: attr (the property that triggered the event handler), old (the old value of the property), and new (the new value of the property).

Listing 3: Saving the file as SelectWidget.py to run it using the Bokeh server

import pandas as pd
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, Select
from bokeh.layouts import column
from bokeh.io import curdoc

# load the dataframe from a CSV file
df = pd.read_csv('AAPL.csv', parse_dates=['Date'])

# extract the columns
Date = df['Date']
Close = df['Close']
AdjClose = df['Adj Close']

# create the data source
source = ColumnDataSource(data=
{
    'x' : Date,
    'y' : Close
})

# called when the Select item changes in value
def update_plot(attr, old, new):
    if new == 'Close':
        # update the data source
        source.data['y'] = Close
        p.yaxis.axis_label = 'Close'
    else:
        source.data['y'] = AdjClose
        p.yaxis.axis_label = 'Adj Close'

# display a selection menu
menu = Select(options=[('Close','Close'), ('Adj Close','Adj Close')],
              value='Close',
              title = 'AAPL Stocks')

# callback when the Select menu changes its value
menu.on_change('value', update_plot)

p = figure(x_axis_type='datetime') # display the x-axis as dates

p.line('x',
       'y',
       source=source,
       color='green',
       width=3)

p.title.text = 'AAPL Stock Prices'
p.xaxis.axis_label = 'Date'
p.yaxis.axis_label = 'Close'

curdoc().theme = 'night_sky'
curdoc().add_root(column(menu, p))
curdoc().title = "First Graph"

Figure 18 shows the output when the file is run in Anaconda Prompt/Terminal:

$ bokeh serve  --show SelectWidget.py
Figure 18: Using the Select Bokeh widget
Figure 18: Using the Select Bokeh widget

Selecting an item in the Select widget updates the chart to display either the closing or adjusted closing price of AAPL.

Another Bokeh widget that's like the Select widget is the Dropdown widget, a button that displays a drop-down list of mutually exclusive items when clicked.

The following code snippet shows how to display a Dropdown widget and wire it up with a callback function when it's clicked:

# display a Dropdown menu
menu = Dropdown(label="Select Company", menu=[
    ('Apple','AAPL'),
    ('Amazon','AMZN'),
    ('Google','GOOG')])

# callback when the Dropdown menu is selected
menu.on_click(update_plot)

The callback function for the Dropdown function takes in a single argument:

# called when the Select item changes in value
def update_plot(event):
    print(event.item)   # the value of the item selected in the
                        # Dropdown widget

The item property allows you to know which item was selected in the Dropdown widget. Listing 4 shows how you can load the content of different CSV files depending on which item has been selected in the widget.

Listing 4: Saving the file as Dropdown.py to run it using the Bokeh server

import pandas as pd
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, Dropdown
from bokeh.layouts import column
from bokeh.io import curdoc

# load the dataframe
df = pd.read_csv('AAPL.csv', parse_dates=['Date'])

# create the data source
source = ColumnDataSource(df)

# called when the Select item changes in value
def update_plot(event):
    if event.item == 'AAPL':
        df = pd.read_csv('AAPL.csv', parse_dates=['Date'])
        p.title.text = 'Apple Stock Prices'
    elif event.item == 'AMZN':
        df = pd.read_csv('AMZN.csv', parse_dates=['Date'])
        p.title.text = 'Amazon Stock Prices'
    else:
        df = pd.read_csv('GOOG.csv', parse_dates=['Date'])
        p.title.text = 'Google Stock Prices'

# update the date source
source.data = df

# display a Dropdown menu
menu = Dropdown(label="Select Company",
                menu=[
                    ('Apple','AAPL'),
                    ('Amazon','AMZN'),
                    ('Google','GOOG')])

# callback when the Dropdown menu is selected
menu.on_click(update_plot)

p = figure(x_axis_type='datetime',
           plot_width=1500)
p.line("Date",
       "Close",
       source=source,
       color='green',
       width=3)

p.title.text = 'Apple Stock Prices'
p.xaxis.axis_label = 'Date'
p.yaxis.axis_label = 'Close'

curdoc().theme = 'night_sky'
curdoc().add_root(column(menu, p))
curdoc().title = "Stocks Chart"

Figure 19 shows the output when the file is run in Anaconda Prompt/Terminal:

$ bokeh serve --show Dropdown.py
Figure 19: Using the Dropdown Bokeh widget
Figure 19: Using the Dropdown Bokeh widget

Tabs Widget

The Tabs widget allows you to display plots in different tabs, much like tabs in a Web browser. You first create Panel objects to act as the containers for your various plots. The Tabs widget then takes the various Panel objects and displays them in tabs.

Listing 5 shows an example of the Tabs widget containing three Panel objects, the first displaying the Sine wave, the second displaying the Cosine wave, and the third one displaying the Tangent wave.

Listing 5: Saving the file as Tabs.py to run it using the Bokeh server

from bokeh.plotting import figure, output_file, show
from bokeh.models import Panel, Tabs
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.models import Range1d

import numpy as np
import math

width = 500
height = 500

x = np.arange(0, math.pi*12, 0.05)

#------------------------------------------------------
#      Sine tab
#------------------------------------------------------
p1 = figure(plot_width=width, plot_height=height)
p1.x_range = Range1d(0, math.pi*2)
p1.line(x, np.sin(x), line_width=2, line_color='yellow')
tab1 = Panel(child = p1, title = "Sine")

#------------------------------------------------------
#      Cosine tab
#------------------------------------------------------
p2 = figure(plot_width=width, plot_height=height)
p2.x_range = Range1d(0, math.pi*2)
p2.line(x,np.cos(x), line_width=2, line_color='orange')
tab2 = Panel(child=p2, title = "Cos")

#------------------------------------------------------
#      Tangent tab
#------------------------------------------------------
p3 = figure(plot_width=width, plot_height=height)
p3.x_range = Range1d(0, math.pi*2)
p3.y_range = Range1d(-5, 5)
p3.line(x,np.tan(x), line_width=2, line_color='green')
tab3 = Panel(child=p3, title = "Tan")

# Group all the panels into tabs
tabs = Tabs(tabs=[tab1,tab2,tab3])

curdoc().theme = 'contrast'
curdoc().add_root(column(tabs))
curdoc().title = "Trigonometry"

Figure 20 shows the output when the file is run in Anaconda Prompt/Terminal:

$ bokeh serve --show Tabs.py
Figure 20: Using the Tabs widget to display the three trigonometric functions: sine, cosine, and tangent
Figure 20: Using the Tabs widget to display the three trigonometric functions: sine, cosine, and tangent

Slider Widget

The Slider widget allows users to select a range of floating values. To use this widget, simply set its start value, end value, current value, and the step size (the value to increment/decrement each time the slider moves).

Listing 6 shows an example of the Slider widget where users can select the frequencies of the sine wave to plot.

Listing 6: Saving the file as Slider.py to run it using the Bokeh server

import numpy as np
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.models import ColumnDataSource
from bokeh.models.widgets import Slider
from bokeh.plotting import figure

N = 200
x = np.linspace(0, 2*np.pi, N)
y = np.sin(x)

source = ColumnDataSource(data = dict(
                          x = x,
                          y = y)
)

p = figure(plot_height = 400, plot_width = 600, title = "Sine")
p.line('x',
       'y',
       source = source,
       line_width = 5,
       line_alpha = 0.9)

freq = Slider(
    title = "Frequency",
    value = 1.0,
    start = 0.1,
    end = 3.0,
    step = 0.1,
)

freq.width = 600

def update_data(attrname, old, new):
    y = np.sin(new * x)    # new is same as freq.value
    # update the source
    source.data['y'] = y

freq.on_change('value', update_data)

curdoc().add_root(column(p, freq, width = 600))
curdoc().title = "Sliders"

Figure 21 shows the output when the file is run in Anaconda Prompt/Terminal:

$ bokeh serve --show Slider.py
Figure 21: Plotting the sine wave with the user chosen frequencies
Figure 21: Plotting the sine wave with the user chosen frequencies

Displaying Tooltips on your Chart

Recall the earlier example where I displayed the stock prices of AAPL? It would be useful to be able to display the various information of AAPL's stock price when the user's mouse hovers over a particular data point on the chart. To do that in Bokeh, you can use the HoverTool class:

from bokeh.models.tools import HoverTool
hover = HoverTool(formatters={'@Date':'datetime'})
hover.tooltips=[
    ('Date',           '@Date{%Y-%m-%d}'),
    ('Opening',        '@Open{$0,0.00f}'),
    ('Highest',        '@High{$0,0.00f}'),
    ('Lowest',         '@Low{$0,0.00f}'),
    ('Closing',        '@Close{$0,0.00f}'),
    ('Adjusted Close', '@{Adj Close}{$0,0.00f}'),
    ('Volume',         '@Volume{0,0f}'),
]
p.add_tools(hover)

Figure 22 explains how the value set in the tooltips property helps to configure the tooltip that's displayed when the user hovers over a data point on the chart.

Figure 22: How to configure the tooltips property in the HoverTool class
Figure 22: How to configure the tooltips property in the HoverTool class

Listing 7 shows the Bokeh application with the code for the HoverTool class added.

Listing 7: Saving the file as Tooltips.py to run it using the Bokeh server

import pandas as pd
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, Dropdown
from bokeh.layouts import column
from bokeh.io import curdoc

# load the dataframe
df = pd.read_csv('AAPL.csv', parse_dates=['Date'])

# create the data source
source = ColumnDataSource(df)

# called when the Select item changes in value
def update_plot(event):
    if event.item == 'AAPL':
        df = pd.read_csv('AAPL.csv', parse_dates=['Date'])
        p.title.text = 'Apple Stock Prices'
    elif event.item == 'AMZN':
        df = pd.read_csv('AMZN.csv', parse_dates=['Date'])
        p.title.text = 'Amazon Stock Prices'
    else:
        df = pd.read_csv('GOOG.csv', parse_dates=['Date'])
        p.title.text = 'Google Stock Prices'

    # update the date source
    source.data = df

# display a Dropdown menu
menu = Dropdown(label="Select Company",
                menu=[
                    ('Apple','AAPL'),
                    ('Amazon','AMZN'),
                    ('Google','GOOG')])

# callback when the Dropdown menu is selected
menu.on_click(update_plot)

p = figure(x_axis_type='datetime', plot_width=1500)
p.line("Date", "Close",
       source=source,
       color='green',
       width=3)

p.title.text = 'Apple Stock Prices'
p.xaxis.axis_label = 'Date'
p.yaxis.axis_label = 'Close'

from bokeh.models.tools import HoverTool
hover = HoverTool(formatters={'@Date':'datetime'})
hover.tooltips=[
    ('Date',           '@Date{%Y-%m-%d}'),
    ('Opening',        '@Open{$0,0.00f}'),        # format as $
    ('Highest',        '@High{$0,0.00f}'),        # format as $
    ('Lowest',         '@Low{$0,0.00f}'),         # format as $
    ('Closing',        '@Close{$0,0.00f}'),       # format as $
    ('Adjusted Close', '@{Adj Close}{$0,0.00f}'), # format as $
    ('Volume',         '@Volume{0,0f}'),          # format as number
]
p.add_tools(hover)

curdoc().theme = 'night_sky'
curdoc().add_root(column(menu, p))
curdoc().title = "Stocks Chart"

Figure 23 shows the output when the file is run in Anaconda Prompt/Terminal.

Figure 23: Displaying a tooltip when the user hovers over a chart
Figure 23: Displaying a tooltip when the user hovers over a chart

Putting It Altogether

If you have been following along up to this point, you've learned quite a few tricks with Bokeh. So, let's now put everything that you've learned to good use and create a dashboard that users can interact with.

You'll make use of the dataframe that I have used earlier:

    teams  males  females
0  Team A      3        4
1  Team B      2       10
2  Team C      9        6
3  Team D      7        3
4  Team E      6        9

Note that the numbers for each gender in each team is randomly generated, so the chart you'll see later may not correspond to the numbers you see here. Here's what you'll build for your dashboard:

  • A dashboard containing three charts:
    • A stacked bar chart showing the number of males and females for each team
    • A pie chart showing the total number of members in each team
    • A pie chart showing the proportion of males and females in each selected team(s)
  • When a bar or slice is selected, the other charts update accordingly.

Creating the ColumnDataSource objects

For this application, you'll create two ColumnDataSource objects:

  • The first one for use by the stacked bar chart and the first pie chart
  • The second one for use by the second pie chart

The data for the first ColumnDataSource object have the following values (the values in the angle column are calculated based on the total number of males and females in each team):

    teams  males  females    angles   colors
0  Team A      3        4  0.745463  #0868ac
1  Team B      2       10  1.277936  #43a2ca
2  Team C      9        6  1.597420  #7bccc4
3  Team D      7        3  1.064947  #bae4bc
4  Team E      6        9  1.597420  #f0f9e8

The data for the second ColumnDataSource object have the following values:

         count    angles   colors
males       27  2.875356  #98df8a
females     32  3.407829  #d62728

The values for this object are updated dynamically whenever the user changes his selection on the bar or pie chart.

Allowing Items to Be Selected in Your Chart

To allow users to select bars or slices in a bar or pie chart, you need to include the tap tool in the toolbar (see Figure 24). The Tools parameter in the figure() function allows you to select which tools to display in the toolbar:

p1 = figure(plot_width=400,
            plot_height=400,
            x_range=source.data['teams'],
            tools='tap,pan,wheel_zoom,box_zoom,reset')

The Tap tool is, by default, not shown in the toolbar.

Figure 24: Locating the Tap tool in the toolbar
Figure 24: Locating the Tap tool in the toolbar

Synchronizing Charts

To update all of the affected charts when a selection is made on one, you need to handle the indices event of the ColumnDataSource object:

#---set the event handler for ColumnDataSource---
source.selected.on_change('indices', source_index_change)

The event handler for the indices event has three parameters � attrname (name of the attribute changed), old (old value for the attribute), and new (the new value of the attribute). Here, you'll make use of new to know which item(s) has been selected in the bar or pie chart. Based on the bars or slice(s) selected, you can now recalculate the data for the second ColumnDataSource object:

#------event handler for ColumnDataSource-------
def source_index_change(attrname, old, new):
    # new is a list containing the index of all
    # selected items (e.g. bars in a bar chart, etc)
    # new is same as source.selected.indices

    # if user clicks outside the bar
    if new == []: new = range(len(df)) # set to all selected

    print('Selected column(s):', new)
    print('Total Males:',
        df.iloc[new]['males'].sum())
    print('Total Females:',
        df.iloc[new]['females'].sum())

    # count the males and females for the selected team(s)
    df2.loc['males','count'] = \
        df.iloc[new]['males'].sum()
    df2.loc['females','count'] = \
        df.iloc[new]['females'].sum()
    source2.data['angles'] = \
        df2['count'] / df2['count'].sum() * \ 2*math.pi
    p3.title.text = \
        f"Gender distribution for {[df.iloc[t,0] for t in new]}"

The full code listing for this application is shown in Listing 8.

Listing 8: Saving the file as Dashboard.py to run it using the Bokeh server

from bokeh.plotting import figure, output_file, show
from bokeh.models import ColumnDataSource, Legend
from bokeh.palettes import Category20, Spectral, GnBu
from bokeh.io import curdoc
from bokeh.layouts import row
from bokeh.transform import cumsum

import pandas as pd
import random
import math

df = pd.DataFrame(dict(
    teams = ['Team A','Team B','Team C','Team D','Team E'],
    males = random.sample(range(1,11), 5),
    females = random.sample(range(1,11), 5),
))

#----used for displaying a pie chart---
df['angles'] = (df['males'] +  df['females']) / (df['males'] +
                df['females']).sum() * 2*math.pi
df['colors'] = GnBu[len(df['teams'])]

source = ColumnDataSource(data=df)

#---create another source to store count of males and females---
males = df['males'].sum()
females = df['females'].sum()
df2 = pd.DataFrame([males,females],
    columns=['count'],
    index =['males','females'])
df2['angles'] = df2['count'] / df2['count'].sum() * 2*math.pi
df2['colors'] = Category20[8][5:7]

source2 = ColumnDataSource(data=df2)

#----------------------------------------------------
#      Bar chart
#----------------------------------------------------
p1 = figure(plot_width=400,
            plot_height=400,
            x_range=source.data['teams'],
            tools='tap,pan,wheel_zoom,box_zoom,reset')
v = p1.vbar_stack(['males', 'females'],
            source = source,
            x = 'teams',
            width=0.8,
            color=Category20[8][5:7])
legend = Legend(items=[
    ("males",   [v[0]]),
    ("females", [v[1]]),
], location=(0, 0))
p1.add_layout(legend)
p1.title.text = "Gender distribution in each team"

#----------------------------------------------------
#      Pie chart 1
#----------------------------------------------------
p2 = figure(plot_width=400,
            plot_height=400,
            tools='tap,pan,wheel_zoom,box_zoom,reset')
p2.wedge(x=0,
         y=1,
         radius=0.7,
         start_angle=cumsum('angles', include_zero=True),
         end_angle=cumsum('angles'),
         line_color="white",
         fill_color='colors',
         legend_field='teams',
         source=source)
p2.axis.visible = False
p2.title.text = "Members in each team"

#----------------------------------------------------
#      Pie chart 2
#----------------------------------------------------
p3 = figure(plot_width=400,
            plot_height=400)
p3.wedge(x=0,
         y=1,
         radius=0.7,
         start_angle=cumsum('angles', include_zero=True),
         end_angle=cumsum('angles'),
         line_color="white",
         fill_color='colors',
         legend_field='index',
         source=source2)
p3.axis.visible = False
p3.title.text = "Gender distribution for all teams"

#------event handler for ColumnDataSource-------
def source_index_change(attrname, old, new):
    # new is a list containing the index of all
    # selected items (e.g. bars in a bar chart, etc)
    # new is same as source.selected.indices

    # if user clicks outside the bar
    if new == []: new = range(len(df))
    print('Selected column(s):', new)
    print('Total Males:', df.iloc[new]['males'].sum())
    print('Total Females:',df.iloc[new]['females'].sum())

    # count the males and females for the selected team(s)
    df2.loc['males','count'] =  df.iloc[new]['males'].sum()
    df2.loc['females','count'] =  df.iloc[new]['females'].sum()
    source2.data['angles'] = df2['count'] / df2['count'].sum() * 2*math.pi
    p3.title.text = \f"Gender distribution for {[df.iloc[t,0] for t in new]}"

#------set the event handler for ColumnDataSource-------
source.selected.on_change('indices',source_index_change)

curdoc().add_root(row(p1,p2,p3, width = 1200))
curdoc().title = "Interactive Graph"

Figure 25 shows the dashboard showing the stacked bar chart and the two pie charts when the file is run in Anaconda Prompt/Terminal:

$ bokeh serve --show Dashboard.py
Figure 25: The dashboard containing three plots
Figure 25: The dashboard containing three plots

You can click on any of the bars in the stacked bar chart as well as any of the slices in the pie chart (press the Shift key to select more than one item) and the second pie chart will now display the distribution of the gender in the select bars or slices (see Figure 26).

Figure 26: Selecting any bar or slice will update the third plot
Figure 26: Selecting any bar or slice will update the third plot

Summary

In this article, I've taken you on a whirlwind tour of Bokeh. I've discussed how to plot different plots using the various glyphs methods, as well as how to group them using the various layout classes. You also learned how to deploy your Bokeh plots using the Bokeh server, and how to use the Bokeh widgets to facilitate user interactions. Finally, I end this article by creating an interactive dashboard where interacting with one plot automatically updates the others. I hope you have fun with your newfound knowledge!