Bokeh Tutorial

A1. Models and Primitives

Overview

Although typically referred to by a single name, Bokeh comprises two separate libraries.

The first component is a JavaScript library, BokehJS, that runs in the browser. This library is responsible for all of the rendering and user interaction. Its input is a collection of declarative JSON objects that comprise a “scenegraph”. The objects in this scenegraph describe everything that BokehJS should handle: what plots and widgets are present and in what arrangement, what tools and renderers and axes the plots will have, etc. These JSON objects are converted into JavaScript objects in the browser.

The second component is a library in Python (or other languages) that can generate the JSON described above. In the Python Bokeh library, this is accomplished at the lowest level by exposing a set of Model subclasses that exactly mirror the set of BokehJS Models that are use in the browser. Most of the models are very simple, usually consisting of a few property attributes and no methods. Model attributes can either be configured when the model is created, or later by setting attribute values on the model object:

properties can be configured when a model object is initialized

glyph = Rect(x="x", y="y2", w=10, h=20, line_color=None)

properties can be configured by assigning values to attributes on exitsting models

glyph.fill_alpha = 0.5
glyph.fill_color = "navy"

These methods of configuration work in general for all Bokeh models.

Bokeh models are eventually collected into a Bokeh Document for serialization between BokehJS and the Bokeh Python bindings. As an example, the following image shows an example document for simple plot:

Document Structure

Such a document could be created at a high level using the bokeh.plotting API, which automatically assembles any models such as axes and grids and toolbars in a reasonable way. However it is also possible to assemble all the components "by hand" using the low-level bokeh.models API. Using the bokeh.models interface provides complete control over how Bokeh plots and Bokeh widgets are put together and configured. However, it provides no help with assembling the models in meaningful or correct ways. It is entirely up to developers to build the scenegraph “by hand”.

For more information about the details of all Bokeh models, consult the Reference Guide.

Example Walkthrough

Let's try to reproduce this NYTimes interactive chart Usain Bolt vs. 116 years of Olympic sprinters using the bokeh.models interface.

The first thing we need is to get the data. The data for this chart is located in the bokeh.sampledata module as a Pandas DataFrame. You can see the first ten rows below:

In [ ]:
from bokeh.sampledata.sprint import sprint
sprint[:10]

Next we import some of the Bokeh models that need to be assembled to make a plot. At a minimum, we need to start with Plot, the glyphs (Circle and Text) we want to display, as well as ColumnDataSource to hold the data and range objects to set the plot bounds.

In [ ]:
from bokeh.io import output_notebook, show
from bokeh.models import Circle, Text, ColumnDataSource, Range1d, DataRange1d, Plot
In [ ]:
output_notebook()

Setting up Data

Next we need set up all the columns we want in our column data source. Here we add a few extra columns like MetersBack and SelectedName that we will use for a HoverTool later.

In [ ]:
abbrev_to_country = {
    "USA": "United States",
    "GBR": "Britain",
    "JAM": "Jamaica",
    "CAN": "Canada",
    "TRI": "Trinidad and Tobago",
    "AUS": "Australia",
    "GER": "Germany",
    "CUB": "Cuba",
    "NAM": "Namibia",
    "URS": "Soviet Union",
    "BAR": "Barbados",
    "BUL": "Bulgaria",
    "HUN": "Hungary",
    "NED": "Netherlands",
    "NZL": "New Zealand",
    "PAN": "Panama",
    "POR": "Portugal",
    "RSA": "South Africa",
    "EUA": "United Team of Germany",
}

fill_color = { "gold": "#efcf6d", "silver": "#cccccc", "bronze": "#c59e8a" }
line_color = { "gold": "#c8a850", "silver": "#b0b0b1", "bronze": "#98715d" }

def selected_name(name, medal, year):
    return name if medal == "gold" and year in [1988, 1968, 1936, 1896] else ""

t0 = sprint.Time[0]

sprint["Abbrev"]       = sprint.Country
sprint["Country"]      = sprint.Abbrev.map(lambda abbr: abbrev_to_country[abbr])
sprint["Medal"]        = sprint.Medal.map(lambda medal: medal.lower())
sprint["Speed"]        = 100.0/sprint.Time
sprint["MetersBack"]   = 100.0*(1.0 - t0/sprint.Time)
sprint["MedalFill"]    = sprint.Medal.map(lambda medal: fill_color[medal])
sprint["MedalLine"]    = sprint.Medal.map(lambda medal: line_color[medal])
sprint["SelectedName"] = sprint[["Name", "Medal", "Year"]].apply(tuple, axis=1).map(lambda args: selected_name(*args))

source = ColumnDataSource(sprint)

Building in stages

Let's build up our plot in stages, stopping to check the output along the way to see how things look.

As we go through, note the three methods that Plot, Chart, and Figure all have:

  • p.add_glyph
  • p.add_tools
  • p.add_layout

These are actually small convenience methods that help us add models to Plot objects in the correct way.

Basic Plot with Just Glyphs

First we create just the Plot with a title and some basic styling applied, as well add a few Circle glyphs for the actual race data. To manually configure glyphs, we first create a glyph object (e.g., Text or Circle) that is configured with the visual properties we want as well as the data columns to use for coordinates, etc. Then we call plot.add_glyph with the glyph, and the data source that the glyph should use.

In [ ]:
plot_options = dict(width=800, height=480, toolbar_location=None, outline_line_color=None)
In [ ]:
medal_glyph = Circle(x="MetersBack", y="Year", size=10, fill_color="MedalFill", 
                     line_color="MedalLine", fill_alpha=0.5)

athlete_glyph = Text(x="MetersBack", y="Year", x_offset=10, text="SelectedName",
                     text_align="left", text_baseline="middle", text_font_size="9pt")

no_olympics_glyph = Text(x=7.5, y=1942, text="No Olympics in 1940 or 1944",
                         text_align="center", text_baseline="middle",
                         text_font_size="9pt", text_font_style="italic", text_color="silver")
In [ ]:
xdr = Range1d(start=sprint.MetersBack.max()+2, end=0)  # +2 is for padding
ydr = DataRange1d(range_padding=0.05)  

plot = Plot(x_range=xdr, y_range=ydr, **plot_options)
plot.title.text = "Usain Bolt vs. 116 years of Olympic sprinters"
plot.add_glyph(source, medal_glyph)
plot.add_glyph(source, athlete_glyph)
plot.add_glyph(no_olympics_glyph)
In [ ]:
show(plot)

Adding Axes and Grids

Next we add in models for the Axis and Grids that we would like to see. Since we want to exert more control over the appearance, we can choose specific tickers for the axes models to use (SingleIntervalTicker in this case). We add these guides to the plot using the plot.add_layout method.

In [ ]:
from bokeh.models import Grid, LinearAxis, SingleIntervalTicker
In [ ]:
xdr = Range1d(start=sprint.MetersBack.max()+2, end=0)  # +2 is for padding
ydr = DataRange1d(range_padding=0.05)  

plot = Plot(x_range=xdr, y_range=ydr, **plot_options)
plot.title.text = "Usain Bolt vs. 116 years of Olympic sprinters"
plot.add_glyph(source, medal_glyph)
plot.add_glyph(source, athlete_glyph)
plot.add_glyph(no_olympics_glyph)
In [ ]:
xticker = SingleIntervalTicker(interval=5, num_minor_ticks=0)
xaxis = LinearAxis(ticker=xticker, axis_line_color=None, major_tick_line_color=None,
                   axis_label="Meters behind 2012 Bolt", axis_label_text_font_size="10pt", 
                   axis_label_text_font_style="bold")
plot.add_layout(xaxis, "below")

xgrid = Grid(dimension=0, ticker=xaxis.ticker, grid_line_dash="dashed")
plot.add_layout(xgrid)

yticker = SingleIntervalTicker(interval=12, num_minor_ticks=0)
yaxis = LinearAxis(ticker=yticker, major_tick_in=-5, major_tick_out=10)
plot.add_layout(yaxis, "right")
In [ ]:
show(plot)

Adding a Hover Tool

Finally we add a hover tool to display those extra columns that we put into our column data source. We use the template syntax for the tooltips, to have more control over the appearance. Tools can be added using the plot.add_tools method.

In [ ]:
from bokeh.models import HoverTool
In [ ]:
tooltips = """
<div>
    <span style="font-size: 15px;">@Name</span>&nbsp;
    <span style="font-size: 10px; color: #666;">(@Abbrev)</span>
</div>
<div>
    <span style="font-size: 17px; font-weight: bold;">@Time{0.00}</span>&nbsp;
    <span style="font-size: 10px; color: #666;">@Year</span>
</div>
<div style="font-size: 11px; color: #666;">@{MetersBack}{0.00} meters behind</div>
"""
In [ ]:
xdr = Range1d(start=sprint.MetersBack.max()+2, end=0)  # +2 is for padding
ydr = DataRange1d(range_padding=0.05)  

plot = Plot(x_range=xdr, y_range=ydr, **plot_options)
plot.title.text = "Usain Bolt vs. 116 years of Olympic sprinters"
medal = plot.add_glyph(source, medal_glyph)  # we need this renderer to configure the hover tool
plot.add_glyph(source, athlete_glyph)
plot.add_glyph(no_olympics_glyph)

xticker = SingleIntervalTicker(interval=5, num_minor_ticks=0)
xaxis = LinearAxis(ticker=xticker, axis_line_color=None, major_tick_line_color=None,
                   axis_label="Meters behind 2012 Bolt", axis_label_text_font_size="10pt", 
                   axis_label_text_font_style="bold")
plot.add_layout(xaxis, "below")

xgrid = Grid(dimension=0, ticker=xaxis.ticker, grid_line_dash="dashed")
plot.add_layout(xgrid)

yticker = SingleIntervalTicker(interval=12, num_minor_ticks=0)
yaxis = LinearAxis(ticker=yticker, major_tick_in=-5, major_tick_out=10)
plot.add_layout(yaxis, "right")
In [ ]:
hover = HoverTool(tooltips=tooltips, renderers=[medal])
plot.add_tools(hover)
In [ ]:
show(plot)

Exercises

Custom User Models

It is possible to extend the set of built-in Bokeh models with your own custom user models. The capability opens some valuable use-cases:

  • customizing existing Bokeh model behaviour
  • wrapping and connecting other JS libraries to Bokeh

With this capability, advanced users can try out new features or techniques easily, without having to set up a full Bokeh development environment.

The section gives a basi outline of a custom model starts with a JavaScript implementation, which subclasses an existing BokehJS model. Full details can be found in the Extending Bokeh chapter of the User's Guide.

Implement the JavaScript Model

In [ ]:
CODE = """
import {UIElement, UIElementView} from "models/ui/ui_element"
import {Slider} from "models/widgets/slider"
import {div} from "core/dom"
import * as p from "core/properties"

export class CustomView extends UIElementView {
  model: Custom

  private content_el: HTMLElement

  override connect_signals(): void {
    super.connect_signals()
    this.connect(this.model.slider.change, () => this._update_text())
  }

  override render(): void {
    // BokehJS views create <div> elements by default. These are accessible
    // as ``this.el``. Many Bokeh views ignore the default <div> and
    // instead do things like draw to the HTML canvas. In this case though,
    // the program changes the contents of the <div> based on the current
    // slider value.
    super.render()

    this.content_el = div({style: {
      textAlign: "center",
      fontSize: "1.2em",
      padding: "2px",
      color: "#b88d8e",
      backgroundColor: "#2a3153",
    }})
    this.shadow_el.appendChild(this.content_el)

    this._update_text()
  }

  private _update_text(): void {
    this.content_el.textContent = `${this.model.text}: ${this.model.slider.value}`
  }
}

export namespace Custom {
  export type Attrs = p.AttrsOf<Props>

  export type Props = UIElement.Props & {
    text: p.Property<string>
    slider: p.Property<Slider>
  }
}

export interface Custom extends Custom.Attrs {}

export class Custom extends UIElement {
  properties: Custom.Props
  __view_type__: CustomView

  constructor(attrs?: Partial<Custom.Attrs>) {
    super(attrs)
  }

  static {
    // If there is an associated view, this is typically boilerplate.
    this.prototype.default_view = CustomView

    // The this.define() block adds corresponding "properties" to the JS
    // model. These should normally line up 1-1 with the Python model
    // class. Most property types have counterparts. For example,
    // bokeh.core.properties.String will correspond to ``String`` in the
    // JS implementation. Where JS lacks a given type, you can use
    // ``p.Any`` as a "wildcard" property type.
    this.define<Custom.Props>(({String, Ref}) => ({
      text:   [ String, "Custom text" ],
      slider: [ Ref(Slider) ],
    }))
  }
}
"""

Define the Python Model

This JavaScript implementation is then attached to a corresponding Python Bokeh model:

In [ ]:
from bokeh.core.properties import Instance, Required, String

class Custom(UIElement):

    __implementation__ = TypeScript(CODE)

    text = String(default="Custom text")

    slider = Required(Instance(Slider))

Use the Python Model

Then the new model can be used seamlessly in the same way as any built-in Bokeh model:

In [ ]:
from bokeh.io import show, output_file
from bokeh.layouts import column
from bokeh.models import Slider

slider = Slider(start=0, end=10, step=0.1, value=0, title="value")

custom = Custom(text="Special Slider Display", slider=slider)

layout = column(slider, custom)

show(layout)

# Necessary to explicitly reload BokehJS to pick up new extension code
output_notebook()
In [ ]:
show(layout)

Next Section

To explore the next topic in the appendices, click on this link: A2 - Visualizing Big Data with Datashader

To go back to the overview, click here.