User Guide¶
Install¶
pip install pavement
You can install your backend of choice separately, or explicitly pip
install pavement[matplotlib] (or bokeh, plotly, holoviews).
Usage¶
Pick a backend by importing its submodule. Every backend exposes the same
plot, so the import line is the only thing you change to switch:
import pavement.matplotlib as pavement # or .bokeh / .plotly / .holoviews
pavement.plot([1, 2, 3, 4, 5])
plot accepts the same three input shapes on every backend — a single dataset,
a wide list of datasets, or tidy data plus categories — along with bins (use
bins=None for a rug), weights, positions, widths, labels, and
orientation. It returns that framework's native object (matplotlib artists, a
bokeh.plotting.figure, a plotly.graph_objects.Figure, or a HoloViews
element), so the result drops straight into the rest of your workflow.
A rug (bins=None) drops the two long box edges by default, leaving just the
value ticks — so it reads like an ordinary rug plot, and the presence of the box
is a quick visual cue that you're looking at quantiles rather than raw points.
Pass show_box=True to keep the box on a rug (or show_box=False to drop it
from a binned plot); it is resolved per row, so a mixed bins sequence gets the
right default for each.
The backend-agnostic statistics live at the top level, with no plotting dependency of their own:
import pavement
pavement.pavement_stats([1, 2, 3, 4, 5], bins=4) # quantile cut points
pavement.quantiles([1, 2, 3, 4, 5], [0.25, 0.5, 0.75])
Missing values (NaN, None, pandas NA/NaT) are dropped before the
quantiles are computed, so they can't skew the cut points. The column summaries
behind summary are here too: pavement.tally_stats (a column's distinct /
repeated / missing make-up) and pavement.proportion_stats (value counts, like
pandas value_counts).
matplotlib (pavement.matplotlib)¶
The static backend draws pavements as matplotlib artists on an Axes:
import pavement.matplotlib as pavement
pavement.plot([1, 2, 3, 4, 5])
It also has three things specific to matplotlib: plot2d for 2D pavements (a
grid where every cell holds an equal share of the data), margin for a single
marginal strip — a richer drop-in for a rug plot — placed just inside or outside
any edge of an existing plot, and spark for a borderless, word-sized image
that drops inline into text:
pavement.spark(values, path="spark.png") #  in your prose
Inline sparklines (pavement.svg)¶
For sparklines on the web, pavement.svg emits a self-contained <svg> string
you can drop straight into HTML — no plotting library, no JavaScript, no image
files. It has no dependencies, so it ships with the base install.
import pavement.svg as pavement
html = pavement.spark([1, 2, 3, 4, 5]) # an <svg>...</svg> string
The result is built for running text. Lines default to currentColor, so a
spark inherits the surrounding font color (dark mode included), and it scales
with the text (height: 1em by default) while staying crisp at any size. Every
equal-mass bin is a hover target carrying its quantile band and value range as a
native <title> tooltip — the same hover the Bokeh and Plotly backends show —
with a CSS :hover highlight, all without a line of JavaScript. The bin or value
line under the cursor also highlights, so the interactivity is discoverable. A
bins=None rug makes each value hoverable when there are few of them — along
with the spaces between them, so a wide gap is as easy to hover as a value line
is hard to hit — or shows a single whole-spark summary when there are many
(tunable with tick_hover_limit). The tooltip values format through
value_format like the other backends (e.g.
value_format=lambda v: f"${v:,.2f}"). Pass color, orientation, or
path="spark.svg" / path="spark.html" to save.
This is the web counterpart of pavement.matplotlib.spark, which renders the
same idea to a raster image for print.
Alongside spark, pavement.svg has two column-summary strips in the same
borderless form factor: tally, which shows how much of a column is distinct,
duplicate, or missing, and proportion, which shows its value counts (like
pandas value_counts) with a catch-all for a long tail. Both take a column of
any type and return an <svg> string like spark does. See
examples/svg_demo.py.
Dataframe summaries (pavement.summary)¶
pavement.summary turns a whole dataframe, Series, or sequence into one inline
HTML table — the thing to glance at when data first lands (pictured at the top).
Each column becomes a row pairing its tally (how much is distinct, duplicate,
or missing) with its distribution: a pavement spark for ordered columns —
numbers, decimals, and dates/datetimes (a temporal column is laid out on a time
axis) — and a proportion strip for categorical ones, so every column gets a
distribution view where a pavement alone would leave the categorical rows blank.
A dataframe is topped by a row summarizing the frame itself — its row count and a
tally that treats each whole row as the entity, so "duplicate" means a
duplicated row and "missing" a row that is entirely blank.
import pavement
pavement.summary(df) # renders inline in a Jupyter cell
The result renders itself in Jupyter (via _repr_html_), so it
appears on its own when it's the last line of a cell. summary
accepts a pandas or polars DataFrame or Series, a plain dict of
columns (no pandas required), or any 1D sequence. A numeric column's
resolution adapts to its number of distinct values — a rug when few,
then 4, 8, or 16 equal-mass bins as it grows — so a small column reads
value-by-value and a large one as a smooth shape. It has no
dependencies; the strips are pure SVG, and the only JavaScript is the
optional drag-to-reorder (a grip handle on each row, off with
draggable=False). str() gives the HTML fragment and
path="summary.html" saves a standalone page. See
examples/summary_demo.py.
Tighter dataframe integration (pavement.pandas, pavement.polars)¶
For pandas or polars users, importing pavement.pandas (or pavement.polars)
registers a .pave accessor on DataFrame and Series — through each library's
own accessor/namespace API, so it's namespaced and won't clash — putting the
strips a method away:
import pavement.pandas # registers .pave (or: import pavement.polars)
df.pave() # the whole-frame summary, rendered inline
df.pave.summary() # the same, spelled out
df.pave.spark("price") # a numeric column's pavement sparkline
df.pave.tally("plan") # a column's distinct/duplicate/missing strip
df.pave.proportion("plan") # a column's value-counts strip
df["price"].pave.spark() # on a Series, the helpers take no column name
The two read identically; pavement.summary(df) itself also accepts a frame from
either library directly. The single-column helpers return the glyph's <svg>
string, but wrapped so it also renders inline in a notebook (it's a str
subclass, so it still embeds and saves like the plain string elsewhere). You can
also make the summary a frame's default notebook display — strictly opt-in, since
it replaces the usual data-table preview:
pavement.pandas.enable_repr() # every DataFrame/Series previews as a summary
pavement.pandas.disable_repr() # restore the library's normal display
The integration activates on import pavement.pandas / import pavement.polars
(never on a bare import pavement), in the spirit of import hvplot.pandas, so
the core package stays dependency-free.
Interactive plots (Plotly)¶
pavement.plotly targets Plotly directly. It builds pavements from plain
plotly.graph_objects traces (no figure-level shapes), so a pavement carries its
own hover and drops into any subplot cell:
import pavement.plotly as pavement
pavement.plot([1, 2, 3, 4, 5]).show()
Every interactive backend formats the values it shows on hover the same way:
pass value_format, a function from a value to its display string, and the hover
renders through it. The one callable works unchanged on Plotly, Bokeh,
HoloViews, and pavement.svg, so lambda v: f"${v:,.2f}" reads 1200.0 as
$1,200.00 everywhere (it defaults to three significant figures). See
examples/value_format_demo.py.
pavement.plot(prices, value_format=lambda v: f"${v:,.2f}").show()
A pavement is a drop-in for a rug plot, including as a marginal: with_marginals
adjoins pavement strips to a scatter — x on top, y on the right — in the spirit
of Plotly's own marginal plots,
keeping them aligned with the scatter and matching its per-category colors:
import plotly.express as px
import pavement.plotly as pavement
df = px.data.iris()
fig = px.scatter(df, x="sepal_width", y="sepal_length", color="species")
pavement.with_marginals(fig, x=df.sepal_width, y=df.sepal_length,
categories=df.species).show()
Install with pip install pavement[plotly]. See
examples/interactive_demo.py.
Interactive plots (Bokeh)¶
pavement.bokeh draws pavements with plain Bokeh glyphs (filled quads for the
bins, segments for the ticks and box edges), so each row carries its own hover
and drops onto any figure:
import pavement.bokeh as pavement
from bokeh.plotting import show
show(pavement.plot([1, 2, 3, 4, 5]))
It returns a plain bokeh.plotting.figure, with a hover tool over the bins and
ticks and a clickable legend for multiple rows. As with the other backends,
with_marginals arranges a scatter with pavement strips — x on top, y on the
right — with their ranges linked to the scatter and matching its per-category
colors:
from bokeh.plotting import figure
import pavement.bokeh as pavement
scatter = figure()
for g in ["A", "B"]:
scatter.scatter(xs[g], ys[g], color=palette[g], name=g)
show(pavement.with_marginals(scatter, x=xs_all, y=ys_all, categories=groups))
Install with pip install pavement[bokeh]. See
examples/interactive_demo.py.
Interactive plots (HoloViews)¶
pavement.holoviews builds the same pavement geometry as HoloViews elements, so
one definition renders through any HoloViews backend (bokeh or plotly for
interactivity, matplotlib for a static image). Select the backend with
hv.extension(...) first, as usual:
import holoviews as hv
import pavement.holoviews as pavement
hv.extension("bokeh")
pavement.plot([1, 2, 3, 4, 5])
It returns a plain HoloViews object, so it composes with the framework.
with_marginals adjoins category-split pavement marginals to a scatter in one
call:
pavement.with_marginals(scatter, x=xs, y=ys, categories=groups)
Install with pip install pavement[holoviews] (plus bokeh and/or plotly).
See examples/interactive_demo.py.
Using pavement with Claude¶
This repo ships a Claude Code plugin that teaches
Claude to use pavement correctly — which backend to import, the three plot
input shapes, and the idioms that are easy to get wrong from memory (bins=None
rugs, the per-row show_box default, value_format).
Add this repo as a plugin marketplace and install it:
/plugin marketplace add ajschumacher/pavement
/plugin install pavement-plots@pavement
Once installed, Claude consults the skill automatically whenever you ask it to make a pavement plot or sparkline. To try it without installing — or when working in a clone of this repo — load it directly for one session:
claude --plugin-dir ./plugins/pavement-plots
The skill itself is plain Markdown at
plugins/pavement-plots/skills/pavement-plots/, so you can read or adapt it
without Claude Code.
Development¶
pip install -e '.[test]' # core only
pip install -e '.[test,matplotlib]' # + matplotlib
pip install -e '.[test,all]' # + every backend
pytest
The images at the top of this README are regenerated by
examples/readme_assets.py (the summary screenshot
additionally needs pandas and a headless Chrome via selenium).