Pebble Bar Chart#
The pebble bar chart is a logarithmic-density stacked bar chart where every data point is individually visible. Items stack from large squares at the bottom to tiny ones at the top, creating a natural visual hierarchy that shows both exact counts and relative magnitudes — like a histogram where every data point gets its own pixel.
Each bar gets a watercolor wash by default — a turbulence texture that gives the chart a hand-painted, organic look. Items with images sit on top of the wash; items without images are represented by the wash alone.
Every bar is drawn in Python (numpy and Pillow) to a single image: the layout, pictures, white outlines, rounded corners and watercolor all go into it. The same images feed three outputs:
Function |
Output |
Use it for |
|---|---|---|
|
Interactive HTML page |
Reports where items show tooltips and link to detail pages |
|
matplotlib figure |
Static PNG/PDF charts and notebooks |
|
One image per bar + |
Websites that place the chart in their own page with |
The browser never draws pebbles. The HTML page loads one image per bar and a
small script (pebble-view.js) that maps the pointer to the item under it, so
a chart with thousands of items loads as fast as one with ten.
Quick start#
from matviz import pebble_bar_chart
categories = [
{"name": "trucks", "label": "Trucks", "color": "#b87200",
"items": [{"id": str(i)} for i in range(87)]},
{"name": "vans", "label": "Vans", "color": "#186b4e",
"items": [{"id": str(i)} for i in range(54)]},
{"name": "sedans", "label": "Sedans", "color": "#4e82b8",
"items": [{"id": str(i)} for i in range(18)]},
]
pebble_bar_chart(
categories,
"fleet.html",
title="Vehicle Fleet",
subtitle="Each square = one vehicle",
bar_width=150,
log_base=1.1,
item_offset=16,
h_squeeze=0.7,
bar_gap=21,
)
Open fleet.html in a browser to see the chart. The page is a single file:
the bar images are embedded in it.
The same chart as a static matplotlib figure:
from matviz import pebble_bar_figure
fig = pebble_bar_figure(categories, bar_width=150, log_base=1.1,
item_offset=16, h_squeeze=0.7, bar_gap=21)
fig.savefig("fleet.png", dpi=144, bbox_inches="tight")
How it works#
Each bar is a vertical stack of rows. Row r (counting from 0 at the bottom)
has floor(base^r) columns. Each cell is barWidth / base^r pixels wide and
tall (square). The logarithmic stacking means:
Bottom rows: 1–2 large squares per row (prominent, individually visible)
Top rows: many tiny squares packed together (conveys volume)
The log_base parameter controls how fast columns multiply:
|
Behavior |
|---|---|
√2 ≈ 1.414 (default) |
Columns double every 2 rows — balanced |
1.1 |
Slow growth — tall, narrow pyramids |
2.0 |
Fast growth — short, wide pyramids |
Item data#
Each item in a category can carry:
Key |
Type |
Description |
|---|---|---|
|
str |
Unique identifier |
|
str |
Tooltip text on hover |
|
str |
URL opened on click |
|
str |
Local image file drawn in the cell (relative to |
|
PIL.Image |
An already-loaded image, instead of |
items = [
{"id": "v001", "label": "Truck #1", "link": "https://example.com/v001",
"src": "images/v001.png"},
{"id": "v002", "label": "Truck #2"}, # represented by the watercolor wash
]
pebble_bar_chart(categories, "fleet.html", image_dir="data/")
Pictures are stretched to fill their square, given a white outline that
follows the image’s alpha mask (outline_radius, default 1px) and rounded
corners. Transparent cutouts (PNG with alpha) look best. Items without
images are represented by the wash alone. Images must be local files: download
remote images first, or pass image_loader= a function that returns a PIL image
for an item. A missing file stops the build with the item’s id.
Watercolor styling#
Watercolor is on by default — the color you set on each category is used
for the wash. You only need watercolor_color if you want the wash to be a
different shade from the base color:
categories = [
# Watercolor uses the color automatically
{"name": "alpha", "label": "Alpha", "color": "#b87200",
"items": [...]},
# Override: base color is gray, but wash is amber
{"name": "beta", "label": "Beta", "color": "#888",
"watercolor_color": "#b87200",
"items": [...]},
# Opt out of watercolor entirely — flat solid squares
{"name": "gamma", "label": "Gamma", "color": "#4e82b8",
"watercolor_color": "",
"items": [...]},
]
Key parameters#
Parameter |
Default |
Description |
|---|---|---|
|
120 |
Pixel width of each bar before squeeze |
|
√2 |
Log base controlling density growth |
|
0 |
Invisible padding items to skip sparse bottom rows |
|
1.0 |
Horizontal squeeze factor (0–1) |
|
8 |
Pixels between adjacent bars |
|
1 |
White outline around image items (0 to disable) |
|
True |
Sort categories largest first |
|
True |
Show item count next to labels |
|
2 |
Image pixels per CSS pixel. 2 is sharp on most high-density screens; 3 is a little sharper on some phones at about twice the file size |
|
4 |
Drawing resolution multiplier before shrinking to |
|
0 |
Fixes which pebble overlaps which and the watercolor texture. The same seed always gives identical images |
|
True |
( |
|
None |
Base folder for relative |
|
False |
Open the file in the default browser |
Using the chart on your own website#
write_pebble_assets writes one image per bar and a manifest.json. Copy
matviz/_assets/pebble-view.js into the site and draw the chart with it:
from matviz import write_pebble_assets
write_pebble_assets(categories, "site/static/chart/", bar_width=150,
log_base=1.1, item_offset=16, h_squeeze=0.7, bar_gap=21)
<div id="chart"></div>
<script src="pebble-view.js"></script>
<script>
fetch("/static/chart/manifest.json").then(r => r.json()).then(data => {
const chart = renderPebbleView(document.getElementById("chart"), data,
{ baseUrl: "/static/chart/" });
// chart.yAxis can be moved outside a horizontal scroll container
// to keep the axis pinned; chart.bars are the bar elements.
});
</script>
The images are built once, offline. Rebuild them only when the data or chart options change.
Real-world examples#
Freer Gallery Japanese Ceramics#
251 objects from the Smithsonian — each pebble is a clickable thumbnail. View the live chart →
What Blocks Bike Lanes?#
Urban infrastructure data visualized as a pebble bar chart. View the article →
Full example#
See examples/pebble_bar_example.py in the repository for a complete working
example with multiple categories.
API reference#
- matviz.pebble_bar.pebble_bar_chart(categories: Sequence[Dict[str, Any]], output: str | Path | None = None, *, title: str = '', subtitle: str = '', embed_images: bool = True, font_family: str = 'system-ui, -apple-system, sans-serif', extra_css: str = '', extra_js: str = '', open_browser: bool = False, **opts) str[source]#
Generate an interactive pebble bar chart as an HTML page.
The bars are drawn in Python; the page only shows the bar images and maps the pointer to the item under it (tooltip from
labelorid, click openslink).- Parameters:
- categorieslist of dict
Each category dict has
name,label,color(also the default watercolor wash), optionalwatercolor_color(Noneor""for flat squares without a wash), anditems: dicts withid,label(tooltip),link(URL) andsrc(local image path) orimage(PIL image).- outputstr or Path or None
Path to write the HTML file. If None, the HTML string is returned without writing to disk.
- title, subtitlestr
Heading and description line.
- embed_imagesbool
True (default) inlines the bar images as data URIs so the chart is a single file. False writes them to
<output stem>_files/beside the page (requiresoutput).- font_family, extra_css, extra_js
Page styling and extra script run after the chart is drawn (
chartholds the return value ofrenderPebbleView).- open_browserbool
Open the file in the default browser after writing.
- **opts
Chart options for
render_pebble_chart():bar_width(default 120),log_base(√2),item_offset(0),h_squeeze(1.0),bar_gap(8),sort_desc,show_count,outline_radius(1),scale(2),supersample(4),seed(0),image_dir, …
- Returns:
- str
The HTML string. Also written to
outputif provided.
Examples
>>> categories = [ ... {"name": "trucks", "label": "Trucks", "color": "#b87200", ... "items": [{"id": str(i)} for i in range(45)]}, ... {"name": "vans", "label": "Vans", "color": "#186b4e", ... "items": [{"id": str(i)} for i in range(30)]}, ... ] >>> html = pebble_bar_chart(categories, "fleet.html", ... title="Vehicle Fleet", log_base=1.1, ... item_offset=16, h_squeeze=0.7)
- matviz.pebble_bar.pebble_bar_figure(categories: Sequence[Dict[str, Any]], *, ax=None, figsize=None, label_rotation=0, **opts)[source]#
Draw a static pebble bar chart with matplotlib.
Takes the same options as
render_pebble_chart(). Passaxto draw into existing axes. One axes unit is one CSS pixel of the HTML chart. Returns the figure.
- matviz.pebble_bar.write_pebble_assets(categories: Sequence[Dict[str, Any]], out_dir: str | Path, *, manifest_name: str = 'manifest.json', image_format: str = 'webp', quality: int = 90, item_fields: Sequence[str] = ('id', 'link', 'label'), **opts) Dict[str, Any][source]#
Write one image per bar and a manifest for
pebble-view.js.Writes
<out_dir>/<name>.<image_format>for each category and<out_dir>/<manifest_name>. Image paths in the manifest are relative to the manifest, so passbaseUrl(the manifest’s folder URL) torenderPebbleView. Other keyword arguments go torender_pebble_chart(). Returns the manifest.
- matviz.pebble_bar.render_pebble_chart(categories: Sequence[Dict[str, Any]], *, bar_width: float = 120, log_base: float = 1.4142135623730951, item_offset: int = 0, h_squeeze: float = 1.0, gutter: float = 0, bar_gap: float = 8, sort_desc: bool = True, show_count: bool = True, outline_radius: float = 1, background: str = '#faf8f2', scale: int = 2, supersample: int = 4, seed: int = 0, image_dir: str | Path | None = None, image_loader=None, min_tick: int = 3, skip_ticks: Sequence[int] = (1000,)) PebbleChart[source]#
Render every bar of a pebble chart (the shared step of all outputs).
- Parameters:
- categorieslist of dict
Each dict has
name,label,color(flat fill and default watercolor), optionalwatercolor_color(falsy for no wash) anditems. Items are drawn bottom-first in list order.- bar_width, log_base, item_offset, h_squeeze, gutter
Layout; see
matviz.pebble_layout.bar_rows().- bar_gapfloat
CSS px between bars.
- sort_descbool
Sort categories by item count, largest first.
- show_countbool
Show each bar’s item count beside its label.
- outline_radiusfloat
White outline around pictures, in CSS px (0 = none).
- backgroundstr
Page colour the watercolor is blended onto.
- scaleint
Image pixels per CSS px. 2 is sharp on most high-density screens; 3 is a little sharper on some phones at about twice the file size.
- supersampleint
Drawing resolution multiplier before shrinking to
scale; higher keeps more of the tiny top-row pebbles.- seedint
Seed for the overlap order and the watercolor texture. Each bar uses
seedmixed with its name, so rebuilding gives the same images.- image_dirpath, optional
Base directory for relative item
srcpaths.- image_loadercallable, optional
image_loader(item) -> PIL.Imageto load pictures some other way.- min_tick, skip_ticks
Y-axis ticks: 1-2-5 values from
min_tickup, minusskip_ticks.
- matviz.pebble_render.render_bar(items: Sequence[Dict[str, Any]], *, bar_width=120, log_base=1.4142135623730951, item_offset=0, gutter=0, h_squeeze=1.0, color='#888888', watercolor_color=None, background='#faf8f2', image_loader: Callable[[Dict[str, Any]], Image] | None = None, image_dir=None, outline_radius=1.0, corner_radius=None, scale=2, supersample=4, seed=0, wash_seeds=None) PebbleBar[source]#
Draw one pebble bar to an RGBA image.
- Parameters:
- itemslist of dict
Bar items, bottom (largest) first. Items with
src(a local file path, resolved againstimage_dir) orimage(a PIL image) are drawn as pictures. Without a picture, an item is a solidcolorsquare when there is no watercolor and is left to the wash otherwise.- bar_width, log_base, item_offset, gutter, h_squeeze
Layout, as in
matviz.pebble_layout.bar_rows().- colorstr
Fill for items without a picture (flat mode only).
- watercolor_colorstr or None
Colour of the watercolor wash behind the bar; falsy for no wash.
- backgroundstr
Page colour the wash is blended onto (the bar itself is opaque).
- image_loadercallable, optional
image_loader(item) -> PIL.Image. Defaults todefault_image_loader()withimage_dir.- outline_radiusfloat
White outline around each picture’s alpha, in CSS px (0 = none).
- corner_radiusfloat or callable, optional
Corner radius in CSS px, or
f(size). Defaultmax(1, min(3, size * 0.05)).- scaleint
Output pixels per CSS px (2 = sharp on most high-density screens).
- supersampleint
Extra drawing resolution before shrinking to
scale.- seedint
Seed for which pebble overlaps which; the same seed gives the same image.
- wash_seeds(int, int), optional
Noise seeds for the wash grain and edge warp. Default derived from
seed.
- Returns:
- PebbleBar

