{"spec_id":"icicle-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// icicle-basic: Basic Icicle Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 24, bottom: 24, left: 24 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: a small project file tree (name/parent/value), in-memory --------\n// value = approximate file size in KB, so leaf magnitudes read as a plausible\n// real-world size distribution rather than abstract units.\nconst data = [\n  { name: \"project\", parent: null, value: null },\n  { name: \"src\", parent: \"project\", value: null },\n  { name: \"components\", parent: \"src\", value: null },\n  { name: \"Button.js\", parent: \"components\", value: 23 },\n  { name: \"Header.js\", parent: \"components\", value: 17 },\n  { name: \"Footer.js\", parent: \"components\", value: 11 },\n  { name: \"utils\", parent: \"src\", value: null },\n  { name: \"format.js\", parent: \"utils\", value: 12 },\n  { name: \"validate.js\", parent: \"utils\", value: 19 },\n  { name: \"index.js\", parent: \"src\", value: 6 },\n  { name: \"docs\", parent: \"project\", value: null },\n  { name: \"README.md\", parent: \"docs\", value: 28 },\n  { name: \"CHANGELOG.md\", parent: \"docs\", value: 9 },\n  { name: \"tests\", parent: \"project\", value: null },\n  { name: \"unit\", parent: \"tests\", value: null },\n  { name: \"button.test.js\", parent: \"unit\", value: 15 },\n  { name: \"utils.test.js\", parent: \"unit\", value: 10 },\n  { name: \"integration\", parent: \"tests\", value: null },\n  { name: \"api.test.js\", parent: \"integration\", value: 21 },\n  { name: \"assets\", parent: \"project\", value: null },\n  { name: \"logo.svg\", parent: \"assets\", value: 34 },\n  { name: \"styles.css\", parent: \"assets\", value: 16 },\n  { name: \"fonts\", parent: \"assets\", value: null },\n  { name: \"Inter.woff2\", parent: \"fonts\", value: 41 },\n  { name: \"Mono.woff2\", parent: \"fonts\", value: 26 },\n];\n\n// --- Hierarchy + icicle layout ----------------------------------------------\n// x-extent encodes value (breadth), y-extent encodes depth (root at top,\n// children stacked below) — the classic top-to-bottom icicle orientation.\nconst root = d3\n  .stratify()\n  .id((d) => d.name)\n  .parentId((d) => d.parent)(data)\n  .sum((d) => d.value || 0)\n  .sort((a, b) => b.value - a.value);\n\nd3.partition().size([iw, ih]).padding(1.5)(root);\n\nconst nodes = root.descendants();\n\n// --- Color: brand green + Imprint order for each top-level category --------\nconst categoryScale = d3\n  .scaleOrdinal()\n  .domain(root.children.map((d) => d.data.name))\n  .range(t.palette);\n\nconst categoryOf = (node) => {\n  let ancestor = node;\n  while (ancestor.depth > 1) ancestor = ancestor.parent;\n  return ancestor.data.name;\n};\n\n// Root uses a muted neutral (not raw t.ink) so it doesn't flip between a\n// stark black block (light) and a stark white block (dark) like the rest\n// of the chrome — a softer, less dominant theme transition.\nconst fillOf = (node) => (node.depth === 0 ? t.inkSoft : categoryScale(categoryOf(node)));\n\n// Largest top-level branch (root.children is pre-sorted descending by value).\nconst largestCategory = root.children[0];\n\n// --- Label contrast: pick dark/light ink from the cell's own fill luminance\nconst LABEL_DARK = \"#1A1A17\";\nconst LABEL_LIGHT = \"#F0EFE8\";\nconst relLuminance = (hex) => {\n  const c = d3.color(hex).rgb();\n  const chan = (v) => {\n    v /= 255;\n    return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\n  };\n  return 0.2126 * chan(c.r) + 0.7152 * chan(c.g) + 0.0722 * chan(c.b);\n};\nconst labelColorOf = (node) => (relLuminance(fillOf(node)) > 0.42 ? LABEL_DARK : LABEL_LIGHT);\n\nconst truncate = (name, cellWidth) => {\n  const maxChars = Math.floor((cellWidth - 12) / 6.4);\n  return name.length > maxChars ? `${name.slice(0, Math.max(0, maxChars - 1))}…` : name;\n};\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Cells --------------------------------------------------------------------\ng.selectAll(\"rect\")\n  .data(nodes)\n  .join(\"rect\")\n  .attr(\"x\", (d) => d.x0)\n  .attr(\"y\", (d) => d.y0)\n  .attr(\"width\", (d) => Math.max(0, d.x1 - d.x0))\n  .attr(\"height\", (d) => Math.max(0, d.y1 - d.y0))\n  .attr(\"fill\", (d) => fillOf(d))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\n// --- Highlight ring: draw the eye to the single largest top-level branch ------\ng.append(\"rect\")\n  .attr(\"x\", largestCategory.x0 - 3)\n  .attr(\"y\", largestCategory.y0 - 3)\n  .attr(\"width\", largestCategory.x1 - largestCategory.x0 + 6)\n  .attr(\"height\", ih - largestCategory.y0 + 6)\n  .attr(\"rx\", 4)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", categoryScale(largestCategory.data.name))\n  .attr(\"stroke-width\", 2.5);\n\n// --- Labels: only where a cell has room ---------------------------------------\ng.selectAll(\"text\")\n  .data(nodes.filter((d) => d.x1 - d.x0 > 60 && d.y1 - d.y0 > 22))\n  .join(\"text\")\n  .attr(\"x\", (d) => d.x0 + 8)\n  .attr(\"y\", (d) => (d.y0 + d.y1) / 2)\n  .attr(\"dominant-baseline\", \"middle\")\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", (d) => (d.depth === 0 ? \"600\" : \"400\"))\n  .attr(\"fill\", (d) => labelColorOf(d))\n  .text((d) => truncate(d.data.name, d.x1 - d.x0));\n\n// --- Title ----------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"icicle-basic · javascript · d3 · anyplot.ai\");\n"}