Annotations

Overview

We can add visual cues (boundary lines, shaded areas, labels and arrows, etc.) to our plots to emphasise certain features. Bokeh offers several annotation types for this purpose. To add annotations, we usually create a low-level object and add it to our diagram using add_layout. Let’s look at some concrete examples:

Spans

Spans are vertical or horizontal lines for which the spans between the dimensions can be specified, for example:

[1]:
from bokeh.io import output_notebook, show
from bokeh.plotting import figure


output_notebook()
Loading BokehJS ...
[2]:
import numpy as np

from bokeh.models.annotations import Span


x = np.linspace(0, 20, 200)
y = np.sin(x)

p = figure(y_range=(-2, 2))
p.line(x, y)

upper = Span(location=1, dimension="width", line_color="olive", line_width=4)
p.add_layout(upper)

lower = Span(
    location=-1, dimension="width", line_color="firebrick", line_width=4
)
p.add_layout(lower)

show(p)

Box annotations

If an area of a diagram is to be highlighted, this can be done with BoxAnnotation, the coordinate properties

  • top

  • left

  • bottom

  • right

as well as line and fill properties to configure the appearance.

Infinite boxes can be created by not specifying the coordinates. For example, if top is not specified, the box is always displayed up to the top edge of the plot area, regardless of whether panning or zooming takes place, for example:

[3]:
import numpy as np

from bokeh.models.annotations import BoxAnnotation


x = np.linspace(0, 20, 200)
y = np.sin(x)

p = figure(y_range=(-2, 2))
p.line(x, y)

# region that always fills the top of the plot
upper = BoxAnnotation(bottom=1, fill_alpha=0.1, fill_color="olive")
p.add_layout(upper)

# region that always fills the bottom of the plot
lower = BoxAnnotation(top=-1, fill_alpha=0.1, fill_color="firebrick")
p.add_layout(lower)

# a finite region
center = BoxAnnotation(
    top=0.6, bottom=-0.3, left=7, right=12, fill_alpha=0.1, fill_color="navy"
)
p.add_layout(center)

show(p)

Label

With the Label annotation, we can easily attach individual labels to plots. The position to be displayed and the text are configured as x, y and text:

Label(x=10, y=5, text="Some Label")

By default, the units are in the data space, but x_units and y_units can be set to screen to position the label relative to the drawing area, and labels can be positioned with x_offset and y_offset.

Label objects also have standard text, line (border_line) and fill (background_fill) properties. The line and fill properties apply to a bounding box around the text:

Label(x=10, y=5, text="Some Label", text_font_size="12pt",
      border_line_color="red", background_fill_color="blue")
[4]:
from bokeh.models.annotations import Label
from bokeh.plotting import figure

p = figure(x_range=(0,10), y_range=(0,10))
p.circle([2, 5, 8], [4, 7, 6], color="olive", size=10)

label = Label(x=5, y=7, x_offset=12, text="Second Point", text_baseline="middle")
p.add_layout(label)

show(p)

LabelSet

With the LabelSet annotation, you can create many labels at the same time, for example if you want to label a whole set of scatter markers. They are similar to Label, but can also use a ColumnDataSource as a source property, where x and y refer to columns in the data source.

[5]:
from bokeh.models import ColumnDataSource, LabelSet
from bokeh.plotting import figure


source = ColumnDataSource(
    data=dict(
        temp=[166, 171, 172, 168, 174, 162],
        pressure=[165, 189, 220, 141, 260, 174],
        names=["A", "B", "C", "D", "E", "F"],
    )
)

p = figure(x_range=(160, 175))
p.scatter(x="temp", y="pressure", size=8, source=source)
p.xaxis.axis_label = "Temperature (C)"
p.yaxis.axis_label = "Pressure (lbs)"

labels = LabelSet(
    x="temp",
    y="pressure",
    text="names",
    level="glyph",
    x_offset=5,
    y_offset=5,
    source=source,
)


p.add_layout(labels)

show(p)

Arrows

You can use the Arrow annotation to point to various elements in your drawing. This can be particularly helpful for labelling.

For example, you can create an arrow that points from (0,0) to (1,1):

p.add_layout(Arrow(x_start=0, y_start=0, x_end=1, y_end=0))

This arrow has the standard OpenHead tip. Other types of arrowheads are NormalHead and VeeHead. The type of arrowhead can be configured using the start and end properties of Arrow objects:

p.add_layout(Arrow(start=OpenHead(), end=VeeHead(),
             x_start=0, y_start=0, x_end=1, y_end=0))

This creates a double arrow with OpenHead and VeeHead. Arrowheads also have the standard set of line and fill properties to configure their appearance, for example

OpenHead(line_color="firebrick", line_width=4)

The code and diagram below show several of these configurations together.

[6]:
from bokeh.models.annotations import Arrow
from bokeh.models.arrow_heads import NormalHead, OpenHead, VeeHead


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

p.circle(
    x=[0, 1, 0.5],
    y=[0, 0, 0.7],
    radius=0.1,
    color=["navy", "yellow", "red"],
    fill_alpha=0.1,
)

p.add_layout(
    Arrow(
        end=OpenHead(line_color="firebrick", line_width=4),
        x_start=0,
        y_start=0,
        x_end=1,
        y_end=0,
    )
)

p.add_layout(
    Arrow(
        end=NormalHead(fill_color="orange"),
        x_start=1,
        y_start=0,
        x_end=0.5,
        y_end=0.7,
    )
)

p.add_layout(
    Arrow(
        end=VeeHead(size=35),
        line_color="red",
        x_start=0.5,
        y_start=0.7,
        x_end=0,
        y_end=0,
    )
)

show(p)

Legends

When diagrams contain multiple glyphs, it is desirable to add a legend to help viewers interpret them. Bokeh can easily generate legends based on the added glyphs.

Simple legends

In the simplest case, you can pass a string as a legend to a glyph function:

p.circle(x, y, legend="sin(x)")

In this case, Bokeh automatically creates a legend that shows a representation of this glyph.

[7]:
import numpy as np


x = np.linspace(0, 4 * np.pi, 100)
y = np.sin(x)

p = figure(height=400)

p.circle(x, y, legend_label="sin(x)")
p.line(
    x,
    2 * y,
    legend_label="2*sin(x)",
    line_dash=[4, 4],
    line_color="orange",
    line_width=2,
)

show(p)

Composite legends

In the example above, we have provided a different label for each glyph method. Sometimes two (or more) different glyphs are used with a single data source. In this case, composite legends can be created. For example, if you plot a sin curve with a line and a marker, you can give them the same label to make them appear together in the legend:

p.circle(x, y, legend="sin(x)")
p.line(x, y, legend="sin(x)", line_dash=[4, 4], line_color="orange", line_width=2)

Colour bars

Colour bars are particularly useful when we vary the colour of a glyph according to the colour mapping. Bokeh colour bars are configured with a colour mapping and added to plots using the add_layout method:

color_mapper = LinearColorMapper(palette="Viridis256", low=data_low, high=data_high)
color_bar = ColorBar(color_mapper=color_mapper, location=(0,0))
p.add_layout(color_bar, 'right')

In the following example, the colour mapping is also used to convert the glyph colour.

[8]:
from bokeh.models import ColorBar, LinearColorMapper
from bokeh.sampledata.autompg import autompg
from bokeh.transform import transform


source = ColumnDataSource(autompg)
color_mapper = LinearColorMapper(
    palette="Viridis256", low=autompg.weight.min(), high=autompg.weight.max()
)

p = figure(
    x_axis_label="Horsepower",
    y_axis_label="MPG",
    tools="",
    toolbar_location=None,
)
p.circle(
    x="hp",
    y="mpg",
    color=transform("weight", color_mapper),
    size=20,
    alpha=0.6,
    source=autompg,
)

color_bar = ColorBar(
    color_mapper=color_mapper,
    label_standoff=12,
    location=(0, 0),
    title="Weight",
)
p.add_layout(color_bar, "right")

show(p)