Circle Packing Chart — Seaborn

A circle packing chart displays hierarchical data as nested circles, where each circle contains smaller circles representing its children. Circle size is proportional to node value, and circles are packed efficiently without overlap. This visualization excels at revealing hierarchical structures while simultaneously showing quantitative relationships through area encoding.

Circle Packing Chart rendered with Seaborn

Renders

Python source (Seaborn)

""" anyplot.ai
circlepacking-basic: Circle Packing Chart
Library: seaborn 0.13.2 | Python 3.13.13
Quality: 93/100 | Created: 2026-05-11
"""

import os

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib.patches import Circle


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"

IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477"]

# Data: File system hierarchy
np.random.seed(42)
nodes = []

node_id = 0
nodes.append({"id": node_id, "parent": None, "value": 100, "label": "root/", "depth": 0})
node_id += 1

folders = ["Documents", "Media", "Projects", "Archive"]
for folder in folders:
    size = np.random.uniform(15, 25)
    nodes.append({"id": node_id, "parent": 0, "value": size, "label": folder, "depth": 1})
    parent_id = node_id
    node_id += 1

    for i in range(np.random.randint(3, 5)):
        file_size = np.random.uniform(2, 6)
        nodes.append({"id": node_id, "parent": parent_id, "value": file_size, "label": f"item_{i + 1}", "depth": 2})
        node_id += 1

df = pd.DataFrame(nodes)


def pack_circles(df):
    positions = {}

    # Root at center
    root = df[df["parent"].isna()].iloc[0]
    root_radius = np.sqrt(root["value"]) * 3
    positions[root["id"]] = (0, 0, root_radius)

    # Level 1 nodes (direct children of root)
    level_1 = df[df["parent"] == root["id"]].sort_values("value", ascending=False)
    angle_step = 2 * np.pi / len(level_1)
    distance_from_root = root_radius + 35

    for idx, (_, node) in enumerate(level_1.iterrows()):
        radius = np.sqrt(node["value"]) * 3
        angle = idx * angle_step
        x = distance_from_root * np.cos(angle)
        y = distance_from_root * np.sin(angle)
        positions[node["id"]] = (x, y, radius)

    # Level 2 nodes (grandchildren)
    for _, parent_node in level_1.iterrows():
        parent_id = parent_node["id"]
        parent_x, parent_y, parent_radius = positions[parent_id]

        children = df[df["parent"] == parent_id]
        if len(children) == 0:
            continue

        angle_step = 2 * np.pi / len(children)
        distance = parent_radius + 18

        for idx, (_, child_node) in enumerate(children.iterrows()):
            radius = np.sqrt(child_node["value"]) * 3
            angle = idx * angle_step
            x = parent_x + distance * np.cos(angle)
            y = parent_y + distance * np.sin(angle)
            positions[child_node["id"]] = (x, y, radius)

    return positions


positions = pack_circles(df)

# Plot
fig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)
ax.set_facecolor(PAGE_BG)

# Draw circles
for _, row in df.iterrows():
    x, y, radius = positions[row["id"]]
    depth = row["depth"]

    color = IMPRINT[depth % len(IMPRINT)]
    circle = Circle((x, y), radius, facecolor=color, edgecolor=INK_SOFT, linewidth=2, alpha=0.85)
    ax.add_patch(circle)

    # Label for larger circles
    if radius > 8:
        text_color = PAGE_BG if depth == 0 else INK
        ax.text(
            x,
            y,
            row["label"],
            ha="center",
            va="center",
            fontsize=14 if depth == 0 else 12,
            color=text_color,
            fontweight="bold",
        )

# Set limits
all_x = [pos[0] for pos in positions.values()]
all_y = [pos[1] for pos in positions.values()]
all_r = [pos[2] for pos in positions.values()]
xlim = (min(all_x) - max(all_r) - 15, max(all_x) + max(all_r) + 15)
ylim = (min(all_y) - max(all_r) - 15, max(all_y) + max(all_r) + 15)
ax.set_xlim(xlim)
ax.set_ylim(ylim)
ax.set_aspect("equal")

# Remove axes
ax.set_xticks([])
ax.set_yticks([])
for spine in ax.spines.values():
    spine.set_visible(False)

# Title
ax.text(
    0.5,
    0.98,
    "circlepacking-basic · seaborn · anyplot.ai",
    ha="center",
    va="top",
    transform=ax.transAxes,
    fontsize=24,
    fontweight="medium",
    color=INK,
)

plt.tight_layout()
plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG)
plt.close()

Part of Circle Packing Chart on anyplot.ai.

Other implementations