{"spec_id":"circlepacking-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// circlepacking-basic: Circle Packing Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-02\n\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data: repository storage breakdown (folders + files, KB) --------------\n// Deterministic LCG so file sizes are reproducible without a real RNG.\nlet seed = 42;\nconst nextSize = (min, max) => {\n  seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff;\n  return min + Math.round((seed / 0x7fffffff) * (max - min));\n};\n\nconst categories = [\n  { id: \"src\", label: \"src/\" },\n  { id: \"assets\", label: \"assets/\" },\n  { id: \"docs\", label: \"docs/\" },\n  { id: \"tests\", label: \"tests/\" },\n  { id: \"config\", label: \"config/\" },\n];\n\nconst folders = [\n  { id: \"src-components\", parent: \"src\", label: \"components/\" },\n  { id: \"src-hooks\", parent: \"src\", label: \"hooks/\" },\n  { id: \"src-utils\", parent: \"src\", label: \"utils/\" },\n  { id: \"src-services\", parent: \"src\", label: \"services/\" },\n  { id: \"assets-images\", parent: \"assets\", label: \"images/\" },\n  { id: \"assets-fonts\", parent: \"assets\", label: \"fonts/\" },\n  { id: \"assets-icons\", parent: \"assets\", label: \"icons/\" },\n  { id: \"docs-guides\", parent: \"docs\", label: \"guides/\" },\n  { id: \"docs-api\", parent: \"docs\", label: \"api/\" },\n  { id: \"tests-unit\", parent: \"tests\", label: \"unit/\" },\n  { id: \"tests-integration\", parent: \"tests\", label: \"integration/\" },\n];\n\nconst filesByParent = {\n  \"src-components\": [\"Header.tsx\", \"Footer.tsx\", \"Sidebar.tsx\", \"Chart.tsx\", \"Modal.tsx\", \"Button.tsx\"],\n  \"src-hooks\": [\"useAuth.ts\", \"useFetch.ts\", \"useTheme.ts\", \"useDebounce.ts\"],\n  \"src-utils\": [\"format.ts\", \"validate.ts\", \"parse.ts\", \"colors.ts\", \"dates.ts\"],\n  \"src-services\": [\"api.ts\", \"storage.ts\", \"analytics.ts\", \"sockets.ts\"],\n  \"assets-images\": [\"hero.png\", \"logo.png\", \"banner.png\", \"avatar.png\", \"og-image.png\"],\n  \"assets-fonts\": [\"Inter.woff2\", \"Mono.woff2\", \"Serif.woff2\"],\n  \"assets-icons\": [\"arrow.svg\", \"check.svg\", \"close.svg\", \"search.svg\"],\n  \"docs-guides\": [\"quickstart.md\", \"deployment.md\", \"theming.md\", \"faq.md\"],\n  \"docs-api\": [\"reference.md\", \"changelog.md\", \"migration.md\"],\n  \"tests-unit\": [\"format.test.ts\", \"validate.test.ts\", \"parse.test.ts\", \"colors.test.ts\", \"dates.test.ts\", \"api.test.ts\"],\n  \"tests-integration\": [\"auth.spec.ts\", \"checkout.spec.ts\", \"search.spec.ts\", \"onboarding.spec.ts\"],\n  config: [\"tsconfig.json\", \"eslint.config.js\", \"vite.config.ts\"],\n};\n\nconst nodes = [{ id: \"repo\", parent: null, value: null, label: \"repo/\" }];\nfor (const c of categories) nodes.push({ id: c.id, parent: \"repo\", value: null, label: c.label });\nfor (const f of folders) nodes.push({ id: f.id, parent: f.parent, value: null, label: f.label });\nfor (const [parent, files] of Object.entries(filesByParent)) {\n  const sizeRange = parent === \"assets-images\" ? [60, 480] : parent.startsWith(\"assets\") ? [10, 90] : [4, 60];\n  for (const name of files) {\n    nodes.push({ id: `${parent}/${name}`, parent, value: nextSize(...sizeRange), label: name });\n  }\n}\n\n// --- Hierarchy + pack layout -------------------------------------------------\nconst margin = { top: 110, right: 50, bottom: 50, left: 50 };\nconst diameter = Math.min(width - margin.left - margin.right, height - margin.top - margin.bottom);\n\nconst root = d3\n  .stratify()\n  .id((d) => d.id)\n  .parentId((d) => d.parent)(nodes)\n  .sum((d) => d.value || 0)\n  .sort((a, b) => b.value - a.value);\n\nd3.pack().size([diameter, diameter]).padding((d) => (d.depth === 0 ? 12 : d.depth === 1 ? 7 : 3))(root);\n\n// --- Color: hue by top-level category, opacity deepens with nesting --------\nconst color = d3.scaleOrdinal(\n  categories.map((c) => c.id),\n  t.palette.slice(0, categories.length)\n);\nconst categoryOf = (d) => d.ancestors().find((a) => a.depth === 1);\nconst opacityByDepth = { 1: 0.16, 2: 0.4, 3: 0.9 };\n\n// --- SVG mount ----------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${(width - diameter) / 2},${margin.top + (height - margin.top - margin.bottom - diameter) / 2})`);\n\n// Root boundary — outline only, no fill, encompasses every child circle.\ng.append(\"circle\")\n  .attr(\"cx\", root.x)\n  .attr(\"cy\", root.y)\n  .attr(\"r\", root.r)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1.5);\n\nconst packNodes = root.descendants().filter((d) => d.depth > 0);\n\n// Circles only — nested children are drawn after (and on top of) their parent,\n// matching the pack layout's visual containment.\ng.selectAll(\"circle.node\")\n  .data(packNodes)\n  .join(\"circle\")\n  .attr(\"class\", \"node\")\n  .attr(\"cx\", (d) => d.x)\n  .attr(\"cy\", (d) => d.y)\n  .attr(\"r\", (d) => d.r)\n  .attr(\"fill\", (d) => color(categoryOf(d).data.id))\n  .attr(\"fill-opacity\", (d) => opacityByDepth[d.depth])\n  .attr(\"stroke\", (d) => (!d.children ? t.pageBg : t.grid))\n  .attr(\"stroke-width\", (d) => (!d.children ? 1.5 : 1));\n\n// Labels are drawn in a later pass so every label paints on top of every\n// circle — including a category label that would otherwise sit under its own\n// packed children.\nconst estCharWidth = 0.56;\nconst fitLabel = (d, fontSize) => {\n  const maxChars = Math.max(3, Math.floor((d.r * 1.7) / (fontSize * estCharWidth)));\n  return d.data.label.length > maxChars ? `${d.data.label.slice(0, maxChars - 1)}…` : d.data.label;\n};\n\n// Category labels sit near the top rim, clear of the packed children below.\ng.selectAll(\"text.category\")\n  .data(packNodes.filter((d) => d.depth === 1 && d.r > 40))\n  .join(\"text\")\n  .attr(\"class\", \"category\")\n  .attr(\"x\", (d) => d.x)\n  .attr(\"y\", (d) => d.y - d.r + 26)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .style(\"font-weight\", \"700\")\n  .text((d) => fitLabel(d, 17));\n\n// Leaf labels sit centered — leaves have no children to be covered by. Only\n// leaves large enough to stay legible once the PNG is scaled down to a\n// mobile-width thumbnail get a label; the font-size floor is raised to match.\nconst leafFontSize = (d) => Math.min(16, Math.max(13, d.r / 3));\ng.selectAll(\"text.leaf\")\n  .data(packNodes.filter((d) => !d.children && d.r > 34))\n  .join(\"text\")\n  .attr(\"class\", \"leaf\")\n  .attr(\"x\", (d) => d.x)\n  .attr(\"y\", (d) => d.y)\n  .attr(\"dy\", \"0.35em\")\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.pageBg)\n  .style(\"font-size\", (d) => `${leafFontSize(d)}px`)\n  .text((d) => fitLabel(d, leafFontSize(d)));\n\n// --- Title --------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"24px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"circlepacking-basic · javascript · d3 · anyplot.ai\");\n"}