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) Bokeh Accessible Plot Examples
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.
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
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=Falseinlines BokehJS from your installedbokehpackage and ships the bundledmaidr.js, so the saved page works with no network connection. BokehJS is over a megabyte, so the page is that much larger.use_cdn=Trueand 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 formaidr.jsonly; a local fallback for BokehJS would have to carry the whole of it in every page, which is whatFalseis for.