Basic Icicle Chart — Pygal

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 Pygal

Python source (Pygal)

""" anyplot.ai
icicle-basic: Basic Icicle Chart
Library: pygal 3.1.0 | Python 3.13.13
Quality: 90/100 | Updated: 2026-05-13
"""

import os
import sys
import xml.etree.ElementTree as ET


sys.path.pop(0)

import cairosvg
from pygal.style import Style


# Theme tokens (see prompts/default-style-guide.md)
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"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Okabe-Ito palette
IMPRINT = (
    "#009E73",  # bluish green (brand)
    "#C475FD",  # vermillion
    "#4467A3",  # blue
    "#BD8233",  # reddish purple
    "#AE3030",  # orange
    "#2ABCCD",  # sky blue
    "#954477",  # yellow
)

# Data: Organizational hierarchy (Company -> Divisions -> Departments -> Teams)
hierarchy_data = [
    ("TechCorp", None, 0),
    ("Engineering", "TechCorp", 0),
    ("Operations", "TechCorp", 0),
    ("Sales", "TechCorp", 0),
    ("Backend", "Engineering", 0),
    ("Frontend", "Engineering", 0),
    ("DevOps", "Engineering", 0),
    ("HR", "Operations", 0),
    ("Finance", "Operations", 0),
    ("APAC", "Sales", 0),
    ("EMEA", "Sales", 0),
    ("APIs", "Backend", 45),
    ("Databases", "Backend", 50),
    ("Services", "Backend", 40),
    ("Web", "Frontend", 35),
    ("Mobile", "Frontend", 30),
    ("Infrastructure", "DevOps", 55),
    ("Recruiting", "HR", 25),
    ("Payroll", "HR", 15),
    ("Accounting", "Finance", 35),
    ("Planning", "Finance", 25),
    ("Enterprise", "APAC", 60),
    ("SMB", "APAC", 40),
    ("EMEA_Enterprise", "EMEA", 70),
    ("EMEA_SMB", "EMEA", 50),
]

# Build tree structure
nodes = {}
children = {}

for name, parent, value in hierarchy_data:
    nodes[name] = {"name": name, "parent": parent, "value": value}
    if parent is not None:
        if parent not in children:
            children[parent] = []
        children[parent].append(name)

# Calculate total values bottom-up
node_depths = {"TechCorp": 0}
queue = ["TechCorp"]
depth_order = []
while queue:
    current = queue.pop(0)
    depth_order.append(current)
    if current in children:
        for child in children[current]:
            node_depths[child] = node_depths[current] + 1
            queue.append(child)

node_values = {}
for node_name in reversed(depth_order):
    if node_name not in children:
        node_values[node_name] = nodes[node_name]["value"]
    else:
        node_values[node_name] = sum(node_values[child] for child in children[node_name])

# Calculate positions (horizontal icicle layout)
positions = {}
positions["TechCorp"] = {"x_start": 0, "x_end": 1, "depth": 0, "value": node_values["TechCorp"]}

for node_name in depth_order:
    if node_name in children:
        pos = positions[node_name]
        current_x = pos["x_start"]
        total_value = node_values[node_name]
        for child in children[node_name]:
            child_value = node_values[child]
            child_width = (child_value / total_value) * (pos["x_end"] - pos["x_start"])
            positions[child] = {
                "x_start": current_x,
                "x_end": current_x + child_width,
                "depth": pos["depth"] + 1,
                "value": child_value,
            }
            current_x += child_width

max_depth = max(pos["depth"] for pos in positions.values())

# Chart dimensions
WIDTH = 4800
HEIGHT = 2700
MARGIN_TOP = 140
MARGIN_BOTTOM = 120
MARGIN_LEFT = 60
MARGIN_RIGHT = 200
PLOT_WIDTH = WIDTH - MARGIN_LEFT - MARGIN_RIGHT
PLOT_HEIGHT = HEIGHT - MARGIN_TOP - MARGIN_BOTTOM

# Color by depth using Okabe-Ito subset with brand as primary
DEPTH_COLORS = [IMPRINT[i % len(IMPRINT)] for i in range(max_depth + 1)]

# Create pygal style for metadata extraction
custom_style = Style(
    background=PAGE_BG,
    plot_background=PAGE_BG,
    foreground=INK,
    foreground_strong=INK,
    foreground_subtle=INK_MUTED,
    colors=IMPRINT,
    title_font_size=28,
    label_font_size=22,
    major_label_font_size=18,
    legend_font_size=16,
)

# Build SVG manually (icicle requires custom rendering)
svg_ns = "http://www.w3.org/2000/svg"
ET.register_namespace("", svg_ns)

svg_root = ET.Element("svg", xmlns=svg_ns, width=str(WIDTH), height=str(HEIGHT), viewBox=f"0 0 {WIDTH} {HEIGHT}")
svg_root.set("style", f"background-color: {PAGE_BG};")

# Add title
title_elem = ET.SubElement(svg_root, "text")
title_elem.set("x", str(WIDTH / 2))
title_elem.set("y", "90")
title_elem.set("text-anchor", "middle")
title_elem.set("fill", INK)
title_elem.set("font-size", "28")
title_elem.set("font-family", "sans-serif")
title_elem.set("font-weight", "500")
title_elem.text = "icicle-basic · pygal · anyplot.ai"

# Create main group for rectangles
g = ET.SubElement(svg_root, "g")
g.set("class", "icicle-chart")

# Draw rectangles
row_height = PLOT_HEIGHT / (max_depth + 1)
gap = 2

for node_name, pos in positions.items():
    depth = pos["depth"]
    x_start = pos["x_start"]
    x_end = pos["x_end"]
    width = x_end - x_start

    # Calculate pixel positions
    px_x = MARGIN_LEFT + x_start * PLOT_WIDTH
    px_width = width * PLOT_WIDTH - gap
    px_y = MARGIN_TOP + depth * row_height
    px_height = row_height - gap

    # Color by depth from Okabe-Ito
    color = DEPTH_COLORS[depth % len(DEPTH_COLORS)]

    # Create rectangle
    rect = ET.SubElement(g, "rect")
    rect.set("x", f"{px_x:.1f}")
    rect.set("y", f"{px_y:.1f}")
    rect.set("width", f"{max(0, px_width):.1f}")
    rect.set("height", f"{px_height:.1f}")
    rect.set("fill", color)
    rect.set("stroke", PAGE_BG)
    rect.set("stroke-width", "2")

    # Add tooltip
    title = ET.SubElement(rect, "title")
    title.text = f"{node_name}: {pos['value']}"

    # Add label if rectangle is wide enough
    if px_width > 70:
        label = node_name.replace("_", " ")
        max_chars = max(3, int(px_width / 24))
        if len(label) > max_chars:
            label = label[: max_chars - 2] + ".."

        fontsize = min(22, max(14, int(px_width / 7)))

        text = ET.SubElement(g, "text")
        text.set("x", f"{px_x + px_width / 2:.1f}")
        text.set("y", f"{px_y + px_height / 2 + fontsize / 3:.1f}")
        text.set("text-anchor", "middle")
        text.set("dominant-baseline", "central")
        text.set("fill", INK)
        text.set("font-size", str(fontsize))
        text.set("font-family", "sans-serif")
        text.set("font-weight", "500")
        text.text = label

# Add depth level legend on right
level_labels = ["Company", "Division", "Department", "Team", "Function"]
labels_g = ET.SubElement(svg_root, "g")
labels_g.set("class", "level-labels")

for depth in range(max_depth + 1):
    y_pos = MARGIN_TOP + depth * row_height + row_height / 2
    level_label = level_labels[depth] if depth < len(level_labels) else f"Level {depth}"

    text = ET.SubElement(labels_g, "text")
    text.set("x", str(MARGIN_LEFT + PLOT_WIDTH + 30))
    text.set("y", f"{y_pos + 6:.1f}")
    text.set("fill", INK_SOFT)
    text.set("font-size", "16")
    text.set("font-family", "sans-serif")
    text.text = level_label

# Write SVG to file and render PNG
svg_output = ET.tostring(svg_root, encoding="unicode")

with open(f"plot-{THEME}.html", "w") as f:
    f.write(svg_output)

cairosvg.svg2png(bytestring=svg_output.encode("utf-8"), write_to=f"plot-{THEME}.png")

Part of Basic Icicle Chart on anyplot.ai.

Other implementations