Pictogram Chart (Isotype Visualization) — lets-plot

A pictogram chart represents quantities using repeated icons or symbols, where each icon stands for a fixed number of units. Inspired by Otto Neurath's ISOTYPE system, this visualization makes numerical comparisons more intuitive and engaging than plain bar charts. It is especially effective for public-facing data communication and infographics where visual appeal and immediate comprehension are important.

Pictogram Chart (Isotype Visualization) rendered with lets-plot

Python source (lets-plot)

""" anyplot.ai
pictogram-basic: Pictogram Chart (Isotype Visualization)
Library: letsplot 4.10.1 | Python 3.13.13
Quality: 88/100 | Updated: 2026-06-03
"""

import os

from lets_plot import *


LetsPlot.setup_html()

# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

# Subtle alternating lane color derived from theme surface
LANE_BG = "#E4E2DB" if THEME == "light" else "#2A2A26"

# Imprint palette — canonical order, first series always #009E73
IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030"]

# Data — Top coffee-producing countries (thousands of metric tonnes, ~2023)
categories = ["Brazil", "Vietnam", "Colombia", "Indonesia", "Ethiopia"]
values = [45, 32, 14, 11, 8]
icon_value = 5  # Each icon represents 5 thousand metric tonnes

max_icons = max(v // icon_value + (1 if v % icon_value else 0) for v in values)

MIN_ALPHA = 0.33  # floor so faintest partial icon remains clearly visible

# Build pictogram grid — full icons at alpha=1.0, partial via fractional alpha
tile_data = {"category": [], "col": [], "row": [], "alpha": [], "value": []}

for i, (cat, val) in enumerate(zip(categories, values)):
    y_pos = len(categories) - 1 - i  # Highest value at top
    full_icons = val // icon_value
    remainder = val % icon_value
    for c in range(full_icons):
        tile_data["category"].append(cat)
        tile_data["col"].append(float(c))
        tile_data["row"].append(float(y_pos))
        tile_data["alpha"].append(1.0)
        tile_data["value"].append(val)
    if remainder > 0:
        tile_data["category"].append(cat)
        tile_data["col"].append(float(full_icons))
        tile_data["row"].append(float(y_pos))
        tile_data["alpha"].append(max(MIN_ALPHA, remainder / icon_value))
        tile_data["value"].append(val)

# Alternating background lanes for readability (even category indices)
even_lanes = {
    "ymin": [float(len(categories) - 1 - i) - 0.48 for i in range(len(categories)) if i % 2 == 0],
    "ymax": [float(len(categories) - 1 - i) + 0.48 for i in range(len(categories)) if i % 2 == 0],
}

# Value labels placed to the right of each row
label_data = {
    "col": [float(max_icons) + 0.3] * len(categories),
    "row": [float(len(categories) - 1 - i) for i in range(len(categories))],
    "label": [f"{v}k MT" for v in values],
}

# Top producer annotation for data storytelling
anno_data = {"col": [float(max_icons) + 0.3], "row": [float(len(categories) - 1) + 0.42], "label": ["★ Top producer"]}

# Icon scale legend note in bottom-left corner for self-contained chart
scale_note = {"col": [-0.45], "row": [-0.44], "label": ["● = 5k MT"]}

y_breaks = [float(len(categories) - 1 - i) for i in range(len(categories))]

title = "pictogram-basic · python · letsplot · anyplot.ai"
subtitle = "Coffee Production by Country — Each icon represents 5 thousand metric tonnes"

# Plot
plot = (
    ggplot()
    + geom_rect(
        aes(ymin="ymin", ymax="ymax"), data=even_lanes, xmin=-0.6, xmax=float(max_icons) + 2.2, fill=LANE_BG, size=0
    )
    + geom_point(
        aes(x="col", y="row", alpha="alpha", fill="category"),
        data=tile_data,
        shape=21,
        size=12,
        color=PAGE_BG,
        stroke=1.5,
        tooltips=layer_tooltips().line("@category").line("Total: @value thousand metric tonnes").format("@value", "d"),
    )
    + scale_alpha_identity()
    + scale_fill_manual(values=IMPRINT_PALETTE, limits=categories)
    + geom_text(aes(x="col", y="row", label="label"), data=label_data, size=5, color=INK_SOFT, hjust=0, fontface="bold")
    + geom_text(
        aes(x="col", y="row", label="label"),
        data=anno_data,
        size=4,
        color=IMPRINT_PALETTE[0],
        hjust=0,
        fontface="italic",
    )
    + geom_text(
        aes(x="col", y="row", label="label"), data=scale_note, size=3.5, color=INK_SOFT, hjust=0, fontface="italic"
    )
    + scale_y_continuous(breaks=y_breaks, labels=categories, limits=[-0.6, len(categories) - 0.3], expand=[0, 0])
    + scale_x_continuous(limits=[-0.6, float(max_icons) + 2.3], expand=[0, 0])
    + labs(x="", y="", title=title, subtitle=subtitle)
    + ggsize(800, 450)
    + theme_minimal()
    + theme(
        plot_title=element_text(size=16, face="bold", color=INK),
        plot_subtitle=element_text(size=11, color=INK_SOFT),
        axis_title=element_blank(),
        axis_text_y=element_text(size=10, face="bold", color=INK),
        axis_text_x=element_blank(),
        axis_ticks=element_blank(),
        panel_grid=element_blank(),
        legend_position="none",
        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG, size=0),
        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG, size=0),
    )
)

# Save
ggsave(plot, f"plot-{THEME}.png", path=".", scale=4)
ggsave(plot, f"plot-{THEME}.html", path=".")

Part of Pictogram Chart (Isotype Visualization) on anyplot.ai.

Other implementations