Circle Packing Chart — D3.js

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 D3.js

Renders

JavaScript source (D3.js)

// anyplot.ai
// circlepacking-basic: Circle Packing Chart
// Library: d3 7.9.0 | JavaScript 22.23.2
// Quality: 93/100 | Created: 2026-09-02

//# anyplot-orientation: square

const t = window.ANYPLOT_TOKENS;
const { width, height } = window.ANYPLOT_SIZE;

// --- Data: repository storage breakdown (folders + files, KB) --------------
// Deterministic LCG so file sizes are reproducible without a real RNG.
let seed = 42;
const nextSize = (min, max) => {
  seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff;
  return min + Math.round((seed / 0x7fffffff) * (max - min));
};

const categories = [
  { id: "src", label: "src/" },
  { id: "assets", label: "assets/" },
  { id: "docs", label: "docs/" },
  { id: "tests", label: "tests/" },
  { id: "config", label: "config/" },
];

const folders = [
  { id: "src-components", parent: "src", label: "components/" },
  { id: "src-hooks", parent: "src", label: "hooks/" },
  { id: "src-utils", parent: "src", label: "utils/" },
  { id: "src-services", parent: "src", label: "services/" },
  { id: "assets-images", parent: "assets", label: "images/" },
  { id: "assets-fonts", parent: "assets", label: "fonts/" },
  { id: "assets-icons", parent: "assets", label: "icons/" },
  { id: "docs-guides", parent: "docs", label: "guides/" },
  { id: "docs-api", parent: "docs", label: "api/" },
  { id: "tests-unit", parent: "tests", label: "unit/" },
  { id: "tests-integration", parent: "tests", label: "integration/" },
];

const filesByParent = {
  "src-components": ["Header.tsx", "Footer.tsx", "Sidebar.tsx", "Chart.tsx", "Modal.tsx", "Button.tsx"],
  "src-hooks": ["useAuth.ts", "useFetch.ts", "useTheme.ts", "useDebounce.ts"],
  "src-utils": ["format.ts", "validate.ts", "parse.ts", "colors.ts", "dates.ts"],
  "src-services": ["api.ts", "storage.ts", "analytics.ts", "sockets.ts"],
  "assets-images": ["hero.png", "logo.png", "banner.png", "avatar.png", "og-image.png"],
  "assets-fonts": ["Inter.woff2", "Mono.woff2", "Serif.woff2"],
  "assets-icons": ["arrow.svg", "check.svg", "close.svg", "search.svg"],
  "docs-guides": ["quickstart.md", "deployment.md", "theming.md", "faq.md"],
  "docs-api": ["reference.md", "changelog.md", "migration.md"],
  "tests-unit": ["format.test.ts", "validate.test.ts", "parse.test.ts", "colors.test.ts", "dates.test.ts", "api.test.ts"],
  "tests-integration": ["auth.spec.ts", "checkout.spec.ts", "search.spec.ts", "onboarding.spec.ts"],
  config: ["tsconfig.json", "eslint.config.js", "vite.config.ts"],
};

const nodes = [{ id: "repo", parent: null, value: null, label: "repo/" }];
for (const c of categories) nodes.push({ id: c.id, parent: "repo", value: null, label: c.label });
for (const f of folders) nodes.push({ id: f.id, parent: f.parent, value: null, label: f.label });
for (const [parent, files] of Object.entries(filesByParent)) {
  const sizeRange = parent === "assets-images" ? [60, 480] : parent.startsWith("assets") ? [10, 90] : [4, 60];
  for (const name of files) {
    nodes.push({ id: `${parent}/${name}`, parent, value: nextSize(...sizeRange), label: name });
  }
}

// --- Hierarchy + pack layout -------------------------------------------------
const margin = { top: 110, right: 50, bottom: 50, left: 50 };
const diameter = Math.min(width - margin.left - margin.right, height - margin.top - margin.bottom);

const root = d3
  .stratify()
  .id((d) => d.id)
  .parentId((d) => d.parent)(nodes)
  .sum((d) => d.value || 0)
  .sort((a, b) => b.value - a.value);

d3.pack().size([diameter, diameter]).padding((d) => (d.depth === 0 ? 12 : d.depth === 1 ? 7 : 3))(root);

// --- Color: hue by top-level category, opacity deepens with nesting --------
const color = d3.scaleOrdinal(
  categories.map((c) => c.id),
  t.palette.slice(0, categories.length)
);
const categoryOf = (d) => d.ancestors().find((a) => a.depth === 1);
const opacityByDepth = { 1: 0.16, 2: 0.4, 3: 0.9 };

// --- SVG mount ----------------------------------------------------------------
const svg = d3.select("#container").append("svg").attr("width", width).attr("height", height);
const g = svg
  .append("g")
  .attr("transform", `translate(${(width - diameter) / 2},${margin.top + (height - margin.top - margin.bottom - diameter) / 2})`);

// Root boundary — outline only, no fill, encompasses every child circle.
g.append("circle")
  .attr("cx", root.x)
  .attr("cy", root.y)
  .attr("r", root.r)
  .attr("fill", "none")
  .attr("stroke", t.grid)
  .attr("stroke-width", 1.5);

const packNodes = root.descendants().filter((d) => d.depth > 0);

// Circles only — nested children are drawn after (and on top of) their parent,
// matching the pack layout's visual containment.
g.selectAll("circle.node")
  .data(packNodes)
  .join("circle")
  .attr("class", "node")
  .attr("cx", (d) => d.x)
  .attr("cy", (d) => d.y)
  .attr("r", (d) => d.r)
  .attr("fill", (d) => color(categoryOf(d).data.id))
  .attr("fill-opacity", (d) => opacityByDepth[d.depth])
  .attr("stroke", (d) => (!d.children ? t.pageBg : t.grid))
  .attr("stroke-width", (d) => (!d.children ? 1.5 : 1));

// Labels are drawn in a later pass so every label paints on top of every
// circle — including a category label that would otherwise sit under its own
// packed children.
const estCharWidth = 0.56;
const fitLabel = (d, fontSize) => {
  const maxChars = Math.max(3, Math.floor((d.r * 1.7) / (fontSize * estCharWidth)));
  return d.data.label.length > maxChars ? `${d.data.label.slice(0, maxChars - 1)}…` : d.data.label;
};

// Category labels sit near the top rim, clear of the packed children below.
g.selectAll("text.category")
  .data(packNodes.filter((d) => d.depth === 1 && d.r > 40))
  .join("text")
  .attr("class", "category")
  .attr("x", (d) => d.x)
  .attr("y", (d) => d.y - d.r + 26)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "17px")
  .style("font-weight", "700")
  .text((d) => fitLabel(d, 17));

// Leaf labels sit centered — leaves have no children to be covered by. Only
// leaves large enough to stay legible once the PNG is scaled down to a
// mobile-width thumbnail get a label; the font-size floor is raised to match.
const leafFontSize = (d) => Math.min(16, Math.max(13, d.r / 3));
g.selectAll("text.leaf")
  .data(packNodes.filter((d) => !d.children && d.r > 34))
  .join("text")
  .attr("class", "leaf")
  .attr("x", (d) => d.x)
  .attr("y", (d) => d.y)
  .attr("dy", "0.35em")
  .attr("text-anchor", "middle")
  .attr("fill", t.pageBg)
  .style("font-size", (d) => `${leafFontSize(d)}px`)
  .text((d) => fitLabel(d, leafFontSize(d)));

// --- Title --------------------------------------------------------------------
svg
  .append("text")
  .attr("x", width / 2)
  .attr("y", 60)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "24px")
  .style("font-weight", "600")
  .text("circlepacking-basic · javascript · d3 · anyplot.ai");

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/circlepacking-basic/d3/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "circlepacking-basic",
  "language": "javascript",
  "library": "d3",
  "page": "https://anyplot.ai/circlepacking-basic/javascript/d3",
  "hub": "https://anyplot.ai/circlepacking-basic",
  "code_json": "https://api.anyplot.ai/specs/circlepacking-basic/d3/code",
  "spec_json": "https://api.anyplot.ai/specs/circlepacking-basic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/circlepacking-basic/javascript/d3/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/circlepacking-basic/javascript/d3/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/circlepacking-basic/javascript/d3/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/circlepacking-basic/javascript/d3/plot-dark.html",
  "quality_score": 93.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Circle Packing Chart on anyplot.ai.

Other implementations