Bokeh Essentials¶
Interactive visualization for the browser with a Python API. Bokeh renders with Canvas/WebGL, scales to large data, and powers dashboards with linked plots, hover tooltips, and widgets.
Table of Contents¶
- Installation / Setup
- Core Syntax
- Common Operations
- Practical Examples
- Common Patterns
- Best Practices
- Common Mistakes
- Troubleshooting
- Related Cheat Sheets
- Official Documentation
Installation / Setup¶
from bokeh.plotting import figure, show
from bokeh.io import output_file, output_notebook, save
from bokeh.models import ColumnDataSource, HoverTool
from bokeh.layouts import row, column, gridplot
output_notebook() # in Jupyter
output_file("plot.html") # to write a standalone file (before show/save)
output_file(...) must be called once before show()/save() in a script.
Core Syntax¶
Figure + glyphs¶
p = figure(title="My plot", width=700, height=400,
x_axis_label="total bill", y_axis_label="tip")
p.scatter(x, y, size=8, color="steelblue", alpha=0.6, legend_label="points")
p.line(x, y2, line_width=2, color="firebrick", legend_label="trend")
show(p)
| Glyph | Method | Typical data |
|---|---|---|
| Scatter | p.scatter(x, y, marker="circle") |
two numeric columns |
| Line | p.line(x, y) |
time series |
| Bars (vertical) | p.vbar(x=cat, top=heights, width=0.6) |
categories → value |
| Bars (horizontal) | p.hbar(y=cat, right=values, height=0.6) |
long category labels |
| Step / area | p.step(x, y), p.patch(x, y) |
cumulative curves |
| Segments/rects | p.segment(...), p.rect(...) |
building blocks for box/whiskers |
ColumnDataSource (the core pattern)¶
source = ColumnDataSource(df) # wrap a DataFrame once
p = figure()
p.scatter(x="total_bill", y="tip", source=source, size=8, color="day")
Glyph fields refer to column names with "..." — updating source.data updates every glyph that shares it (the foundation of Bokeh interactivity).
Histograms & distributions¶
import numpy as np
hist, edges = np.histogram(df["total_bill"], bins=30)
p = figure(height=350, x_axis_label="total_bill", y_axis_label="count")
p.quad(top=hist, bottom=0, left=edges[:-1], right=edges[1:], fill_color="navy", alpha=0.7)
Box/violin equivalents¶
Bokeh has no one-call boxplot — compose glyphs from summary stats:
groups = df.groupby("day")["total_bill"]
stats = pd.DataFrame({
"q1": groups.quantile(.25), "med": groups.median(), "q3": groups.quantile(.75),
"lo": groups.quantile(.05), "hi": groups.quantile(.95)})
p = figure(x_range=stats.index.tolist(), height=350)
p.vbar(x=stats.index, top=stats["q3"], bottom=stats["q1"], width=0.5,
fill_color="lightsteelblue", line_color="steelblue")
p.segment(x0=stats.index, y0=stats["lo"], x1=stats.index, y1=stats["hi"], color="black")
p.scatter(x=stats.index, y=stats["med"], size=10, color="darkred") # median marks
Common Operations¶
Hover tooltips¶
p = figure(tooltips=[("species", "@species"), ("mass", "@body_mass_g{0.0}")])
p.scatter(x="flipper_length_mm", y="bill_length_mm", source=source,
size=8, color="species", legend_field="species")
Layouts & subplots¶
p1 = figure(); p2 = figure(); p3 = figure()
layout = gridplot([[p1, p2], [p3, None]], width=400, height=300)
show(layout) # row/column also available
# Linked panning/zooming: share the x range
p2 = figure(x_range=p1.x_range, y_range=p1.y_range) # p2 follows p1
Widgets + interactivity¶
from bokeh.models import Slider, Select, CustomJS
slider = Slider(start=0, end=10, value=5, step=1, title="n")
slider.js_on_change("value", CustomJS(
args=dict(source=source),
code="source.data['y'] = source.data['x'].map(v => v * this.value); source.change.emit();"))
from bokeh.layouts import column
show(column(slider, p))
Axes, legends, annotations, styles¶
p.title.text = "Comparison"
p.xaxis.axis_label = "date"; p.yaxis.axis_label = "score"
p.xaxis.major_label_orientation = 0.8 # rotate ticks (radians or "vertical")
p.x_range = Range1d(0, 100); p.y_range = Range1d(0, 1)
p.legend.location = "top_left"; p.legend.title = "groups"
p.grid.grid_line_dash = [4, 4]
from bokeh.models import Label, BoxAnnotation
p.add_layout(Label(x=5, y=0.9, text="peak", text_color="red"))
p.add_layout(BoxAnnotation(top=0.95, fill_alpha=0.1, fill_color="red")) # highlight zone
from bokeh.themes import Theme
p.theme = Theme(json={"attrs": {"Figure": {"background_fill_color": "#fafafa"}}})
Exporting¶
from bokeh.io import export_png, export_svg
save(p) # writes output_file target
export_png(p, filename="plot.png") # needs selenium + a browser driver
# interactive sharing: save(p) -> standalone .html
Practical Examples¶
1. Hover-enabled scatter from pandas¶
from bokeh.plotting import figure, show, output_notebook
from bokeh.models import ColumnDataSource, HoverTool
import pandas as pd
output_notebook()
df = pd.read_csv("penguins.csv").dropna()
source = ColumnDataSource(df)
p = figure(width=700, height=420, tooltips=[("species", "@species"),
("island", "@island")])
p.scatter(x="flipper_length_mm", y="body_mass_g", source=source,
size=8, color="species", legend_field="species", alpha=0.8)
p.xaxis.axis_label = "flipper length (mm)"
p.yaxis.axis_label = "body mass (g)"
show(p)
2. Time series with a range tool¶
from bokeh.models import RangeTool
source = ColumnDataSource(df) # df has 'date' + 'value'
p = figure(x_axis_type="datetime", width=800, height=350)
p.line(x="date", y="value", source=source)
# overview strip below the main plot
select = figure(height=120, x_axis_type="datetime", tools="", toolbar_location=None)
select.line(x="date", y="value", source=source)
range_tool = RangeTool(x_range=p.x_range)
select.add_tools(range_tool)
show(column(p, select))
3. Linked brushing across two panels¶
p1 = figure(height=300, tools="box_select,lasso_select")
p2 = figure(height=300, x_range=p1.x_range)
r1 = p1.scatter(x="x", y="y1", source=source, size=6)
r2 = p2.scatter(x="x", y="y2", source=source, size=6)
# the same source means a box-select on p1 highlights the points in p2
show(gridplot([[p1], [p2]]))
Common Patterns¶
# Reuse a dataframe through ColumnDataSource everywhere
source = ColumnDataSource(df)
p.scatter(x="col_a", y="col_b", source=source, ...)
# Categorical x-axis with strings
p = figure(x_range=sorted(df["day"].unique()))
p.vbar(x="day", top="total", source=source, width=0.6)
# Faster rendering for many points
p = figure(output_backend="webgl")
p.scatter(x="x", y="y", source=source, size=3)
Best Practices¶
- Always plot through
ColumnDataSourcewhen data may update or plots link. - Use
output_notebook()in notebooks,output_file()+save()in scripts. - Prefer
webglbackend for >50k points. - Name axes, titles, and tooltips — tooltips need column-name fields from the source.
- Compose layouts with
row/column/gridplotinstead of one giant figure. - For statistical charts out of the box, check seaborn/Plotly first; use Bokeh when you need custom linked interactivity and dashboards.
Common Mistakes¶
| Mistake | Consequence | Correct approach |
|---|---|---|
Forgetting output_notebook/output_file |
Nothing renders or file not written | Call once before show()/save() |
Passing raw lists after wrapping in ColumnDataSource |
Column not found | Reference columns by name: "x" |
p1 = figure() after p1.x_range = ... |
Range reset | Set ranges at construction or via p.x_range.start/end |
| Re-showing the same figure in a loop | Reused/DOM conflicts | Build new figures or reuse one show(layout) |
| PNG export without drivers | Selenium errors | pip install selenium + matching webdriver; prefer save() HTML |
Mixing legend_label and legend_field |
Confusing legends | One per glyph: label for single series, field for data column |
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
TypeError: 'NoneType' ... show |
output_file never called |
Add output_file("x.html") or output_notebook() |
Tooltip shows ?? for a field |
Tooltip key not in source | Add the column to ColumnDataSource(df) |
| Plot renders but no axes text | Long labels overlap | major_label_orientation or shorter labels |
| Slow with many points | SVG/canvas bottleneck | output_backend="webgl", sample data |
export_png error |
Selenium/webdriver missing | pip install selenium; ChromeDriver matching browser |
Related Cheat Sheets¶
- plotly.md — easier interactive charts (pick per use case)
- altair.md — declarative interactive grammar
- matplotlib.md · seaborn.md — static publication charts
- Data Visualization category index · Home · Index
Official Documentation¶
Last Updated¶
2026-09-06 — Initial version.