Annotations#

Bokeh includes several different types of annotations you can use to add supplemental information to your visualizations.

Titles#

Use Title annotations to add descriptive text which is rendered around the edges of a plot.

If you use the bokeh.plotting interface, the quickest way to add a basic title is to pass the text as the title parameter to figure():

from bokeh.plotting import figure, show

p = figure(title="Basic Title", width=300, height=300)

p.circle([1,2], [3,4])

show(p)

The default title is generally located above a plot, aligned to the left.

The title text may value contain newline characters which will result in a multi-line title.

p = figure(title="A longer title\nwith a second line underneath")

To define the placement of the title in relation to the plot, use the title_location parameter. A title can be located above, below, left, or right of a plot. For example:

from bokeh.plotting import figure, show

p = figure(title="Left Title", title_location="left",
           width=300, height=300)

p.circle([1,2], [3,4])

show(p)

Use your plot’s .title property to customize the default Title. Use the standard text properties to define visual properties such as font, border, and background.

This example uses the .title property to set the font and background properties as well as the title text and title alignment:

from bokeh.plotting import figure, show

p = figure(width=300, height=300)

p.circle([1,2], [3,4])

# configure visual properties on a plot's title attribute
p.title.text = "Title With Options"
p.title.align = "right"
p.title.text_color = "orange"
p.title.text_font_size = "25px"
p.title.background_fill_color = "#aaaaee"

show(p)

Note that the align property is relative to the direction of the text. For example: If you have placed your title on the left side of your plot, setting the align property to "left" means your text is rendered in the lower left corner.

To add more titles to your document, you need to create additional Title objects. Use the add_layout() method of your plot to include those additional Title objects in your document:

from bokeh.models import Title
from bokeh.plotting import figure, show

p = figure(title="Left Title", title_location="left",
           width=300, height=300)

p.circle([1,2], [3,4])

# add extra titles with add_layout(...)
p.add_layout(Title(text="Bottom Centered Title", align="center"), "below")

show(p)

If a title and a toolbar are placed on the same side of a plot, they will occupy the same space:

from bokeh.plotting import figure, show

p = figure(title="Top Title with Toolbar", toolbar_location="above",
           width=600, height=300)

p.circle([1,2], [3,4])

show(p)

If the plot size is large enough, this can result in a more compact plot. However, if the plot size is not large enough, the title and toolbar may visually overlap.

Legends#

The easiest way to add a legend to your plot is to include any of the legend_label, legend_group, or legend_field properties when calling glyph methods. Bokeh then creates a Legend object for you automatically.

For more advanced control over a plot’s legend, access the Legend object directly.

Basic legend label#

To provide a simple explicit label for a glyph, pass the legend_label keyword argument:

p.circle('x', 'y', legend_label="some label")

If you assign the same label name to multiple glyphs, all the glyphs will be combined into a single legend item with that label.

import numpy as np

from bokeh.plotting import figure, show

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

p = figure()

p.square(x, y, legend_label="sin(x)", size=3, line_color="green")
p.line(x, y, legend_label="sin(x)", line_color="green")

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

p.circle(x, 3*y, legend_label="3*sin(x)", size=7, fill_color=None)
p.line(x, 3*y, legend_label="3*sin(x)")

show(p)

Automatic grouping (Python-side)#

If your data is in the form of a ColumnDataSource, Bokeh can generate legend entries from strings in one of the ColumnDataSource’s columns. This way, you can create legend entries based on groups of glyphs.

To use data from a column of a ColumnDataSource to generate your plot’s legend, pass the column name as the legend_group keyword argument to a glyph method:

p.circle('x', 'y', legend_group="colname", source=source)

Because legend_group references a column of a ColumnDataSource, you need to always provide a source argument to the glyph method as well. Additionally, the column containing the label names has to be present in the data source at that point:

from bokeh.models import ColumnDataSource
from bokeh.plotting import figure, show

orange, blue = '#ef8a62', '#67a9cf'

source = ColumnDataSource(dict(
    x=[1, 2, 3, 4, 5, 6],
    y=[2, 1, 2, 1, 2, 1],
    color=[orange, blue, orange, blue, orange, blue],
    label=['hi', 'lo', 'hi', 'lo', 'hi', 'lo'],
))

p = figure(x_range=(0, 7), y_range=(0, 3), height=300, tools='save')

# legend field matches the column in the source
p.circle('x', 'y', radius=0.5, color='color',
         legend_group='label', source=source)

show(p)

Using legend_group means that Bokeh groups the legend entries immediately. Therefore, any subsequent Python code will be able to see the individual legend items in the Legend.items property. This way, you can re-arrange or modify the legend at any time.

Automatic grouping (browser-side)#

You also have the option to only group elements within your legend on the JavaScript side, in the browser. Using browser-side grouping makes sense if you want to group a column that is only computed on the JavaScript side, for example.

p.circle('x', 'y', legend_field="colname", source=source)

In this case, the Python code does not see multiple items in Legend.items. Instead, there is only a single item that represents the grouping, and the grouping happens in the browser.

from bokeh.models import ColumnDataSource
from bokeh.plotting import figure, show

orange, blue = '#ef8a62', '#67a9cf'

source = ColumnDataSource(dict(
    x=[1, 2, 3, 4, 5, 6],
    y=[2, 1, 2, 1, 2, 1],
    color=[orange, blue, orange, blue, orange, blue],
    label=['hi', 'lo', 'hi', 'lo', 'hi', 'lo'],
))

p = figure(x_range=(0, 7), y_range=(0, 3), height=300, tools='save')

# legend field matches the column in the source
p.circle('x', 'y', radius=0.5, color='color',
         legend_field='label', source=source)

show(p)

Hiding legend items#

To manually control the visibility of individual legend items, set the visible property of a LegendItem to either True or False.

import numpy as np

from bokeh.plotting import figure, show

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

p = figure(height=300)

# create two renderers with legend labels
p.circle(x, y, legend_label="cox(x)")
p.line(x, 2*y, legend_label="2*cos(x)",
       line_dash=[4, 4], line_color="orange", line_width=2)

# set legend label visibility for second renderer to False
p.legend.items[1].visible = False

show(p)

Note

If all items in a legend are invisible, the entire legend will be hidden. Also, if you use automatic grouping on the browser side and set the visibility of a legend_field item to False, the entire group will be invisible.

Two dimensional legends#

To get a legend with more than one column or row, it is possible to set the nrows or ncols property of the Legend with a positive integer. This can be useful if there is not enough space for all legend items in one line and can avoid a truncated legend. The definition of the rows and columns depends on the orientation of the legend.

import numpy as np

from bokeh.layouts import column
from bokeh.plotting import figure, show

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

p1 = figure(title='Default legend layout', width=500, height=300)
[p1.line(x, (1 + i/20)*sinx, legend_label=f"{1+i/20:.2f}*sin(x)") for i in range(7)]

p2 = figure(title='Legend layout with 2 columns', width=500, height=300)
[p2.line(x, (1 + i/20)*sinx, legend_label=f"{1+i/20:.2f}*sin(x)") for i in range(7)]
p2.legend.ncols=2

p3 = figure(title='Legend layout with 3 rows', width=500, height=300)
[p3.line(x, (1 + i/20)*sinx, legend_label=f"{1+i/20:.2f}*sin(x)") for i in range(7)]
p3.legend.nrows=3

show(column(p1, p2, p3))

Manual legends#

To build a legend by hand, don’t use any of the legend arguments and instead assign values to the various properties of a Legend object directly.

To see the source code which creates the figure above please visit the complete legend example single page.

Explicit index#

To explicitly specify which index into a ColumnDataSource to use in a legend, set the index property of a LegendItem.

This is useful for displaying multiple entries in a legend when you use glyphs that are rendered in several parts, such as MultiLine (multi_line()) or Patches patches():

from bokeh.models import Legend, LegendItem
from bokeh.plotting import figure, show

p = figure()

r = p.multi_line([[1,2,3], [1,2,3]], [[1,3,2], [3,4,3]],
                 color=["orange", "red"], line_width=4)

legend = Legend(items=[
    LegendItem(label="orange", renderers=[r], index=0),
    LegendItem(label="red", renderers=[r], index=1),
])
p.add_layout(legend)

show(p)

Interactive legends#

You can use legends as interactive elements to control some aspects of the appearance of your plot. Clicking or tapping on interactive legend entries controls the visibility of the glyphs associated with the legend entry.

See interactive legends in the user guide for more information and examples.

Note

The features of interactive legends currently only work on the basic legend labels described above. Legends that are created by specifying a column to automatically group do not yet support interactive features.

Color bars#

To create a ColorBar, you can pass an instance of ColorMapper containing a color palette, for example:

color_bar = ColorBar(color_mapper=color_mapper, padding=5)

However, for many glyphs, you can call construct_color_bar on the renderer returned by the glyph method to create a color bar automatically, if the glyph already has a color mapping configured:

color_bar = r.construct_color_bar(padding=5)

Color bars can be located inside as well as left, right, below, or above the plot. Specify the location of a color bar when adding the ColorBar object to the plot using the add_layout() method.

import numpy as np

from bokeh.models import LogColorMapper
from bokeh.plotting import figure, show


def normal2d(X, Y, sigx=1.0, sigy=1.0, mux=0.0, muy=0.0):
    z = (X-mux)**2 / sigx**2 + (Y-muy)**2 / sigy**2
    return np.exp(-z/2) / (2 * np.pi * sigx * sigy)

X, Y = np.mgrid[-3:3:200j, -2:2:200j]
Z = normal2d(X, Y, 0.1, 0.2, 1.0, 1.0) + 0.1*normal2d(X, Y, 1.0, 1.0)
image = Z * 1e6

color_mapper = LogColorMapper(palette="Viridis256", low=1, high=1e7)

plot = figure(x_range=(0,1), y_range=(0,1), toolbar_location=None)
r = plot.image(image=[image], color_mapper=color_mapper,
               dh=1.0, dw=1.0, x=0, y=0)

color_bar = r.construct_color_bar(padding=1)

plot.add_layout(color_bar, "right")

show(plot)

Arrows#

You can use Arrow annotations to connect glyphs and label annotations. Arrows can also help highlight plot regions.

Arrows are compound annotations. This means that they use additional ArrowHead objects as their start and end. By default, the Arrow annotation is a one-sided arrow: The end property is set to an OpenHead-type arrowhead (looking like an open-backed wedge style) and the start property is set to None. If you want to create double-sided arrows, set both the start and end properties to one of the available arrowheads.

The available arrowheads are:

Control the appearance of an arrowhead with these properties:

  • use the size property to control the size of any arrowheads

  • use the standard line properties such as line_color and line_alpha to control the appearance of the outline of the arrowhead.

  • use fill_color and fill_alpha to control the appearance of the arrowhead’s inner surface, if applicable.

Arrow objects themselves have the standard line properties. Set those properties to control the color and appearance of the arrow shaft. For example:

my_arrow.line_color = "blue"
my_arrow.line_alpha = 0.6

Optionally, you can set the x_range and y_range properties to make an arrow annotation refer to additional non-default x- or y-ranges. This works the same as Twin axes.

from bokeh.models import Arrow, NormalHead, OpenHead, VeeHead
from bokeh.palettes import Muted3 as color
from bokeh.plotting import figure, show

p = figure(tools="", toolbar_location=None, background_fill_color="#efefef")
p.grid.grid_line_color = None

p.circle(x=(0, 1, 0.5), y=(0, 0, 0.7), radius=0.1, color="#fafafa")

vh = VeeHead(size=35, fill_color=color[0])
p.add_layout(Arrow(end=vh, x_start=0.5, y_start=0.7, x_end=0, y_end=0))

nh = NormalHead(fill_color=color[1], fill_alpha=0.5, line_color=color[1])
p.add_layout(Arrow(end=nh, line_color=color[1], line_dash=[15, 5],
                   x_start=1, y_start=0, x_end=0.5, y_end=0.7))

oh = OpenHead(line_color=color[2], line_width=5)
p.add_layout(Arrow(end=oh, line_color=color[2], line_width=5,
                   x_start=0, y_start=0, x_end=1, y_end=0))

show(p)

Bands#

A Band annotation is a colored stripe that is dimensionally linked to the data in a plot. One common use for the band annotation is to indicate uncertainty related to a series of measurements.

To define a band, use either screen units or data units.

import numpy as np
import pandas as pd

from bokeh.models import Band, ColumnDataSource
from bokeh.plotting import figure, show

# Create some random data
x = np.random.random(2500) * 140 +20
y = np.random.normal(size=2500) * 2 + 6 * np.log(x)

df = pd.DataFrame(data=dict(x=x, y=y)).sort_values(by="x")

df2 = df.y.rolling(window=300).agg({"y_mean": np.mean, "y_std": np.std})

df = pd.concat([df, df2], axis=1)
df["lower"] = df.y_mean - df.y_std
df["upper"] = df.y_mean + df.y_std

source = ColumnDataSource(df.reset_index())

p = figure(tools="", toolbar_location=None, x_range=(40, 160))
p.title.text = "Rolling Standard Deviation"
p.xgrid.grid_line_color=None
p.ygrid.grid_line_alpha=0.5

p.scatter(x="x", y="y", color="blue", marker="dot", size=10, alpha=0.4, source=source)

p.line("x", "y_mean", line_dash=(10, 7), line_width=2, source=source)

band = Band(base="x", lower="lower", upper="upper", source=source,
            fill_alpha=0.3, fill_color="yellow", line_color="black")
p.add_layout(band)

show(p)

Box annotations#

A BoxAnnotation is a rectangular box that you can use to highlight specific plot regions. Use either screen units or data units to position a box annotation.

To define the bounds of these boxes, use the left/right or top/ bottom properties. If you provide only one bound (for example, a left value but no right value), the box will extend to the edge of the available plot area for the dimension you did not specify.

from bokeh.models import BoxAnnotation
from bokeh.plotting import figure, show
from bokeh.sampledata.glucose import data

TOOLS = "pan,wheel_zoom,box_zoom,reset,save"

#reduce data size
data = data.loc['2010-10-06':'2010-10-13'].reset_index()

p = figure(x_axis_type="datetime", tools=TOOLS)

p.line("datetime", "glucose", source=data, color="gray", legend_label="glucose")

low_box = BoxAnnotation(top=80, fill_alpha=0.2, fill_color='#D55E00')
mid_box = BoxAnnotation(bottom=80, top=180, fill_alpha=0.2, fill_color='#0072B2')
high_box = BoxAnnotation(bottom=180, fill_alpha=0.2, fill_color='#D55E00')

p.add_layout(low_box)
p.add_layout(mid_box)
p.add_layout(high_box)

p.title.text = "Glucose Range"
p.xgrid.grid_line_color=None
p.ygrid.grid_line_alpha=0.5
p.xaxis.axis_label = 'Time'
p.yaxis.axis_label = 'Value'
p.legend.level = "overlay"
p.legend.location = "top_left"

show(p)

Polygon annotations#

A PolyAnnotation is a polygon with vertices in either screen units or data units.

To define the polygon’s vertices, supply a series of coordinates to the xs and ys properties. Bokeh automatically connects the last vertex to the first to create a closed shape.

from datetime import datetime as dt

import pandas as pd

from bokeh.models import PolyAnnotation
from bokeh.plotting import figure, show
from bokeh.sampledata.stocks import GOOG

p = figure(height=200, x_axis_type="datetime",
           background_fill_color="#efefef", title="Google stock")

df = pd.DataFrame(GOOG)
df["date"] = pd.to_datetime(df["date"])

p.line(df["date"], df["close"], line_width=1.5, color="grey")

start_date = dt(2008, 11, 24)
start_y = df.loc[df["date"] == start_date]["close"].values[0]

end_date = dt(2010, 1, 4)
end_y = df.loc[df["date"] == end_date]["close"].values[0]

polygon = PolyAnnotation(
    fill_color="blue", fill_alpha=0.2,
    xs=[start_date, start_date, end_date, end_date],
    ys=[start_y - 100, start_y + 100, end_y + 100, end_y - 100],
)
p.add_layout(polygon)

show(p)

Labels#

Labels are rectangular boxes with additional information about glyphs or plot regions.

To create a single text label, use the Label annotation. Those are the most important properties for this annotation:

  • A text property containing the text to display inside the label.

  • x and y properties to set the position (in screen units or data units).

  • x_offset and y_offset properties to specify where to place the label in relation to its x and y coordinates.

  • The standard text properties as well as other styling parameters such as

  • border_line and background_fill properties.

Label(x=100, y=5, x_units='screen', text='Some Stuff',
      border_line_color='black', border_line_alpha=1.0,
      background_fill_color='white', background_fill_alpha=1.0)

The text may value contain newline characters which will result in a multi-line label.

Label(x=100, y=5, text='A very long label\nwith mutiple lines')

To create several labels at once, use the LabelSet annotation. To configure the labels of a label set, use a data source that contains columns with data for the labels’ properties such as text, x and y. If you assign a value to a property such as x_offset and y_offset directly instead of a column name, this value is used for all labels of the label set.

LabelSet(x='x', y='y', text='names',
         x_offset=5, y_offset=5, source=source)

The following example illustrates the use of Label and LabelSet:

from bokeh.models import ColumnDataSource, Label, LabelSet
from bokeh.plotting import figure, show

source = ColumnDataSource(data=dict(
    height=[66, 71, 72, 68, 58, 62],
    weight=[165, 189, 220, 141, 260, 174],
    names=['Mark', 'Amir', 'Matt', 'Greg', 'Owen', 'Juan'],
))

p = figure(title='Dist. of 10th Grade Students', x_range=(140, 275))
p.xaxis.axis_label = 'Weight (lbs)'
p.yaxis.axis_label = 'Height (in)'

p.scatter(x='weight', y='height', size=8, source=source)

labels = LabelSet(x='weight', y='height', text='names',
                  x_offset=5, y_offset=5, source=source)

citation = Label(x=70, y=70, x_units='screen', y_units='screen',
                 text='Collected by Luke C. 2016-04-01',
                 border_line_color='black', background_fill_color='white')

p.add_layout(labels)
p.add_layout(citation)

show(p)

The text values for LabelSet may value contain newline characters which will result in multi-line labels.

Slopes#

Slope annotations are lines that can go from one edge of the plot to another at a specific angle.

These are the most commonly used properties for this annotation:

import numpy as np

from bokeh.models import Slope
from bokeh.palettes import Sunset10
from bokeh.plotting import figure, show

# linear equation parameters
slope, intercept = 2, 10

xpts = np.arange(0, 20, 0.2)
ypts = slope * xpts + intercept + np.random.normal(0, 4, 100)

blue, yellow = Sunset10[0], Sunset10[5]

p = figure(width=600, height=600, x_axis_label='x', y_axis_label='y',
           background_fill_color="#fafafa")
p.y_range.start = 0

p.circle(xpts, ypts, size=8, alpha=0.8, fill_color=yellow, line_color="black")

slope = Slope(gradient=slope, y_intercept=intercept,
              line_color=blue, line_dash='dashed', line_width=4)

p.add_layout(slope)

show(p)

Spans#

Span annotations are lines that are orthogonal to the x or y axis of a plot. They have a single dimension (width or height) and go from one edge of the plot area to the opposite edge.

These are the most commonly used properties for this annotation:

  • dimension: The direction of the span line. The direction can be one of these two values: Either * "height" for a line that is parallel to the plot’s x axis. Or "width" for a line that is parallel to the plot’s y axis.

  • location: The location of the span along the axis specified with dimension.

  • location_units: The unit type for the location property. The default is to use data units.

  • The standard line properties.

from datetime import datetime as dt

from bokeh.models import Span
from bokeh.plotting import figure, show
from bokeh.sampledata.daylight import daylight_warsaw_2013

p = figure(height=350, x_axis_type="datetime", y_axis_type="datetime",
           title="2013 Sunrise and Sunset in Warsaw with DST dates marked",
           y_axis_label="Time of Day", background_fill_color="#fafafa")
p.y_range.start = 0
p.y_range.end = 24 * 60 * 60 * 1000

p.line("Date", "Sunset", source=daylight_warsaw_2013,
       color='navy', line_dash="dotted", line_width=2, legend_label="Sunset")
p.line("Date", "Sunrise", source=daylight_warsaw_2013,
       color='orange', line_dash="dashed", line_width=2, legend_label="Sunrise")

dst_start = Span(location=dt(2013, 3, 31, 2, 0, 0), dimension='height',
                 line_color='#009E73', line_width=5)
p.add_layout(dst_start)

dst_end = Span(location=dt(2013, 10, 27, 3, 0, 0), dimension='height',
               line_color='#009E73', line_width=5)
p.add_layout(dst_end)

p.yaxis.formatter.days = "%Hh"
p.xgrid.grid_line_color = None

show(p)

Whiskers#

A Whisker annotation is a “stem” that is dimensionally linked to the data in the plot. You can define this annotation using data units or screen units.

A common use for whisker annotations is to indicate error margins or uncertainty for measurements at a single point.

These are the most commonly used properties for this annotation:

  • lower: The coordinates of the lower end of the whisker.

  • upper: The coordinates of the upper end of the whisker.

  • dimension: The direction of the whisker. The direction can be one of these two values: Either * "width" for whiskers that are parallel to the plot’s x axis. Or "height" for whiskers that are parallel to the plot’s y axis.

  • base: The location of the whisker along the dimension specified with dimension.

  • The standard line properties.

from bokeh.models import ColumnDataSource, Whisker
from bokeh.plotting import figure, show
from bokeh.sampledata.autompg2 import autompg2 as df
from bokeh.transform import factor_cmap, jitter

classes = list(sorted(df["class"].unique()))

p = figure(height=400, x_range=classes, background_fill_color="#efefef",
           title="Car class vs HWY mpg with quantile ranges")
p.xgrid.grid_line_color = None

g = df.groupby("class")
upper = g.hwy.quantile(0.80)
lower = g.hwy.quantile(0.20)
source = ColumnDataSource(data=dict(base=classes, upper=upper, lower=lower))

error = Whisker(base="base", upper="upper", lower="lower", source=source,
                level="annotation", line_width=2)
error.upper_head.size=20
error.lower_head.size=20
p.add_layout(error)

p.circle(jitter("class", 0.3, range=p.x_range), "hwy", source=df,
         alpha=0.5, size=13, line_color="white",
         color=factor_cmap("class", "Light7", classes))

show(p)