{"spec_id":"icicle-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// icicle-basic: Basic Icicle Chart\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-09-05\n\n//# anyplot-orientation: landscape\n\n// Only the core Highcharts bundle is loaded (no `treemap`/`icicle` module — the\n// icicle series is itself built on top of the treemap module, neither of which\n// ships here), so the layered rectangles are computed with a partition layout\n// (root spans the full width; each child's width is its value's share of the\n// parent's width; a childless node stretches down to the last row so no gaps\n// appear) and drawn by hand with `chart.renderer.rect()` — the same hand-drawn\n// technique treemap-basic / sunburst-basic / sankey-basic use for series types\n// the core bundle doesn't ship. Hover uses a native SVG `<title>` per\n// rectangle, covering the whole tile.\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: repository file-system breakdown (KB) — name/parent/value --------\nconst RECORDS = [\n  { name: \"repo\", parent: null },\n  { name: \"src\", parent: \"repo\" },\n  { name: \"node_modules\", parent: \"repo\" },\n  { name: \"docs\", parent: \"repo\" },\n  { name: \"tests\", parent: \"repo\" },\n  { name: \"assets\", parent: \"repo\" },\n  { name: \"components\", parent: \"src\" },\n  { name: \"utils\", parent: \"src\" },\n  { name: \"api\", parent: \"src\" },\n  { name: \"styles\", parent: \"src\" },\n  { name: \"react\", parent: \"node_modules\", value: 890 },\n  { name: \"webpack\", parent: \"node_modules\", value: 540 },\n  { name: \"lodash\", parent: \"node_modules\", value: 310 },\n  { name: \"guides\", parent: \"docs\", value: 85 },\n  { name: \"api-reference\", parent: \"docs\", value: 45 },\n  { name: \"unit\", parent: \"tests\", value: 95 },\n  { name: \"integration\", parent: \"tests\", value: 70 },\n  { name: \"images\", parent: \"assets\", value: 210 },\n  { name: \"fonts\", parent: \"assets\", value: 60 },\n  { name: \"Button.jsx\", parent: \"components\", value: 95 },\n  { name: \"Modal.jsx\", parent: \"components\", value: 80 },\n  { name: \"Chart.jsx\", parent: \"components\", value: 90 },\n  { name: \"Table.jsx\", parent: \"components\", value: 55 },\n  { name: \"format.js\", parent: \"utils\", value: 55 },\n  { name: \"validate.js\", parent: \"utils\", value: 45 },\n  { name: \"api-client.js\", parent: \"utils\", value: 40 },\n  { name: \"routes.js\", parent: \"api\", value: 65 },\n  { name: \"middleware.js\", parent: \"api\", value: 45 },\n  { name: \"theme.css\", parent: \"styles\", value: 35 },\n  { name: \"globals.css\", parent: \"styles\", value: 25 },\n];\n\n// --- Build tree + bottom-up value rollup ------------------------------------\nconst nodesByName = new Map(RECORDS.map((r) => [r.name, { ...r, children: [] }]));\nnodesByName.forEach((node) => {\n  if (node.parent && nodesByName.has(node.parent)) {\n    nodesByName.get(node.parent).children.push(node);\n  }\n});\nfunction rollUp(node) {\n  if (node.children.length) {\n    node.value = node.children.reduce((s, c) => s + rollUp(c), 0);\n  }\n  return node.value;\n}\nconst ROOT = nodesByName.get(\"repo\");\nrollUp(ROOT);\n\n// --- Color + text helpers ----------------------------------------------------\nfunction hexToRgb(hex) {\n  const c = parseInt(hex.slice(1), 16);\n  return [(c >> 16) & 255, (c >> 8) & 255, c & 255];\n}\nfunction mix(hexA, hexB, f) {\n  const [r1, g1, b1] = hexToRgb(hexA);\n  const [r2, g2, b2] = hexToRgb(hexB);\n  const r = Math.round(r1 + (r2 - r1) * f);\n  const g = Math.round(g1 + (g2 - g1) * f);\n  const b = Math.round(b1 + (b2 - b1) * f);\n  return `#${[r, g, b].map((v) => v.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\nfunction luma(hex) {\n  const [r, g, b] = hexToRgb(hex);\n  return (0.299 * r + 0.587 * g + 0.114 * b) / 255;\n}\nconst labelColorFor = (bgHex) => (luma(bgHex) > 0.55 ? \"#1A1A17\" : \"#FFFDF6\");\nconst kb = (v) => `${v.toLocaleString(\"en-US\")} KB`;\n\n// Shrink-to-fit: largest fontSize in [min, nominal] whose estimated text width\n// fits maxWidth, or null if even `min` overflows — callers omit the label then.\nfunction fitFontSize(text, maxWidth, nominal, min) {\n  for (let size = nominal; size >= min; size--) {\n    if (text.length * size * 0.56 + 6 <= maxWidth) return size;\n  }\n  return null;\n}\n\nfunction ancestorsOf(node) {\n  const chain = [node];\n  let cur = node;\n  while (cur.parent && nodesByName.has(cur.parent)) {\n    cur = nodesByName.get(cur.parent);\n    chain.push(cur);\n  }\n  return chain;\n}\n\n// --- Title (fontsize scaled off the 67-char baseline) -----------------------\nconst TITLE_TEXT = \"Repository File Sizes · icicle-basic · javascript · highcharts · anyplot.ai\";\nconst TITLE_FS = Math.max(Math.round(22 * Math.min(1, 67 / TITLE_TEXT.length)), 14);\n\n// --- Chart shell (no series/axes — the icicle is drawn by hand) ------------\nconst chart = Highcharts.chart(\"container\", {\n  chart: {\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    marginTop: 112,\n    marginBottom: 30,\n    marginLeft: 30,\n    marginRight: 30,\n  },\n  credits: { enabled: false },\n  title: {\n    text: TITLE_TEXT,\n    style: { color: t.ink, fontSize: TITLE_FS + \"px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Simulated project repository — tile width ∝ folder/file size (KB), rows ∝ directory depth\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: { visible: false },\n  yAxis: { visible: false },\n  legend: { enabled: false },\n  plotOptions: { series: { animation: false } },\n  series: [],\n});\n\nconst PLOT_W = chart.plotWidth;\nconst PLOT_H = chart.plotHeight;\nconst GAP = 3;\n\n// --- Partition layout (values -> layered rectangles) ------------------------\n// Root spans the full row; each child's width is its value's share of the\n// parent's width. A node without children stretches down to the last row\n// (classic icicle behaviour) instead of leaving blank space beneath it.\nconst DEPTH_COUNT = (() => {\n  let max = 0;\n  (function walk(node, depth) {\n    max = Math.max(max, depth);\n    node.children.forEach((c) => walk(c, depth + 1));\n  })(ROOT, 0);\n  return max + 1;\n})();\n// The root row only ever shows a single label, so giving it a full 1/DEPTH_COUNT\n// share leaves a near-empty band at the top. Shrink it to ~55% of a normal row\n// and redistribute the freed height across the deeper, information-dense rows.\nconst ROW_WEIGHTS = Array.from({ length: DEPTH_COUNT }, (_, d) => (d === 0 ? 0.55 : 1));\nconst ROW_UNIT = PLOT_H / ROW_WEIGHTS.reduce((a, b) => a + b, 0);\nconst ROW_Y = [0];\nROW_WEIGHTS.forEach((w) => ROW_Y.push(ROW_Y[ROW_Y.length - 1] + w * ROW_UNIT));\n\nconst TILES = [];\n(function partition(node, x0, x1, depth, branchColor) {\n  const y0 = ROW_Y[depth];\n  const y1 = node.children.length ? ROW_Y[depth + 1] : PLOT_H;\n  TILES.push({ node, x0, x1, y0, y1, depth, color: branchColor });\n  if (node.children.length) {\n    let cx = x0;\n    node.children.forEach((child, i) => {\n      const w = (x1 - x0) * (child.value / node.value);\n      // Depth 1 (top-level folders) pick the next Imprint hue — abstract\n      // categories, canonical order. Deeper descendants inherit their\n      // branch's hue and get progressively tinted toward the page background.\n      const childColor = depth === 0 ? t.palette[i % t.palette.length] : branchColor;\n      partition(child, cx, cx + w, depth + 1, childColor);\n      cx += w;\n    });\n  }\n})(ROOT, 0, PLOT_W, 0, null);\n\n// --- Draw ---------------------------------------------------------------\nconst g = chart.renderer.g(\"icicle\").add();\n// name -> { rect, fill, stroke, strokeWidth } — lets hover highlight the\n// hovered tile and brighten its full ancestry chain back to the root.\nconst nodeElements = new Map();\n\nfunction addTooltip(el, text) {\n  const titleEl = document.createElementNS(\"http://www.w3.org/2000/svg\", \"title\");\n  titleEl.textContent = text;\n  el.element.appendChild(titleEl);\n}\n\nTILES.forEach(({ node, x0, x1, y0, y1, depth, color }) => {\n  const bx = chart.plotLeft + x0 + GAP / 2;\n  const by = chart.plotTop + y0 + GAP / 2;\n  const bw = Math.max(x1 - x0 - GAP, 0);\n  const bh = Math.max(y1 - y0 - GAP, 0);\n  if (bw <= 0 || bh <= 0) return;\n\n  const isRoot = depth === 0;\n  const fill = isRoot ? t.elevatedBg : mix(color, t.pageBg, Math.min(0.12 * (depth - 1), 0.45));\n  const stroke = isRoot ? t.inkSoft : t.pageBg;\n  // Depth-graded stroke weight (root heaviest, leaves lightest) instead of a\n  // uniform 2px everywhere, plus a subtle corner radius for polish.\n  const strokeWidth = Math.max(2.5 - depth * 0.4, 1.2);\n  const pct = ((node.value / ROOT.value) * 100).toFixed(1);\n  const path = (function ancestry(n) {\n    return n.parent ? `${ancestry(nodesByName.get(n.parent))} → ${n.name}` : n.name;\n  })(node);\n\n  const rect = chart.renderer\n    .rect(bx, by, bw, bh, 3)\n    .attr({\n      fill,\n      stroke,\n      \"stroke-width\": strokeWidth,\n      zIndex: 2 + depth,\n    })\n    .add(g);\n  addTooltip(rect, `${path}\\n${kb(node.value)} (${pct}% of repo)`);\n  nodeElements.set(node.name, { rect, fill, stroke, strokeWidth });\n\n  // Highcharts-specific interactivity: hovering a tile brightens it and\n  // thickens+recolors the stroke of every ancestor up to the root, tracing\n  // the lineage chain — a native mouseover/mouseout touch on top of the\n  // hand-drawn rects, most useful in the interactive HTML view.\n  const chain = ancestorsOf(node);\n  rect.element.addEventListener(\"mouseenter\", () => {\n    chain.forEach((n, i) => {\n      const entry = nodeElements.get(n.name);\n      if (!entry) return;\n      entry.rect.attr({\n        fill: i === 0 ? mix(entry.fill, \"#FFFFFF\", 0.15) : entry.fill,\n        stroke: t.amber,\n        \"stroke-width\": entry.strokeWidth + (i === 0 ? 1.5 : 1),\n      });\n    });\n  });\n  rect.element.addEventListener(\"mouseleave\", () => {\n    chain.forEach((n) => {\n      const entry = nodeElements.get(n.name);\n      if (!entry) return;\n      entry.rect.attr({ fill: entry.fill, stroke: entry.stroke, \"stroke-width\": entry.strokeWidth });\n    });\n  });\n\n  const label = node.name;\n  const nameSize = fitFontSize(label, bw - 10, 16, 11);\n  if (nameSize && bh >= 24) {\n    const showValue = bh >= 46 && bw >= 56;\n    const textColor = labelColorFor(fill);\n    chart.renderer\n      .text(label, bx + 8, showValue ? by + 20 : by + bh / 2 + nameSize * 0.35)\n      .attr({ align: \"left\", zIndex: 6 + depth })\n      .css({ color: textColor, fontSize: `${nameSize}px`, fontWeight: \"600\", pointerEvents: \"none\" })\n      .add(g);\n    if (showValue) {\n      chart.renderer\n        .text(kb(node.value), bx + 8, by + 20 + 16)\n        .attr({ align: \"left\", zIndex: 6 + depth })\n        .css({ color: textColor, fontSize: \"12px\", pointerEvents: \"none\" })\n        .add(g);\n    }\n  }\n});\n"}