Adding 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, output_file, show
p = figure(title="Basic Title", width=300, height=300)
p.circle([1,2], [3,4])
output_file("title.html")
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, output_file, show
p = figure(title="Left Title", title_location="left",
width=300, height=300)
p.circle([1,2], [3,4])
output_file("title.html")
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, output_file, 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"
output_file("title.html")
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, output_file, 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")
output_file("title.html")
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, output_file, show
p = figure(title="Top Title with Toolbar", toolbar_location="above",
width=600, 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.
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, 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-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.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), 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)
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.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), 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)
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, output_file, show
x = np.linspace(0, 4*np.pi, 100)
y = np.cos(x)
output_file("legend_item_visibility.html")
p = figure(height=300)
# create two renderers with legend labels
main = p.circle(x, y, legend_label="Main")
aux = p.line(x, 2*y, legend_label="Auxillary",
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.
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.
See examples/models/legends.py for an example.
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
, use an instance of ColorMapper
containing a color
palette.
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 ColorBar, LogColorMapper
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, label_standoff=12)
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 arrowheadsuse the standard line properties such as
line_color
andline_alpha
to control the appearance of the outline of the arrowhead.use
fill_color
andfill_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.plotting import figure, output_file, show
output_file("arrow.html", title="arrow.py example")
p = figure(width=600, 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
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, 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
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, 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.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[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)
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, output_file, show
from bokeh.sampledata.stocks import GOOG
output_file("polyannotation.html", title="polannotation example")
p = figure(
width=800,
height=250,
x_axis_type="datetime",
title="Google stock",
)
df = pd.DataFrame(GOOG)
df["date"] = pd.to_datetime(df["date"])
p.line(df["date"], df["close"], line_width=2, color="red")
start_date = dt(2008, 11, 24)
start_float = start_date.timestamp() * 1000
start_data = df.loc[df["date"] == start_date]["close"].values[0]
end_date = dt(2010, 1, 4)
end_float = end_date.timestamp() * 1000
end_data = df.loc[df["date"] == end_date]["close"].values[0]
polygon = PolyAnnotation(
fill_color="blue",
fill_alpha=0.3,
xs=[start_float, start_float, end_float, end_float],
ys=[start_data - 100, start_data + 100, end_data + 100, end_data - 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
andy
properties to set the position (in screen units or data units).x_offset
andy_offset
properties to specify where to place the label in relation to itsx
andy
coordinates.The standard text properties as well as other styling parameters such as
border_line
andbackground_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, 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',
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', border_line_alpha=1.0,
background_fill_color='white', background_fill_alpha=1.0)
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:
gradient
: The gradient of the line, in data units.y_intercept
: The y intercept of the line, in data units.The standard line properties.
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(width=450, 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 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 withdimension
.location_units
: The unit type for thelocation
property. The default is to use data units.The standard line properties.
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_color='#0072B2', line_dash='solid', line_width=2,
legend_label="Sunset")
p.line(daylight_warsaw_2013.Date, daylight_warsaw_2013.Sunrise,
line_color='#0072B2', 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='#009E73',
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='#F0E442',
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
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 withdimension
.The standard line properties.
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(width=600, 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)