plotlet v0.6.2

Cookbook

Composed, multi-panel recipes — the figures that take more than one artist. Every one is generated by the script under it. Single plot types live on their reference pages instead. Larger multi-file projects live in the plotlet-cookbook repo.

Composition

Anscombe's quartet

A 2x2 grid with shared scales: same means, same variances, same regression line -- four very different point clouds.

Show code
import plotlet as pt
from plotlet import aes

raw = pt.load_dataset("anscombe")

panels = []
for ds in ("I", "II", "III", "IV"):
    xs = [x for x, d in zip(raw["x"], raw["dataset"]) if d == ds]
    ys = [y for y, d in zip(raw["y"], raw["dataset"]) if d == ds]
    sub = {"x": xs, "y": ys}

    p = pt.chart(sub, aes(x="x", y="y"),
                 title=f"dataset {ds}", data_width=190, data_height=140)
    p.add_scatter(size=3.5, alpha=0.8)
    p.add_regression()
    panels.append(p)

p1, p2, p3, p4 = panels
c = pt.grid([[p1, p2], [p3, p4]]).share_x(True).share_y(True)

Facets

One panel per group with shared axes, from a single call.

Show code
import math

import plotlet as pt
from plotlet import aes

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=150,
             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)

Annotated heatmap

Central heatmap, clustering dendrogram on top, shared legend on the right. The dendrogram orders the columns; the sector split adds the gaps between cluster blocks.

["s11","s6","s3","s8","s1","s4","s9","s7","s10","s5","s2","s12"]["gene 1","gene 2","gene 3","gene 4","gene 5","gene 6"]["s11","s6","s3","s8","s1","s4","s9","s7","s10","s5","s2","s12"]
Show code
import random

import plotlet as pt
from plotlet import aes

rng = random.Random(7)
genes = [f"gene {i + 1}" for i in range(6)]
samples = [f"s{i + 1}" for i in range(12)]
groups = ["X", "Y", "Z", "X", "Y", "Z", "Y", "Z", "X", "Y", "Z", "Y"]
level = {"X": 0.0, "Y": 5.0, "Z": 10.0}
matrix = [[level[groups[j]] + rng.gauss(0, 0.4) for j in range(12)]
          for _ in genes]

# The dendrogram clusters heatmap columns, so it sees the transpose.
by_sample = [[matrix[r][j] for r in range(len(genes))] for j in range(12)]
tree = pt.chart(data_height=60)
tree.add_dendrogram(by_sample, labels=samples, orientation="top",
                    clusters=groups, method="ward")

by_group = {}
for s, g in zip(samples, groups):
    by_group.setdefault(g, []).append(s)

hm = pt.chart(title="expression by sample", data_width=420, data_height=180)
hm.sectors(by_group, axis="x", divider=False, label=False)
data = {"sample": samples}
for r, gene in enumerate(genes):
    data[gene] = matrix[r]
hm.add_heatmap(data=data, mapping=aes(x="sample"), values=genes,
               cmap="viridis", legend={"label": "expression"})
hm.attach_above(tree)

c = pt.grid([[hm, pt.legend()]]).gap(0)

Stacked tracks with sectors

Named, length-weighted partitions of a shared x-axis -- the layout underneath genome-browser and phase-plot figures. Both tracks share x and the sector chrome.

Show code
import random

import plotlet as pt
from plotlet import aes

rng = random.Random(4)
regions = {"region A": 100, "region B": 500, "region C": 200}

pts = {"region": [], "pos": [], "score": []}
for name, length in regions.items():
    for _ in range(14):
        pts["region"].append(name)
        pts["pos"].append(rng.uniform(0, length))
        pts["score"].append(rng.uniform(0.1, 0.9))

steps = {"region": [], "pos": [], "cov": []}
for name, length in regions.items():
    for i in range(24):
        steps["region"].append(name)
        steps["pos"].append(length * i / 23)
        steps["cov"].append(0.5 + 0.3 * rng.uniform(-1, 1))

t1 = pt.chart(pts, aes(x="pos", y="score"),
              data_width=480, data_height=80, ylabel="score", ylim=(0, 1))
t1.add_scatter(size=3, color="#534AB7")

t2 = pt.chart(steps, aes(x="pos", y="cov", group="region"),
              data_width=480, data_height=80, ylabel="coverage",
              ylim=(0, 1))
t2.add_line(color="#1D9E75")
t2.add_axhline(0.5, color="#888888", linestyle="--")

c = (pt.grid([[t1], [t2]])
       .share_x("col")
       .sectors(regions, column="region"))

Circular

Circular coordinate

Any Cartesian chart becomes a ring with one line: bars grouped into named wedges, with gaps between the sector groups.

["a","b","c","d","e","f","g","h"]
Show code
import plotlet as pt
from plotlet import aes

df = {"cat": list("abcdefgh"), "val": [3, 5, 2, 6, 4, 7, 3, 5]}

c = pt.chart(df, aes(x="cat", y="val", fill="cat"),
             title="categorical sectors on a ring")
c.coordinate(pt.CircularCoordinate())
c.sectors({"G1": ["a", "b", "c"], "G2": ["d", "e"], "G3": ["f", "g", "h"]},
          axis="x")
c.add_bar(palette="Set2")

Circular

A radial dendrogram and a heatmap ring stacked on one leaf axis -- the annotated-heatmap shape, wrapped onto a circle.

["L10","L13","L07","L01","L04","L02","L08","L05","L11","L14","L09","L12","L03","L15","L00","L06"]["f0","f1","f2","f3","f4","f5"]["L10","L13","L07","L01","L04","L02","L08","L05","L11","L14","L09","L12","L03","L15","L00","L06"]
Show code
import random

import plotlet as pt
from plotlet import aes
from plotlet.cluster import layout_tree

rng = random.Random(0)
profiles = [[2.0, 1.5, 0.0, -1.0, 0.5, 1.0],
            [-1.0, 0.0, 2.0, 0.5, -1.5, 0.0],
            [0.5, -1.5, -1.0, 2.0, 1.5, -0.5]]
labels, matrix = [], []
for i in range(16):
    labels.append(f"L{i:02d}")
    matrix.append([p + rng.gauss(0, 0.6) for p in profiles[i % 3]])

# Read the leaf order from a local tree so the heatmap columns land at
# the same angles as the dendrogram leaves.
_, _, leaf_order = layout_tree(pt.linkage(matrix, labels=labels,
                                          method="ward"))
row_by_label = dict(zip(labels, matrix))
data = {"leaf": leaf_order}
for j in range(len(matrix[0])):
    data[f"f{j}"] = [row_by_label[l][j] for l in leaf_order]

tree_ring = pt.chart(xlim=(0, 1))
tree_ring.add_dendrogram(data=matrix, labels=labels, method="ward",
                         orientation="bottom", color="#3B3B4F")

hm_ring = pt.chart(xlim=(0, 1), ylim=(0, 1))
hm_ring.add_heatmap(data=data, mapping=aes(x="leaf"),
                    values=[f"f{j}" for j in range(len(matrix[0]))],
                    cmap="RdBu_r", center=0)

pile = (hm_ring / tree_ring).coordinate(
    pt.CircularCoordinate(r_inner=0.08, wrap_gap_deg=4))
pile.heights([1.0, 2.2])

c = pile.title("circular annotated heatmap")

Chord diagram

A sectored ring with chord ribbons through the center disc -- Circos-style flow figures from the same chart API.

Show code
import plotlet as pt
from plotlet import aes

sectors = pt.Sectors(names=["A", "B", "C"], lengths=[30, 25, 20], gap=4)
span = (0, sectors.total())

flows = {
    "src": ["A", "A", "B"],
    "dst": ["B", "C", "C"],
    "x1a": [0, 18, 0], "x1b": [10, 28, 15],
    "x2a": [0, 0, 0],  "x2b": [10, 8, 12],
}
arcs = pt.chart(flows, xlim=span)
arcs.sectors(sectors, column="src", label=False)
arcs.add_chord_ribbon(aes(x1_start="x1a", x1_end="x1b",
                          x2_start="x2a", x2_end="x2b",
                          x1_sector="src", x2_sector="dst",
                          color="src"), alpha=0.6)

ring = pt.chart(xlim=span, ylim=(0, 1))
ring.sectors(sectors, column="x")

c = pt.grid([[ring]]).coordinate(
    pt.CircularCoordinate(r_inner=0.85, inner=arcs)
)

From the extensions catalog

Domain-specific plot types stay out of the core and ship as single-file artists in plotlet-extensions. A single import plotlet.extensions registers the whole catalog. The extensions gallery shows every artist in the catalog, rendered at build time.