4. Multi-panel composition
Operators, grids, shared scales, attachments, insets and facets — the layer the flagship figures are built on. Every snippet on this page is executed on every build — the figures are its output.
Operators and grids
Charts are values: a | b puts panels
side by side, a / b stacks them, and
pt.grid([[...]]) builds full grids —
None leaves a cell empty. Margins are coordinated across
the layout so data regions align.
import math
xs = [i * 0.2 for i in range(40)]
sig = {"x": xs, "y": [math.sin(x) for x in xs]}
counts = {"cat": ["a", "b", "c"], "n": [4, 7, 3]}
dist = {"x": [0.1, 0.4, 0.5, 0.55, 0.7, 0.8, 0.9, 1.1, 1.3]}
a = pt.chart(sig, aes(x="x", y="y"), title="signal",
data_width=220, data_height=140)
a.add_line()
b = pt.chart(counts, aes(x="cat", y="n"), title="counts",
data_width=220, data_height=140)
b.add_bar()
d = pt.chart(dist, aes(x="x"), title="dist",
data_width=220, data_height=140)
d.add_hist(bins=6)
c = pt.grid([[a, b], [d, None]]) # same as (a | b) / bottom row
Panel sizes and gaps
Layouts are body-first: each chart’s
data_width / data_height act as relative size
hints, and the layout sums them. .gap(0) removes the
default spacing; .heights([...]) overrides row
ratios.
import math
xs = [i * 0.15 for i in range(60)]
sig = {"x": xs, "y": [math.sin(x) for x in xs]}
rate = {"x": xs, "y": [math.cos(3 * x) for x in xs]}
main = pt.chart(sig, aes(x="x", y="y"),
data_width=420, data_height=180, ylabel="signal")
main.add_line()
small = pt.chart(rate, aes(x="x", y="y"),
data_width=420, data_height=60, ylabel="rate")
small.add_line(color="C2")
c = pt.grid([[small], [main]]).gap(0).share_x("col")
Attachments
attach_above / _below / _left / _right
glue annotation tracks to a host chart and share the touching axis
— marginal histograms, annotation strips, dendrograms. Multiple
attachments stack outward in call order.
import random
rng = random.Random(5)
xs = [rng.gauss(0, 1) for _ in range(300)]
df = {"x": xs, "y": [x * 0.6 + rng.gauss(0, 0.6) for x in xs]}
c = pt.chart(df, aes(x="x", y="y"), data_width=300, data_height=200,
xlabel="x", ylabel="y")
c.add_scatter(size=2, alpha=0.5)
top = pt.chart(df, aes(x="x"), data_height=55)
top.add_hist(bins=30, fill="#8a8fb0")
c.attach_above(top)
Insets
c.inset(rect=(x, y, w, h)) places a
child panel inside the parent’s data area (fractions of the
panel), with its own limits — the classic zoom-on-the-tail.
labels = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]
counts = [950, 320, 80, 45, 28, 18, 12, 8, 5, 3]
df = {"category": labels, "count": counts}
df_tail = {"category": labels[2:], "count": counts[2:]}
c = pt.chart(df, aes(x="category", y="count"),
data_width=440, data_height=240,
title="long-tail distribution", ylabel="count")
c.add_bar()
inset = c.inset(rect=(0.4, 0.45, 0.55, 0.45), ylim=(0, 100))
inset.add_bar(df_tail, aes(x="category", y="count"))
Facets
When the panels are one-per-group over the same
columns, skip manual loops: pt.facet(df, by=...) splits a
table into a wrapped grid of shared-axis panels, and
row= / col= build two-factor grids.
import math
raw = pt.load_dataset("penguins")
keep = [i for i in range(len(raw["species"]))
if not math.isnan(raw["bill_length_mm"][i])
and not math.isnan(raw["bill_depth_mm"][i])]
df = {k: [raw[k][i] for i in keep]
for k in ("species", "bill_length_mm", "bill_depth_mm")}
c = pt.facet(df, by="species", col_wrap=3,
data_width=170, data_height=140,
xlabel="bill length (mm)", ylabel="bill depth (mm)")
c.add_scatter(aes(x="bill_length_mm", y="bill_depth_mm"),
size=2, alpha=0.7)
← Titles, labels & legends · Sectors & circular coordinates →