{"spec_id":"icicle-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// icicle-basic: Basic Icicle Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ---------------------------------------\n// File system hierarchy: project -> folders -> files, value = size (KB).\nconst nodes = [\n  { name: \"project\", parent: null, value: 100, level: 0 },\n  { name: \"src\", parent: \"project\", value: 55, level: 1 },\n  { name: \"tests\", parent: \"project\", value: 25, level: 1 },\n  { name: \"docs\", parent: \"project\", value: 12, level: 1 },\n  { name: \"config\", parent: \"project\", value: 8, level: 1 },\n  { name: \"components\", parent: \"src\", value: 20, level: 2 },\n  { name: \"utils\", parent: \"src\", value: 15, level: 2 },\n  { name: \"api\", parent: \"src\", value: 12, level: 2 },\n  { name: \"styles\", parent: \"src\", value: 8, level: 2 },\n  { name: \"unit\", parent: \"tests\", value: 15, level: 2 },\n  { name: \"integration\", parent: \"tests\", value: 10, level: 2 },\n  { name: \"guides\", parent: \"docs\", value: 7, level: 2 },\n  { name: \"reference\", parent: \"docs\", value: 5, level: 2 },\n  { name: \"webpack\", parent: \"config\", value: 5, level: 2 },\n  { name: \"eslint\", parent: \"config\", value: 3, level: 2 },\n];\n\n// Partition layout: give each node a horizontal span [x0, x1] within its\n// parent's span, proportional to value (same idea as d3's partition layout).\nconst byParent = new Map();\nfor (const node of nodes) {\n  if (!byParent.has(node.parent)) byParent.set(node.parent, []);\n  byParent.get(node.parent).push(node);\n}\nconst root = nodes.find((node) => node.parent === null);\nroot.x0 = 0;\nroot.x1 = 100;\nconst queue = [root];\nwhile (queue.length) {\n  const parent = queue.shift();\n  const children = byParent.get(parent.name) || [];\n  const total = children.reduce((sum, child) => sum + child.value, 0);\n  let cursor = parent.x0;\n  for (const child of children) {\n    const width = (child.value / total) * (parent.x1 - parent.x0);\n    child.x0 = cursor;\n    child.x1 = cursor + width;\n    cursor += width;\n    queue.push(child);\n  }\n}\n\n// Color by top-level branch (root = neutral, each folder its own hue, files\n// inherit their folder's hue) so the hierarchy groups visually.\nconst branchColor = new Map([\n  [\"src\", t.palette[0]],\n  [\"tests\", t.palette[1]],\n  [\"docs\", t.palette[2]],\n  [\"config\", t.palette[3]],\n]);\nconst colorByName = new Map(nodes.map((node) => [node.name, null]));\nfor (const node of nodes) {\n  if (node.level === 0) colorByName.set(node.name, t.ink);\n  else if (node.level === 1) colorByName.set(node.name, branchColor.get(node.name));\n  else colorByName.set(node.name, colorByName.get(node.parent));\n}\n\nconst rowLabels = [\"Project\", \"Folder\", \"File\"];\n\n// --- Label contrast: pick ink-dark or ink-light text per rectangle fill -----\n// (root uses the theme-adaptive ink/pageBg swap directly; branch/file rows use\n// this since their fill colors are fixed across themes, so the readable text\n// color must be picked per-color rather than swapped by theme.)\nfunction relativeLuminance(r, g, b) {\n  const lin = (c) => {\n    const s = c / 255;\n    return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);\n  };\n  return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);\n}\nfunction contrastRatio(l1, l2) {\n  const lighter = Math.max(l1, l2);\n  const darker = Math.min(l1, l2);\n  return (lighter + 0.05) / (darker + 0.05);\n}\nfunction labelColorFor(hex) {\n  const n = parseInt(hex.slice(1), 16);\n  const luminance = relativeLuminance((n >> 16) & 255, (n >> 8) & 255, n & 255);\n  const darkContrast = contrastRatio(luminance, relativeLuminance(0x1a, 0x1a, 0x17));\n  const lightContrast = contrastRatio(luminance, relativeLuminance(0xf0, 0xef, 0xe8));\n  return darkContrast >= lightContrast ? \"#1A1A17\" : \"#F0EFE8\";\n}\nconst labelColorByName = new Map(\n  nodes.map((node) => [\n    node.name,\n    node.level === 0 ? t.pageBg : labelColorFor(colorByName.get(node.name)),\n  ]),\n);\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Label plugin (draws node names onto rectangles with enough room) -------\nconst rectLabelPlugin = {\n  id: \"icicleRectLabels\",\n  afterDatasetsDraw(chart) {\n    const { ctx } = chart;\n    ctx.save();\n    ctx.font = `600 15px ${Chart.defaults.font.family}`;\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    nodes.forEach((node, i) => {\n      const meta = chart.getDatasetMeta(i);\n      const el = meta.data[node.level];\n      if (!el) return;\n      const { x, base, y } = el.getProps([\"x\", \"base\", \"y\"], true);\n      const rectWidth = Math.abs(x - base);\n      const textWidth = ctx.measureText(node.name).width;\n      if (rectWidth < textWidth + 16) return;\n      ctx.fillStyle = labelColorByName.get(node.name);\n      ctx.fillText(node.name, (x + base) / 2, y);\n    });\n    ctx.restore();\n  },\n};\n\n// --- Chart -------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels: rowLabels,\n    datasets: nodes.map((node) => ({\n      label: node.name,\n      data: rowLabels.map((_, i) => (i === node.level ? [node.x0, node.x1] : null)),\n      backgroundColor: colorByName.get(node.name),\n      borderColor: t.pageBg,\n      borderWidth: 2,\n      borderSkipped: false,\n      grouped: false,\n      barPercentage: 1,\n      categoryPercentage: 0.9,\n    })),\n  },\n  options: {\n    indexAxis: \"y\",\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"icicle-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        min: 0,\n        max: 100,\n        ticks: { color: t.inkSoft, font: { size: 14 }, callback: (v) => `${v}%` },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Share of Project Size\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { display: false },\n        title: { display: true, text: \"Hierarchy Level\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n  plugins: [rectLabelPlugin],\n});\n"}