Skip to content

Plot types

Every 2D mark is a method on Axes. They share a few conventions:

  • color may be None (cycle the theme palette), a "C0".."Cn" palette index, a CSS name, hex, or an (r, g, b[, a]) tuple.
  • label= registers the mark in the legend.
  • alpha= and zorder= work on every mark.
  • Calls return the axes, so they chain. The imshow family returns a colorbar handle instead.

Lines & points

Line

line plots a polyline. linestyle is one of solid, dashed, dotted, dashdot (or none for markers only); an optional marker draws a glyph at each vertex.

"""Line plot: multiple series with a legend."""
import math

import pyplotrs as pp

xs = [i * 0.1 for i in range(80)]
fig, ax = pp.subplots()
ax.line(xs, [math.sin(x) for x in xs], label="sin")
ax.line(xs, [math.sin(x) * math.exp(-0.1 * x) for x in xs],
        label="damped", linestyle="dashed")
ax.set(title="Line plot", xlabel="t", ylabel="amplitude")
ax.legend()
fig.save("line.png")

line plot

Dense data

line collapses runs of near-collinear vertices in device space by default (simplify=True) — visually identical output, far smaller and faster vector files on large data. Pass simplify=False to keep every vertex exactly.

Scatter

scatter places markers. markersize is the marker diameter in points — the same unit line(marker=..., markersize=...) uses, so the same number means the same size everywhere. size is also accepted and means the area in pt², matching matplotlib's s, so size=36 and markersize=6 agree. Marker shapes: o s ^ v D (filled) and + x (stroked).

"""Scatter plot with marker styling."""
import math

import pyplotrs as pp

n = 80
xs = [i / n * 6 for i in range(n)]
ys = [math.sin(x) + 0.15 * math.cos(7 * x) for x in xs]
fig, ax = pp.subplots()
ax.scatter(xs, ys, markersize=6.3, marker="o", color="C0",
           edgecolor=(255, 255, 255, 255), edgewidth=0.8)
ax.set(title="Scatter plot", xlabel="x", ylabel="y")
fig.save("scatter.png")

scatter plot

Pass c= (a per-point array) to color markers by value through cmap/norm; the call then returns a handle for Figure.colorbar.

Steps & stems

Method What it draws
step(xs, ys, where="pre"/"post"/"mid") Step plot
stairs(values, edges=None, fill=False) Step outline over bin edges
stem(xs, ys, bottom=0) Stems from a baseline, topped with markers

Log-scaled shortcuts

loglog, semilogx and semilogy draw a line and set the corresponding scale in one call. See scales & ticks.

Bars & categories

bar draws vertical bars; barh horizontal ones. Bars sit flush on their own base — the value axis stops there rather than padding past it, whether that base is the default 0 or a bottom/left you passed — and string positions give a categorical axis:

ax.bar(["ash", "birch", "cedar"], [12, 19, 7])
"""Vertical bar chart."""
import pyplotrs as pp

x = [0, 1, 2, 3, 4]
heights = [5.1, 7.3, 3.8, 6.4, 4.2]
fig, ax = pp.subplots()
ax.bar(x, heights, width=0.7, color="C3")
ax.set(title="Bar chart", xlabel="category", ylabel="value")
fig.save("bar.png")

bar chart

width (on bar) and height (on barh) are extents in data units, not stroke widths. bottom/left offset the baseline, which is how stacked bars are built.

Related: broken_barh(xranges, yrange) for interval/Gantt bars, and eventplot(positions) for a raster of event marks.

Distributions

Histogram

hist bins data into equal-width bins. Use density=True to normalize to a probability density, and range=(lo, hi) to fix the binning extent.

"""Histogram of a synthetic distribution."""
import math

import pyplotrs as pp


# Box-Muller normal samples (no numpy dependency).
def normals(n, seed=1):
    s = seed
    out = []
    for _ in range(n):
        s = (1103515245 * s + 12345) & 0x7FFFFFFF
        u1 = (s + 1) / 0x80000000
        s = (1103515245 * s + 12345) & 0x7FFFFFFF
        u2 = (s + 1) / 0x80000000
        out.append(math.sqrt(-2 * math.log(u1)) * math.cos(2 * math.pi * u2))
    return out

fig, ax = pp.subplots()
ax.hist(normals(2000), bins=30, color="C2", density=True)
ax.set(title="Histogram", xlabel="value", ylabel="density")
fig.save("histogram.png")

histogram

Box, violin and pie

boxplot draws a box-and-whisker per array (showfliers=False drops the outlier points); violinplot draws a mirrored Gaussian-KDE density — computed in Rust, so no SciPy dependency; pie draws an auto-normalized pie, turning the frame off and fixing an equal aspect so the wedges stay circular.

"""Distribution marks: boxplot, violinplot, and a labeled pie."""
import math
import random

import pyplotrs as pp

random.seed(7)
groups = [[random.gauss(mu, sd) for _ in range(200)]
          for mu, sd in [(0.0, 1.0), (1.2, 0.6), (0.4, 1.6)]]
labels = ["control", "drug A", "drug B"]

fig, (ax_box, ax_violin, ax_pie) = pp.subplots(1, 3, figsize=(660, 230))

ax_box.boxplot(groups)
ax_box.set(title="boxplot", ylabel="response", xticks=[1, 2, 3], xticklabels=labels)

ax_violin.violinplot(groups)
ax_violin.set(title="violinplot", xticks=[1, 2, 3], xticklabels=labels)

ax_pie.pie([42, 31, 27], labels=labels)
ax_pie.set(title="pie")

fig.save("statistical.png")

box, violin and pie

pie is the one mark with no scalar label: its labels are per wedge, so they come from labels=, which is also what feeds legend(). The pie is fitted in device space against its measured labels, so a long label shrinks the pie instead of being clipped.

Uncertainty & bands

Fill between

fill_between shades the band between two curves (or a curve and a constant) — ideal for confidence intervals. alpha controls transparency, and fill_betweenx is the transpose.

"""Fill between curves: a line with a confidence band."""
import math

import pyplotrs as pp

xs = [i * 0.1 for i in range(80)]
mid = [math.sin(x) for x in xs]
lo = [m - 0.2 - 0.05 * x for m, x in zip(mid, xs)]
hi = [m + 0.2 + 0.05 * x for m, x in zip(mid, xs)]
fig, ax = pp.subplots()
ax.fill_between(xs, lo, hi, color="C0", alpha=0.25, label="±1σ")
ax.line(xs, mid, color="C0", label="mean")
ax.set(title="Confidence band", xlabel="t", ylabel="y")
ax.legend()
fig.save("fill_between.png")

fill between

Error bars

errorbar draws symmetric yerr/xerr bars with caps, optionally connected by a line and decorated with markers. Pass linestyle="none" for markers and whiskers only.

"""Error bars with caps."""
import math

import pyplotrs as pp

xs = list(range(1, 9))
ys = [math.log(x) for x in xs]
yerr = [0.08 + 0.02 * x for x in xs]
fig, ax = pp.subplots()
ax.errorbar(xs, ys, yerr=yerr, marker="o", capsize=4, color="C6", label="measured")
ax.set(title="Error bars", xlabel="x", ylabel="log(x)")
ax.legend()
fig.save("errorbar.png")

error bars

Stacked areas

stackplot(x, *ys, labels=[...]) stacks series into filled bands.

Fields, images & contours

imshow displays a 2D field as a colormapped image and returns a handle you can pass to Figure.colorbar. See colormaps & images for the full story.

"""Image / heatmap with a colorbar."""
import math

import pyplotrs as pp

n = 100
data = [[math.sin(i / 8) * math.cos(j / 10) for j in range(n)] for i in range(n)]
fig, ax = pp.subplots()
m = ax.imshow(data, cmap="viridis", extent=(-3, 3, -3, 3))
fig.colorbar(m, label="intensity")
ax.set(title="Heatmap", xlabel="x", ylabel="y")
fig.save("heatmap.png")

heatmap

Method What it draws
imshow(data) Colormapped image of a 2D array
matshow(data) imshow with matrix conventions (origin top-left, equal aspect)
spy(data) Sparsity pattern: a marker at each nonzero entry
pcolormesh(C) or pcolormesh(X, Y, C) Pseudocolor grid (pcolor is an alias)
hist2d(xs, ys, bins=...) 2D histogram as a colormapped image
hexbin(xs, ys, gridsize=...) Hexagonal binning colored by count
contour(Z) / contourf(Z) Contour lines / filled bands
quiver(x, y, u, v) Arrow field
streamplot(x, y, u, v) Streamlines of a vector field, RK4-integrated
"""Vector-field and matrix plot types: quiver, streamplot, stackplot, spy."""

import pyplotrs as pp

N = 21
xc = [-3 + 6 * j / (N - 1) for j in range(N)]
yc = [-3 + 6 * i / (N - 1) for i in range(N)]
# Solid-body rotation: u = -y, v = x.
U = [[-yc[i] for _ in range(N)] for i in range(N)]
V = [[xc[j] for j in range(N)] for _ in range(N)]

fig, axes = pp.subplots(nrows=2, ncols=2, figsize=(720, 560))
(ax_stream, ax_quiver), (ax_stack, ax_spy) = axes

ax_stream.streamplot(xc, yc, U, V, density=1.1)
ax_stream.set(title="streamplot", xlabel="x", ylabel="y")

step = 3
ax_quiver.quiver(
    [xc[::step] for _ in yc[::step]],
    [[y] * len(xc[::step]) for y in yc[::step]],
    [[-yc[i] for j in range(0, N, step)] for i in range(0, N, step)],
    [[xc[j] for j in range(0, N, step)] for i in range(0, N, step)],
    scale=0.25,
)
ax_quiver.set(title="quiver", xlabel="x")

months = list(range(12))
ax_stack.stackplot(
    months,
    [3 + i % 4 for i in months],
    [2 + (i * 2) % 5 for i in months],
    [4 + (i * 3) % 3 for i in months],
    labels=["solar", "wind", "hydro"],
    alpha=0.85,
)
ax_stack.set(title="stackplot", xlabel="month", ylabel="TWh")
ax_stack.legend()

sparse = [[1 if (i * j) % 4 == 0 or i == j else 0 for j in range(24)] for i in range(24)]
ax_spy.spy(sparse, markersize=3.5)
ax_spy.set(title="spy")

fig.save("fields.png")

vector and matrix fields

The binning, marching-squares and band-fill kernels all run in Rust. hist2d, hexbin, pcolormesh, contourf and the colormapped scatter all return a handle for Figure.colorbar.

contour levels are a hint

contour(levels=7) asks for about seven lines and puts them on multiples of a round step, the way matplotlib does, rather than slicing the data range into equal parts. contourf fills the bands of that same lattice, so lines drawn over fills of the same field land on band boundaries — which also means the outermost bands reach a little past the data, out to the round numbers.

Guides & shapes

Guides are drawn over the data and mark a threshold rather than plotting one, but they still have to be visible: a guide contributes the coordinate it sits at, so axhline(500) over data in 0..1 widens y to include 500 instead of landing outside the frame. It does not contribute the direction it spans — that span is an axes fraction, not data — so axhline never touches x. axline is the exception: it is infinite and has no extent to contribute.

Method What it draws
axhline(y) / axvline(x) Reference line across a fraction of the axes
axhspan(ymin, ymax) / axvspan(xmin, xmax) Shaded band, drawn behind the data
axline(xy1, xy2=/slope=) Infinite line, clipped to the plot rect
hlines(y, xmin, xmax) / vlines(x, ymin, ymax) Data-coordinate segments that do autoscale

hlines vs axhline

hlines takes data coordinates in both directions, so it autoscales in both. axhline spans a fraction of the axes in x, so only its y moves the view. Reach for axhline to mark a threshold, hlines to plot data.

Patches are shapes in data space, with facecolor / edgecolor / linewidth / linestyle / alpha / fill / hatch:

Method What it draws
rectangle(xy, width, height, angle=0) Rectangle from its lower-left corner
circle(xy, radius) Circle (an ellipse unless the aspect is equal)
ellipse(xy, width, height, angle=0) Ellipse from its full diameters
polygon(points) Polygon through data-space vertices
fill(x, y) The same, from parallel x/y arrays
arrow(x, y, dx, dy) Arrow from (x, y) to (x + dx, y + dy)

Layering

Marks draw in the order you add them, which is usually all the control you need and the one thing you can read straight off the code. When something has to sit above a mark added after it, give it a higher zorder:

ax.line(xs, ys, zorder=2)              # drawn last despite being added first
ax.fill_between(xs, ys, 0, zorder=1)

Ties keep insertion order, so setting zorder on one mark does not reshuffle the rest. Guides and patches always draw above the data marks.

Other axes kinds

Axes is the Cartesian 2D vocabulary. Polar and 3D axes have their own — reach for them with projection="polar" / projection="3d".