Basic Icicle Chart — plotnine

An icicle chart displaying hierarchical data as adjacent rectangles in a layered structure, where each rectangle's size represents its value in the hierarchy. Unlike treemaps that nest rectangles, icicle charts stack them in rows (horizontal) or columns (vertical), making parent-child relationships explicitly visible through spatial adjacency. This layout excels at showing both the hierarchy levels and the proportional values simultaneously.

Basic Icicle Chart rendered with plotnine

Python source (plotnine)

""" anyplot.ai
icicle-basic: Basic Icicle Chart
Library: plotnine 0.15.4 | Python 3.13.13
Quality: 92/100 | Updated: 2026-05-13
"""

import os

import pandas as pd
from plotnine import (
    aes,
    element_rect,
    element_text,
    geom_rect,
    geom_text,
    ggplot,
    labs,
    scale_fill_manual,
    theme,
    theme_void,
)


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

# Okabe-Ito palette for hierarchy levels
IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD"]

# Data: File system hierarchy with sizes (MB)
data = [
    {"name": "root", "parent": "", "value": 0},
    {"name": "Documents", "parent": "root", "value": 0},
    {"name": "Photos", "parent": "root", "value": 0},
    {"name": "Projects", "parent": "root", "value": 0},
    {"name": "Archive", "parent": "root", "value": 0},
    {"name": "Reports", "parent": "Documents", "value": 450},
    {"name": "Invoices", "parent": "Documents", "value": 280},
    {"name": "Notes", "parent": "Documents", "value": 180},
    {"name": "Vacation", "parent": "Photos", "value": 680},
    {"name": "Family", "parent": "Photos", "value": 520},
    {"name": "Events", "parent": "Photos", "value": 340},
    {"name": "WebApp", "parent": "Projects", "value": 0},
    {"name": "DataSci", "parent": "Projects", "value": 0},
    {"name": "Mobile", "parent": "Projects", "value": 380},
    {"name": "Frontend", "parent": "WebApp", "value": 320},
    {"name": "Backend", "parent": "WebApp", "value": 420},
    {"name": "Config", "parent": "WebApp", "value": 200},
    {"name": "Models", "parent": "DataSci", "value": 520},
    {"name": "Scripts", "parent": "DataSci", "value": 300},
    {"name": "Old2023", "parent": "Archive", "value": 1200},
    {"name": "Old2022", "parent": "Archive", "value": 950},
]

df = pd.DataFrame(data)

# Build lookup tables
name_to_idx = {row["name"]: idx for idx, row in df.iterrows()}
children_map = {name: df[df["parent"] == name]["name"].tolist() for name in df["name"]}

# Calculate values for non-leaf nodes (bottom-up aggregation)
processed = set()
while len(processed) < len(df):
    for _, row in df.iterrows():
        name = row["name"]
        if name in processed:
            continue
        kids = children_map[name]
        if len(kids) == 0:
            processed.add(name)
        elif all(k in processed for k in kids):
            total = sum(df.loc[name_to_idx[k], "value"] for k in kids)
            df.loc[name_to_idx[name], "value"] = total
            processed.add(name)

# Calculate depths (distance from root)
depths = {"root": 0}
queue = ["root"]
while queue:
    current = queue.pop(0)
    for child in children_map[current]:
        depths[child] = depths[current] + 1
        queue.append(child)

max_depth = max(depths.values())

# Build icicle rectangles using iterative BFS
rects = []
layout_queue = [("root", 0.0, 1.0)]

while layout_queue:
    name, x_start, x_end = layout_queue.pop(0)
    depth = depths[name]
    y_top = max_depth - depth + 1
    y_bottom = max_depth - depth
    value = df.loc[name_to_idx[name], "value"]

    rects.append(
        {"name": name, "xmin": x_start, "xmax": x_end, "ymin": y_bottom, "ymax": y_top, "depth": depth, "value": value}
    )

    # Queue children proportionally
    kids = children_map[name]
    if kids:
        kid_values = [(k, df.loc[name_to_idx[k], "value"]) for k in kids]
        kid_values.sort(key=lambda x: -x[1])
        total_value = sum(v for _, v in kid_values)
        if total_value > 0:
            curr_x = x_start
            for kid, val in kid_values:
                width = (val / total_value) * (x_end - x_start)
                layout_queue.append((kid, curr_x, curr_x + width))
                curr_x += width

rect_df = pd.DataFrame(rects)

# Color palette by depth using Okabe-Ito
colors = {0: IMPRINT[0], 1: IMPRINT[1], 2: IMPRINT[2], 3: IMPRINT[3], 4: IMPRINT[4], 5: IMPRINT[5]}
rect_df["fill_color"] = rect_df["depth"].map(colors)

# Calculate label positions and widths
rect_df["width"] = rect_df["xmax"] - rect_df["xmin"]
rect_df["x_center"] = (rect_df["xmin"] + rect_df["xmax"]) / 2
rect_df["y_center"] = (rect_df["ymin"] + rect_df["ymax"]) / 2

# Labels: show name + value for wide rectangles, name only for medium, hide for very narrow
rect_df["label"] = rect_df.apply(
    lambda r: f"{r['name']}\n({int(r['value'])} MB)" if r["width"] > 0.05 else (r["name"] if r["width"] > 0.02 else ""),
    axis=1,
)

# Convert depth to categorical with descriptive labels
level_labels = {
    0: "Level 0 (Root)",
    1: "Level 1 (Folders)",
    2: "Level 2 (Subfolders)",
    3: "Level 3 (Groups)",
    4: "Level 4 (Items)",
    5: "Level 5 (Leaf)",
}
rect_df["depth_label"] = pd.Categorical(
    rect_df["depth"].map(level_labels), categories=list(level_labels.values()), ordered=True
)

# Separate light and dark backgrounds for text color contrast
dark_bg = rect_df[rect_df["depth"].isin([0, 1, 3])]
light_bg = rect_df[rect_df["depth"].isin([2, 4, 5])]

# Create plot
plot = (
    ggplot(rect_df)
    + geom_rect(aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax", fill="depth_label"), color="white", size=1.5)
    + geom_text(aes(x="x_center", y="y_center", label="label"), data=dark_bg, size=11, color=INK)
    + geom_text(
        aes(x="x_center", y="y_center", label="label"),
        data=light_bg,
        size=11,
        color=INK if THEME == "light" else INK_SOFT,
    )
    + scale_fill_manual(
        values={
            "Level 0 (Root)": IMPRINT[0],
            "Level 1 (Folders)": IMPRINT[1],
            "Level 2 (Subfolders)": IMPRINT[2],
            "Level 3 (Groups)": IMPRINT[3],
            "Level 4 (Items)": IMPRINT[4],
            "Level 5 (Leaf)": IMPRINT[5],
        },
        name="Hierarchy Level",
    )
    + labs(title="icicle-basic · plotnine · anyplot.ai")
    + theme_void()
    + theme(
        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_background=element_rect(fill=PAGE_BG),
        figure_size=(16, 9),
        plot_title=element_text(size=28, ha="center", weight="bold", color=INK),
        legend_position="right",
        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
        legend_title=element_text(size=18, color=INK),
        legend_text=element_text(size=14, color=INK_SOFT),
    )
)

# Save
plot.save(f"plot-{THEME}.png", dpi=300, verbose=False)

Part of Basic Icicle Chart on anyplot.ai.

Other implementations