Adding Annotations¶
Bokeh includes several different types of annotations to allow users to add supplemental information to their visualizations.
Titles¶
Title
annotations allow descriptive text to be rendered around the edges
of a plot.
When using bokeh.plotting
or bokeh.Charts
, the quickest way to add
a basic title is to pass the text as the title
parameter to Figure
or
any Chart function:
from bokeh.plotting import figure, output_file, show
p = figure(title="Basic Title", plot_width=300, plot_height=300)
p.circle([1,2], [3,4])
output_file("title.html")
show(p)
The default title is normally on the top of a plot, aligned to the left. But
which side of the plot the default title appears on can be controlled by the
title_location
parameter:
from bokeh.plotting import figure, output_file, show
p = figure(title="Left Title", title_location="left",
plot_width=300, plot_height=300)
p.circle([1,2], [3,4])
output_file("title.html")
show(p)
The default Title
is accessible through the Plot.title
property.
Visual properties for font, border, background, and others can be set
directly on .title
. Here is an example that sets font and background
properties as well as the title text and title alignment using .title
:
from bokeh.plotting import figure, output_file, show
p = figure(plot_width=300, plot_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"
output_file("title.html")
show(p)
Note that the alignment is measured along the direction of text. For example for titles on the left side of a plot “left” will be in the lower corner.
In addition to the default title, it is possible to create and add
additional Title
objects to plots using the add_layout
method
of Plots:
from bokeh.models import Title
from bokeh.plotting import figure, output_file, show
p = figure(title="Left Title", title_location="left",
plot_width=300, plot_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")
output_file("title.html")
show(p)
If a title and a sticky toolbar are set to the same side, they will occupy the same space:
from bokeh.plotting import figure, output_file, show
p = figure(title="Top Title with Toolbar", toolbar_location="above",
plot_width=600, plot_height=300)
p.circle([1,2], [3,4])
output_file("title.html")
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 in way that is not desirable.
Legends¶
It is possible to create Legend
annotations easily by specifying legend
arguments to the glyph methods, when creating a plot.
Basic Legend Label¶
To provide a simple explicit label for a glypy, pass the legend_label
keyword argument:
p.circle('x', 'y', legend_label="some label")
If multiple glyphs are given the same label, they will all be combined in to a single legend item with that label.
import numpy as np
from bokeh.plotting import figure, output_file, show
x = np.linspace(0, 4*np.pi, 100)
y = np.sin(x)
output_file("legend.html")
p = figure()
p.circle(x, y, legend_label="sin(x)")
p.line(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)
p.square(x, 3*y, legend_label="3*sin(x)", fill_color=None, line_color="green")
p.line(x, 3*y, legend_label="3*sin(x)", line_color="green")
show(p)
Automatic Grouping (Python)¶
It is often desirable to generate multiple legend items by grouping the values
in a data source column. It is possible for Bokeh to perform such a grouping by
passing the legend_group
keyword argument to a glyph method:
p.circle('x', 'y', legend_group="colname", source=source)
When this method is used, the grouping is performed immediately in Python, and
subsequent Python code will be able to see the individual legend items in
Legend.items
property. If desired, these items can be re-arranged or modified.
from bokeh.io import show
from bokeh.models import ColumnDataSource
from bokeh.palettes import RdBu3
from bokeh.plotting import figure
c1 = RdBu3[2] # red
c2 = RdBu3[0] # blue
source = ColumnDataSource(dict(
x=[1, 2, 3, 4, 5, 6],
y=[2, 1, 2, 1, 2, 1],
color=[c1, c2, c1, c2, c1, c2],
label=['hi', 'lo', 'hi', 'lo', 'hi', 'lo']
))
p = figure(x_range=(0, 7), y_range=(0, 3), plot_height=300, tools='save')
# legend field matches the column in the source
p.circle( x='x', y='y', radius=0.5, color='color', legend_group='label', source=source)
show(p)
Note
To use this feature, a source
argument must also be provided to the
glyph method. Additionally, the column to be grouped must already be present
in the data source at that point.
Automatic Grouping (Browser)¶
It is also possible to specify that the grouping should happen on the JavaScript side, in the browser. This may be desirable, e.g. if the grouping should happen on a column that is only computed on the JavaScript side.
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 to perform in
the browser.
from bokeh.io import show
from bokeh.models import ColumnDataSource
from bokeh.palettes import RdBu3
from bokeh.plotting import figure
c1 = RdBu3[2] # red
c2 = RdBu3[0] # blue
source = ColumnDataSource(dict(
x=[1, 2, 3, 4, 5, 6],
y=[2, 1, 2, 1, 2, 1],
color=[c1, c2, c1, c2, c1, c2],
label=['hi', 'lo', 'hi', 'lo', 'hi', 'lo']
))
p = figure(x_range=(0, 7), y_range=(0, 3), plot_height=300, tools='save')
# legend field matches the column in the source
p.circle( x='x', y='y', radius=0.5, color='color', legend_field='label', source=source)
show(p)
Manual Legends¶
It is also possible to not specify any of the legend arguments, and manually
build a Legend
by hand. An example of this
can be found in examples/models/file/legends.py:
Explicit Index¶
Other times, it may be useful to explicitly tell Bokeh which index into a
ColumnDataSource
should be used when drawing a legend item. In particular,
if you want to draw multiple legend items for “multi” glyphs such as
MultiLine
or Patches
. This is accomplished by specifying an index
for the legend item as shown below.
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¶
It’s also possible to configure legends to be interactive, so that clicking or tapping on legend entries affects the corresponding glyph visibility. See the Interactive Legends section of the User’s Guide for more information and examples.
Note
Interactive Legends features currently work on the first, “per-glyph” style legends. Legends that are created by specifying a column to automatically group do no yet support interactive features.
Color Bars¶
A ColorBar
can be created using a ColorMapper
instance, which
contains a color palette. Both on- and off-plot color bars are
supported; the desired location can be specified when adding the
ColorBar
to the plot.
Note
This example depends on the open-source NumPy library in order to generate demonstration data.
import numpy as np
from bokeh.models import ColorBar, LogColorMapper, LogTicker
from bokeh.plotting import figure, output_file, show
output_file('color_bar.html')
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:100j, -2:2:100j]
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)
plot.image(image=[image], color_mapper=color_mapper,
dh=[1.0], dw=[1.0], x=[0], y=[0])
color_bar = ColorBar(color_mapper=color_mapper, ticker=LogTicker(),
label_standoff=12, border_line_color=None, location=(0,0))
plot.add_layout(color_bar, 'right')
show(plot)
Arrows¶
Arrow
annotations can be used to connect glyphs and label annotations or
to simply highlight plot regions. Arrows are compound annotations, meaning
that their start
and end
attributes are themselves other ArrowHead
annotations. By default, the Arrow
annotation is one-sided with the end
set as an OpenHead
-type arrow head (an open-backed wedge style) and the
start
property set to None
. Double-sided arrows can be created by
setting both the start
and end
properties as appropriate ArrowHead
subclass instances.
Arrows have standard line properties to set the color and appearance of the arrow shaft:
my_arrow.line_color = "blue"
my_arrow.line_alpha = 0.6
Arrows may also be configured to refer to additional non-default x- or
y-ranges with the x_range
and y_range
properties, in the same way
as Twin Axes.
Additionally any arrow head objects in start
or end
have a size
property to control how big the arrow head is, as well as both line and
fill properties. The line properties control the outline of the arrow head,
and the fill properties control the interior of the arrow head (if applicable).
from bokeh.models import Arrow, NormalHead, OpenHead, VeeHead
from bokeh.plotting import figure, output_file, show
output_file("arrow.html", title="arrow.py example")
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)
Bands¶
A Band
will create a dimensionally-linked “stripe”, either located in data
or screen coordinates. One common use for the Band annotation is to indicate
uncertainty related to a series of measurements.
import numpy as np
import pandas as pd
from bokeh.models import Band, ColumnDataSource
from bokeh.plotting import figure, output_file, show
output_file("band.html", title="band.py example")
# Create some random data
x = np.random.random(2500) * 140 - 20
y = np.random.normal(size=2500) * 2 + 5
df = pd.DataFrame(data=dict(x=x, y=y)).sort_values(by="x")
sem = lambda x: x.std() / np.sqrt(x.size)
df2 = df.y.rolling(window=100).agg({"y_mean": np.mean, "y_std": np.std, "y_sem": sem})
df2 = df2.fillna(method='bfill')
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())
TOOLS = "pan,wheel_zoom,box_zoom,reset,save"
p = figure(tools=TOOLS)
p.scatter(x='x', y='y', line_color=None, fill_alpha=0.3, size=5, source=source)
band = Band(base='x', lower='lower', upper='upper', source=source, level='underlay',
fill_alpha=1.0, line_width=1, line_color='black')
p.add_layout(band)
p.title.text = "Rolling Standard Deviation"
p.xgrid[0].grid_line_color=None
p.ygrid[0].grid_line_alpha=0.5
p.xaxis.axis_label = 'X'
p.yaxis.axis_label = 'Y'
show(p)
Box Annotations¶
A BoxAnnotation
can be linked to either data or screen coordinates in order
to emphasize specific plot regions. By default, box annotation dimensions (e.g.
left
or top
) default will extend the annotation to the edge of the
plot area.
from bokeh.models import BoxAnnotation
from bokeh.plotting import figure, output_file, show
from bokeh.sampledata.glucose import data
output_file("box_annotation.html", title="box_annotation.py example")
TOOLS = "pan,wheel_zoom,box_zoom,reset,save"
#reduce data size
data = data.loc['2010-10-06':'2010-10-13']
p = figure(x_axis_type="datetime", tools=TOOLS)
p.line(data.index.to_series(), data['glucose'], line_color="gray", line_width=1, legend_label="glucose")
low_box = BoxAnnotation(top=80, fill_alpha=0.1, fill_color='red')
mid_box = BoxAnnotation(bottom=80, top=180, fill_alpha=0.1, fill_color='green')
high_box = BoxAnnotation(bottom=180, fill_alpha=0.1, fill_color='red')
p.add_layout(low_box)
p.add_layout(mid_box)
p.add_layout(high_box)
p.title.text = "Glucose Range"
p.xgrid[0].grid_line_color=None
p.ygrid[0].grid_line_alpha=0.5
p.xaxis.axis_label = 'Time'
p.yaxis.axis_label = 'Value'
show(p)
Labels¶
Labels are text elements that can be used to annotate either glyphs or plot regions.
To create a single text label, use the Label
annotation. This annotation
is configured with a text
property containing the text to be displayed,
as well as x
and y
properties to set the position (in screen or data
space units). Additionally a render mode "canvas"
or "css"
may be
specified. Finally, labels have text
, border_line
, and
background_fill
properties. These control the visual appearance of the
text, as well as the border and background of the bounding box for the text:
Label(x=70, y=70, x_units='screen', text='Some Stuff', render_mode='css',
border_line_color='black', border_line_alpha=1.0,
background_fill_color='white', background_fill_alpha=1.0)
To create several labels at once, possibly to easily annotate another existing
glyph, use the LabelSet
annotation, which is configured with a data
source, with the text
and x
and y
positions are given as column
names. LabelSet
objects can also have x_offset
and y_offset
,
which specify a distance in screen space units to offset the label positions
from x
and y
. Finally the render level may be controlled with the
level
property, to place the label above or underneath other renderers:
LabelSet(x='x', y='y', text='names', level='glyph',
x_offset=5, y_offset=5, source=source)
The following example illustrates the use of both:
from bokeh.models import ColumnDataSource, Label, LabelSet, Range1d
from bokeh.plotting import figure, output_file, show
output_file("label.html", title="label.py example")
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 at Lee High',
x_range=Range1d(140, 275))
p.scatter(x='weight', y='height', size=8, source=source)
p.xaxis[0].axis_label = 'Weight (lbs)'
p.yaxis[0].axis_label = 'Height (in)'
labels = LabelSet(x='weight', y='height', text='names', level='glyph',
x_offset=5, y_offset=5, source=source, render_mode='canvas')
citation = Label(x=70, y=70, x_units='screen', y_units='screen',
text='Collected by Luke C. 2016-04-01', render_mode='css',
border_line_color='black', border_line_alpha=1.0,
background_fill_color='white', background_fill_alpha=1.0)
p.add_layout(labels)
p.add_layout(citation)
show(p)
Slopes¶
Slope
annotations are lines which may be sloped and extend to the
edge of the plot area.
import numpy as np
from bokeh.models import Slope
from bokeh.plotting import figure, output_file, show
output_file("slope.html", title="slope.py example")
# linear equation parameters
gradient = 2
y_intercept = 10
# create random data
xpts = np.arange(0, 20)
ypts = gradient * xpts + y_intercept + np.random.normal(0, 4, 20)
p = figure(plot_width=450, plot_height=450, y_range=(0, 1.1 * max(ypts)))
p.circle(xpts, ypts, size=5, color="skyblue")
slope = Slope(gradient=gradient, y_intercept=y_intercept,
line_color='orange', line_dash='dashed', line_width=3.5)
p.add_layout(slope)
p.yaxis.axis_label = 'y'
p.xaxis.axis_label = 'x'
show(p)
Spans¶
Span
annotations are lines that have a single dimension (width or height)
and extend to the edge of the plot area.
import time
from datetime import datetime as dt
from bokeh.models import Span
from bokeh.plotting import figure, output_file, show
from bokeh.sampledata.daylight import daylight_warsaw_2013
output_file("span.html", title="span.py example")
p = figure(x_axis_type="datetime", y_axis_type="datetime")
p.line(daylight_warsaw_2013.Date, daylight_warsaw_2013.Sunset,
line_dash='solid', line_width=2, legend_label="Sunset")
p.line(daylight_warsaw_2013.Date, daylight_warsaw_2013.Sunrise,
line_dash='dotted', line_width=2, legend_label="Sunrise")
start_date = time.mktime(dt(2013, 3, 31, 2, 0, 0).timetuple())*1000
daylight_savings_start = Span(location=start_date,
dimension='height', line_color='green',
line_dash='dashed', line_width=3)
p.add_layout(daylight_savings_start)
end_date = time.mktime(dt(2013, 10, 27, 3, 0, 0).timetuple())*1000
daylight_savings_end = Span(location=end_date,
dimension='height', line_color='red',
line_dash='dashed', line_width=3)
p.add_layout(daylight_savings_end)
p.title.text = "2013 Sunrise and Sunset times in Warsaw"
p.yaxis.axis_label = 'Time of Day'
show(p)
Whiskers¶
A Whisker
will create a dimensionally-linked “stem”, either located in data
or screen coordinates. Indicating error or uncertainty for measurements at a
single point would be one common use for the Whisker annotation.
from bokeh.models import ColumnDataSource, Whisker
from bokeh.plotting import figure, show
from bokeh.sampledata.autompg import autompg as df
colors = ["red", "olive", "darkred", "goldenrod", "skyblue", "orange", "salmon"]
p = figure(plot_width=600, plot_height=300, title="Years vs mpg with Quartile Ranges")
base, lower, upper = [], [], []
for i, year in enumerate(list(df.yr.unique())):
year_mpgs = df[df['yr'] == year]['mpg']
mpgs_mean = year_mpgs.mean()
mpgs_std = year_mpgs.std()
lower.append(mpgs_mean - mpgs_std)
upper.append(mpgs_mean + mpgs_std)
base.append(year)
source_error = ColumnDataSource(data=dict(base=base, lower=lower, upper=upper))
p.add_layout(
Whisker(source=source_error, base="base", upper="upper", lower="lower")
)
for i, year in enumerate(list(df.yr.unique())):
y = df[df['yr'] == year]['mpg']
color = colors[i % len(colors)]
p.circle(x=year, y=y, color=color)
show(p)