Bokeh Accessible Plot Examples

Code examples showing how to make Bokeh bar charts, stacked and grouped bars, histograms, line, step and scatter plots, heatmaps, pie charts, candlestick charts, Gantt charts, hexbin plots and gridplot layouts accessible with py-maidr.

Bokeh Examples

maidr also supports Bokeh figures and layouts. Pass a figure, a gridplot, or a row/column of figures to maidr.show(), maidr.render() or maidr.save_html(), and maidr produces a page where the interactive Bokeh plot keeps its pan, zoom and hover tools and can also be read from the keyboard, as sound, as text and in braille. For the other libraries, see the Matplotlib / Seaborn, Plotly and Altair pages.

WarningExperimental

Bokeh support is experimental. What it reads, and how, may change without a deprecation period, whichever plot type is drawn: see Plot Type Stability. Install it with pip install "maidr[bokeh]".

maidr reads the figure’s own renderers and data sources, not the drawn canvas, so what you hear is the data you plotted. As you move through a chart the mark being read is highlighted in the Bokeh plot itself: a bar, bin, cell, slice, candle or point is selected, dimming the rest, and a line, step or area shows a ring on the current point. A bar, bin, cell or point whose data source another mark also draws from, such as markers on a line built from one ColumnDataSource, gets the ring too, so the other mark is never faded.

Bokeh read as
vbar, hbar bar
vbar_stack, hbar_stack stacked bar
vbar with dodge() or an offset such as ("a", -0.2), or on a nested FactorRange dodged bar
quad histogram
line, multi_line line, one series per renderer or path
step step
scatter, circle point
rect coloured through a continuous colour mapper (linear_cmap, log_cmap) heatmap
image of up to 10,000 cells heatmap
wedge, annular_wedge pie, one slice per row
segment wicks with vbar bodies on a datetime axis (Bokeh’s OHLC recipe) candlestick
varea, varea_stack, harea, harea_stack area [experimental], stacked area [experimental]
hbar from one date to another (left=start, right=end on a datetime axis) gantt [experimental]
hex_tile coloured through a colour mapper, as p.hexbin draws it hexbin [experimental]

Any other glyph is left out with a warning naming it, and a figure with nothing maidr can read is still drawn. A Tabs layout is read one panel at a time: the active one.

Bar Plot

import seaborn as sns
from bokeh.plotting import figure

import maidr  

penguins = sns.load_dataset("penguins")
avg_mass = penguins.groupby("species")["body_mass_g"].mean()
species = avg_mass.index.tolist()

p = figure(
    x_range=species,
    title="Average Body Mass of Penguins by Species",
    x_axis_label="Species",
    y_axis_label="Body Mass (g)",
    height=350,
)
p.vbar(x=species, top=avg_mass.round(1).tolist(), width=0.8, color="#4c72b0")

maidr.show(p)  

Horizontal Bar Plot

import seaborn as sns
from bokeh.plotting import figure

import maidr  

penguins = sns.load_dataset("penguins")
counts = penguins["island"].value_counts().sort_values()

p = figure(
    y_range=counts.index.tolist(),
    title="Penguins Counted on Each Island",
    x_axis_label="Penguins",
    y_axis_label="Island",
    height=300,
)
p.hbar(y=counts.index.tolist(), right=counts.tolist(), height=0.7, color="#55a868")

maidr.show(p)  

Stacked Bar Plot

import seaborn as sns
from bokeh.plotting import figure

import maidr  

tips = sns.load_dataset("tips")
totals = tips.pivot_table(
    index="day", columns="sex", values="total_bill", aggfunc="sum", observed=True
)
days = [str(day) for day in totals.index]
source = {"day": days, "Male": totals["Male"].round(2), "Female": totals["Female"].round(2)}

p = figure(
    x_range=days,
    title="Total Bill by Day and Sex",
    x_axis_label="Day",
    y_axis_label="Total Bill ($)",
    height=350,
)
p.vbar_stack(
    ["Male", "Female"],
    x="day",
    width=0.8,
    source=source,
    color=["#4c72b0", "#dd8452"],
    legend_label=["Male", "Female"],
)
p.legend.title = "Sex"

maidr.show(p)  

Dodged (Grouped) Bar Plot

import seaborn as sns
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure
from bokeh.transform import dodge

import maidr  

penguins = sns.load_dataset("penguins").dropna()
by_sex = penguins.pivot_table(
    index="species", columns="sex", values="body_mass_g", aggfunc="mean"
).round(1)
species = by_sex.index.tolist()
source = ColumnDataSource(
    {"species": species, "Male": by_sex["Male"], "Female": by_sex["Female"]}
)

p = figure(
    x_range=species,
    title="Average Body Mass by Species and Sex",
    x_axis_label="Species",
    y_axis_label="Body Mass (g)",
    height=350,
)
p.vbar(
    x=dodge("species", -0.2, range=p.x_range), top="Male", width=0.35,
    source=source, color="#4c72b0", legend_label="Male",
)
p.vbar(
    x=dodge("species", 0.2, range=p.x_range), top="Female", width=0.35,
    source=source, color="#dd8452", legend_label="Female",
)
p.legend.title = "Sex"

maidr.show(p)  

A vbar on a nested FactorRange – x=[("Adelie", "Male"), ("Adelie", "Female"), ...] – is read the same way: the inner factor names the group, and the outer one the category.

Histogram

import numpy as np
import seaborn as sns
from bokeh.plotting import figure

import maidr  

penguins = sns.load_dataset("penguins").dropna()
counts, edges = np.histogram(penguins["flipper_length_mm"], bins=12)

p = figure(
    title="Distribution of Penguin Flipper Length",
    x_axis_label="Flipper Length (mm)",
    y_axis_label="Count",
    height=350,
)
p.quad(
    top=counts, bottom=0, left=edges[:-1], right=edges[1:],
    fill_color="#8172b3", line_color="white",
)

maidr.show(p)  

Line Plot

import seaborn as sns
from bokeh.plotting import figure

import maidr  

flights = sns.load_dataset("flights")
per_year = flights.groupby("year")["passengers"].sum()

p = figure(
    title="Airline Passengers per Year",
    x_axis_label="Year",
    y_axis_label="Passengers",
    height=350,
)
p.line(per_year.index.tolist(), per_year.tolist(), line_width=2)

maidr.show(p)  

Multi-Line Plot

import seaborn as sns
from bokeh.plotting import figure

import maidr  

flights = sns.load_dataset("flights")

p = figure(
    title="Monthly Passengers in Three Years",
    x_axis_label="Month",
    y_axis_label="Passengers",
    height=350,
)
for year, color in [(1949, "#4c72b0"), (1954, "#55a868"), (1960, "#c44e52")]:
    rows = flights[flights["year"] == year]
    p.line(
        list(range(1, 13)), rows["passengers"].tolist(),
        line_width=2, color=color, legend_label=str(year),
    )
p.legend.title = "Year"
p.legend.location = "top_left"

maidr.show(p)  

Every line on a figure becomes one series of one line layer, named by its legend label; press the up and down arrows to move between them.

Step Plot

import seaborn as sns
from bokeh.plotting import figure

import maidr  

tips = sns.load_dataset("tips").head(20)
running_max = tips["total_bill"].cummax()

p = figure(
    title="Largest Bill So Far",
    x_axis_label="Table",
    y_axis_label="Total Bill ($)",
    height=350,
)
p.step(list(range(1, 21)), running_max.tolist(), mode="after", line_width=2)

maidr.show(p)  

Scatter Plot

import seaborn as sns
from bokeh.plotting import figure

import maidr  

penguins = sns.load_dataset("penguins").dropna()

p = figure(
    title="Bill Length vs Bill Depth",
    x_axis_label="Bill Length (mm)",
    y_axis_label="Bill Depth (mm)",
    height=350,
)
p.scatter(
    penguins["bill_length_mm"].tolist(), penguins["bill_depth_mm"].tolist(),
    size=6, alpha=0.7,
)

maidr.show(p)  

Heatmap

import seaborn as sns
from bokeh.plotting import figure
from bokeh.transform import linear_cmap

import maidr  

flights = sns.load_dataset("flights")
flights["year"] = flights["year"].astype(str)
flights["month"] = flights["month"].astype(str)
years = sorted(flights["year"].unique())
months = flights["month"].unique().tolist()[::-1]

p = figure(
    x_range=years,
    y_range=months,
    title="Airline Passengers by Month and Year",
    x_axis_label="Year",
    y_axis_label="Month",
    height=450,
)
cells = p.rect(
    x="year", y="month", width=1, height=1, source=flights, line_color=None,
    fill_color=linear_cmap(
        "passengers", "Viridis256",
        flights["passengers"].min(), flights["passengers"].max(),
    ),
)
p.add_layout(cells.construct_color_bar(title="Passengers"), "right")

maidr.show(p)  

Image Heatmap

import numpy as np
from bokeh.plotting import figure

import maidr  

xs = np.linspace(0, 3, 12)
ys = np.linspace(0, 3, 12)
field = np.round(np.sin(xs)[None, :] * np.cos(ys)[:, None], 2)

p = figure(
    title="sin(x) cos(y)",
    x_axis_label="x",
    y_axis_label="y",
    x_range=(0, 3),
    y_range=(0, 3),
    height=400,
)
img = p.image(image=[field], x=0, y=0, dw=3, dh=3, palette="Viridis256")
p.add_layout(img.construct_color_bar(title="Value"), "right")

maidr.show(p)  

An image is read as a heatmap whose cells are the array’s, bottom row first as Bokeh draws it, each row and column named by the centre of its cells. An image of more than 10,000 cells is left out with a warning, since no one walks a photograph cell by cell.

Pie Chart

from math import pi

import seaborn as sns
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure
from bokeh.transform import cumsum

import maidr  

penguins = sns.load_dataset("penguins")
counts = penguins["species"].value_counts().sort_index()
source = ColumnDataSource(
    {
        "species": counts.index.tolist(),
        "penguins": counts.tolist(),
        "angle": (counts / counts.sum() * 2 * pi).tolist(),
        "color": ["#4c72b0", "#dd8452", "#55a868"],
    }
)

p = figure(title="Penguins by Species", height=350, toolbar_location=None)
p.wedge(
    x=0, y=1, radius=0.4, source=source,
    start_angle=cumsum("angle", include_zero=True), end_angle=cumsum("angle"),
    line_color="white", fill_color="color", legend_field="species",
)
p.axis.visible = False
p.grid.visible = False

maidr.show(p)  

Each slice is announced with the value it was computed from – here the penguins column, which the angles are proportional to – and named by the legend. The right arrow walks the pie clockwise, as for every pie in maidr, so on a pie Bokeh drew counterclockwise from 3 o’clock it starts on the slice drawn last.

Candlestick Chart

import numpy as np
import pandas as pd
from bokeh.plotting import figure

import maidr  

rng = np.random.default_rng(4)
dates = pd.bdate_range("2024-03-01", periods=15)
close = (100 + rng.normal(0, 1.5, len(dates)).cumsum()).round(2)
open_ = np.r_[100.0, close[:-1]]
df = pd.DataFrame(
    {
        "date": dates,
        "open": open_,
        "close": close,
        "high": (np.maximum(open_, close) + rng.uniform(0.2, 1.5, len(dates))).round(2),
        "low": (np.minimum(open_, close) - rng.uniform(0.2, 1.5, len(dates))).round(2),
    }
)
inc = df.close > df.open
dec = df.open > df.close
w = 12 * 60 * 60 * 1000  # half a day, in ms

p = figure(
    x_axis_type="datetime",
    title="Simulated Stock, March 2024",
    x_axis_label="Date",
    y_axis_label="Price ($)",
    height=350,
)
p.segment(df.date, df.high, df.date, df.low, color="black")
p.vbar(df.date[dec], w, df.open[dec], df.close[dec], color="#c44e52")
p.vbar(df.date[inc], w, df.open[inc], df.close[inc], fill_color="white",
       line_color="#55a868", line_width=2)

maidr.show(p)  

This is Bokeh’s own candlestick recipe: a segment from each day’s high to its low and a vbar between its open and close, one renderer for rising days and one for falling. maidr reads them together as one candlestick, a candle per date. A body is drawn between two prices whichever is larger, so maidr needs to be told which one is the open: here the prices are passed straight through (top=open, bottom=close), which draws a rising day’s body upside down and says so; a source with open and close columns says so too. Bodies drawn the right way up with neither are not guessed at, and are read as bars. A day that closes exactly where it opened gets no body from this recipe, so its open and close are not in the figure: maidr leaves that day out, with a warning.

Multi-Panel Layout (gridplot)

import seaborn as sns
from bokeh.layouts import gridplot
from bokeh.plotting import figure

import maidr  

penguins = sns.load_dataset("penguins")
flights = sns.load_dataset("flights")
counts = penguins["species"].value_counts().sort_index()
per_year = flights.groupby("year")["passengers"].sum()

left = figure(
    x_range=counts.index.tolist(), title="Penguins per Species",
    x_axis_label="Species", y_axis_label="Penguins", width=350, height=300,
)
left.vbar(x=counts.index.tolist(), top=counts.tolist(), width=0.8)

right = figure(
    title="Passengers per Year", x_axis_label="Year",
    y_axis_label="Passengers", width=350, height=300,
)
right.line(per_year.index.tolist(), per_year.tolist(), line_width=2)

maidr.show(gridplot([[left, right]]))  

Each figure of a gridplot, row or column is a subplot. Press Enter to go into one, Escape to come back out, and the arrow keys between them, in the directions they are laid out on the page. A figure with nothing maidr can read is left out of the grid.

Stacked Area Plot [experimental]

import seaborn as sns
from bokeh.plotting import figure

import maidr  

flights = sns.load_dataset("flights")
wide = flights.pivot(index="month", columns="year", values="passengers")
source = {
    "month": list(range(1, 13)),
    "1958": wide[1958].tolist(),
    "1959": wide[1959].tolist(),
    "1960": wide[1960].tolist(),
}

p = figure(
    title="Monthly Passengers, 1958 to 1960",
    x_axis_label="Month",
    y_axis_label="Passengers",
    height=350,
)
p.varea_stack(
    ["1958", "1959", "1960"],
    x="month",
    source=source,
    color=["#4c72b0", "#55a868", "#c44e52"],
    legend_label=["1958", "1959", "1960"],
)
p.legend.location = "top_left"

maidr.show(p)  

Horizontal Area Plot [experimental]

import seaborn as sns
from bokeh.plotting import figure

import maidr  

flights = sns.load_dataset("flights")
months = flights[flights["year"] == 1960]

p = figure(
    title="Passengers by Month, 1960",
    x_axis_label="Passengers",
    y_axis_label="Month",
    height=400,
)
p.harea(
    y=list(range(1, 13)), x1=0, x2=months["passengers"].tolist(),
    fill_color="#8172b3", fill_alpha=0.7,
)

maidr.show(p)  

An harea is read as the same area on its side: the arrow keys step through the months, and the passengers are what you hear.

Gantt Chart [experimental]

import pandas as pd
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure

import maidr  

plan = pd.DataFrame(
    {
        "task": ["Research", "Design", "Build", "Test", "Launch"],
        "start": pd.to_datetime(
            ["2024-01-01", "2024-01-08", "2024-01-15", "2024-01-24", "2024-01-30"]
        ),
        "end": pd.to_datetime(
            ["2024-01-10", "2024-01-17", "2024-01-26", "2024-01-31", "2024-02-02"]
        ),
    }
)

p = figure(
    y_range=plan["task"].tolist()[::-1],
    x_axis_type="datetime",
    title="Project Plan, January 2024",
    x_axis_label="Date",
    y_axis_label="Task",
    height=300,
)
p.hbar(
    y="task", left="start", right="end", height=0.6,
    source=ColumnDataSource(plan), color="#4c72b0",
)

maidr.show(p)  

An hbar running from one date to another is read as a Gantt chart: each task is a lane, the up and down arrows move between lanes as they are drawn, and every bar is announced with its start and end dates and its length in days.

Hexbin Plot [experimental]

import seaborn as sns
from bokeh.plotting import figure

import maidr  

penguins = sns.load_dataset("penguins").dropna()
flipper = (penguins["flipper_length_mm"] - 200) / 10
mass = (penguins["body_mass_g"] - 4200) / 800

p = figure(
    title="Flipper Length vs Body Mass (standardised)",
    x_axis_label="Flipper Length (x10 mm from 200)",
    y_axis_label="Body Mass (x800 g from 4200)",
    match_aspect=True,
    height=400,
)
tiles, bins = p.hexbin(flipper.to_numpy(), mass.to_numpy(), size=0.3)
p.add_layout(tiles.construct_color_bar(title="Penguins"), "right")

maidr.show(p)  

Each hexagon is a bin announced by its centre and how many penguins fell in it; the up and down arrows move to the nearest bin of the staggered row above or below.

Directed Graph [experimental]

import networkx as nx
from bokeh.plotting import figure, from_networkx

import maidr  

pipeline = nx.DiGraph(
    [
        ("load", "clean"),
        ("clean", "features"),
        ("clean", "labels"),
        ("features", "train"),
        ("labels", "train"),
    ]
)

p = figure(title="A training pipeline", height=400)
p.renderers.append(from_networkx(pipeline, nx.spring_layout, seed=1))  

maidr.show(p)  

A graph from_networkx made from a directed graph (nx.DiGraph) is walked node by node in topological order: each node announces what feeds it, what it feeds, and whether it is a branch or merge point, and is selected in the plot. An undirected graph is left unread, as before, since an undirected edge has no direction to announce. Needs networkx (pip install maidr[networkx]).

Saving and embedding

maidr.save_html(p, "chart.html") writes the same page to a file. use_cdn decides where maidr.js comes from, exactly as for matplotlib and Plotly, and where BokehJS comes from too:

  • use_cdn=False inlines BokehJS from your installed bokeh package and ships the bundled maidr.js, so the saved page works with no network connection. BokehJS is over a megabyte, so the page is that much larger.
  • use_cdn=True and the default "auto" load BokehJS from Bokeh’s CDN, at the version you have installed, so opening the page needs a network connection. "auto" falls back to the bundled copy for maidr.js only; a local fallback for BokehJS would have to carry the whole of it in every page, which is what False is for.