Skip to content

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

pip install bokeh pandas      # or: uv add bokeh pandas
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 ColumnDataSource when data may update or plots link.
  • Use output_notebook() in notebooks, output_file() + save() in scripts.
  • Prefer webgl backend for >50k points.
  • Name axes, titles, and tooltips — tooltips need column-name fields from the source.
  • Compose layouts with row/column/gridplot instead 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

Official Documentation

Last Updated

2026-09-06 — Initial version.