{"spec_id":"icicle-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// icicle-basic: Basic Icicle Chart\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 86/100 | Created: 2026-09-05\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\nconst title = \"icicle-basic · javascript · muix · anyplot.ai\";\nconst titleFontSize = Math.round(30 * Math.min(1, 67 / title.length));\n\n// --- Data: a file-system hierarchy (name/children/value in KB) -- one of the\n// spec's listed applications. 4 top-level directories, 11 subdirectories, 25\n// files -- 41 nodes total, well inside the spec's 10-100 node range. Only\n// leaves carry an explicit size; directory sizes are the sum of their\n// contents, exactly like `du` on a real file system. -----------------------\nconst TREE = {\n  name: \"repo/\",\n  children: [\n    {\n      name: \"node_modules/\",\n      children: [\n        {\n          name: \"react/\",\n          children: [\n            { name: \"index.js\", value: 180 },\n            { name: \"package.json\", value: 60 },\n            { name: \"README.md\", value: 80 },\n          ],\n        },\n        {\n          name: \"webpack/\",\n          children: [\n            { name: \"webpack.js\", value: 140 },\n            { name: \"loader.js\", value: 90 },\n            { name: \"config.js\", value: 50 },\n          ],\n        },\n        {\n          name: \"typescript/\",\n          children: [\n            { name: \"tsc.js\", value: 120 },\n            { name: \"lib.d.ts\", value: 70 },\n            { name: \"compiler.js\", value: 60 },\n          ],\n        },\n        {\n          name: \"eslint/\",\n          children: [\n            { name: \"index.js\", value: 55 },\n            { name: \"rules.js\", value: 45 },\n          ],\n        },\n      ],\n    },\n    {\n      name: \"src/\",\n      children: [\n        {\n          name: \"components/\",\n          children: [\n            { name: \"Button.tsx\", value: 90 },\n            { name: \"Modal.tsx\", value: 110 },\n            { name: \"Chart.tsx\", value: 80 },\n          ],\n        },\n        {\n          name: \"utils/\",\n          children: [\n            { name: \"format.ts\", value: 100 },\n            { name: \"api.ts\", value: 90 },\n          ],\n        },\n        {\n          name: \"styles/\",\n          children: [\n            { name: \"theme.css\", value: 75 },\n            { name: \"globals.css\", value: 55 },\n          ],\n        },\n      ],\n    },\n    {\n      name: \"tests/\",\n      children: [\n        {\n          name: \"unit/\",\n          children: [\n            { name: \"button.test.ts\", value: 100 },\n            { name: \"utils.test.ts\", value: 90 },\n          ],\n        },\n        {\n          name: \"integration/\",\n          children: [\n            { name: \"api.test.ts\", value: 85 },\n            { name: \"flow.test.ts\", value: 75 },\n          ],\n        },\n      ],\n    },\n    {\n      name: \"docs/\",\n      children: [\n        {\n          name: \"guides/\",\n          children: [\n            { name: \"getting-started.md\", value: 70 },\n            { name: \"deployment.md\", value: 60 },\n          ],\n        },\n        {\n          name: \"reference/\",\n          children: [{ name: \"api-reference.md\", value: 70 }],\n        },\n      ],\n    },\n  ],\n};\n\n// Bottom-up: a directory's size is the sum of what it contains (leaves carry\n// their own explicit `value`).\nfunction computeValue(node) {\n  if (!node.children) return node.value;\n  node.value = node.children.reduce((sum, c) => sum + computeValue(c), 0);\n  return node.value;\n}\ncomputeValue(TREE);\n\n// Fixed neutral grey for the root/total band -- unlike `t.ink`, this stays\n// pixel-identical across light and dark renders, so every data band\n// (including root) is theme-stable, not just the top-level branches.\nconst ROOT_FILL = \"#6B6A63\";\n\n// Each top-level directory owns one Imprint hue; every descendant inherits\n// its ancestor's hue so a branch stays recognizable at any depth (the\n// spec's \"color by hierarchy level or category\" note).\nfunction assignBranch(node, color) {\n  node.branchColor = color;\n  node.children?.forEach((c) => assignBranch(c, color));\n}\nassignBranch(TREE, ROOT_FILL); // root itself reads as the neutral/total anchor\nTREE.children.forEach((dir, i) => assignBranch(dir, t.palette[i % t.palette.length]));\n\n// --- Icicle (partition) layout: each depth is a full-width horizontal band;\n// within a band, a node's horizontal span is proportional to its value\n// within its parent's span. Root at top, children stacked below -- the\n// spec's \"horizontal orientation, root at top\" note. ------------------------\nfunction partition(node, x0, x1, depth) {\n  node.x0 = x0;\n  node.x1 = x1;\n  node.depth = depth;\n  if (!node.children) return;\n  let cx = x0;\n  const total = node.value;\n  node.children.forEach((child) => {\n    const w = total > 0 ? (child.value / total) * (x1 - x0) : 0;\n    partition(child, cx, cx + w, depth + 1);\n    cx += w;\n  });\n}\n\nfunction flatten(node, acc) {\n  acc.push(node);\n  node.children?.forEach((c) => flatten(c, acc));\n  return acc;\n}\n\nconst DEPTH_COUNT = 4; // root, directory, subdirectory, file\nconst TINT_STEP = 0.28; // lighter per depth level below the branch's directory\n\n// --- Color: contrast-aware text over an arbitrary tinted fill, same\n// relative-luminance technique used for every other custom-drawn anyplot\n// chart so labels stay legible regardless of hue or tint. -------------------\nfunction hexToRgb(hex) {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\nfunction rgbToHsl(r, g, b) {\n  r /= 255;\n  g /= 255;\n  b /= 255;\n  const max = Math.max(r, g, b);\n  const min = Math.min(r, g, b);\n  const l = (max + min) / 2;\n  if (max === min) return [0, 0, l];\n  const d = max - min;\n  const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n  let h;\n  if (max === r) h = (g - b) / d + (g < b ? 6 : 0);\n  else if (max === g) h = (b - r) / d + 2;\n  else h = (r - g) / d + 4;\n  return [h / 6, s, l];\n}\nfunction hueToRgb(p, q, tIn) {\n  let tt = tIn;\n  if (tt < 0) tt += 1;\n  if (tt > 1) tt -= 1;\n  if (tt < 1 / 6) return p + (q - p) * 6 * tt;\n  if (tt < 1 / 2) return q;\n  if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;\n  return p;\n}\nfunction hslToRgb(h, s, l) {\n  if (s === 0) {\n    const v = Math.round(l * 255);\n    return [v, v, v];\n  }\n  const q = l < 0.5 ? l * (1 + s) : l + s - l * s;\n  const p = 2 * l - q;\n  return [Math.round(hueToRgb(p, q, h + 1 / 3) * 255), Math.round(hueToRgb(p, q, h) * 255), Math.round(hueToRgb(p, q, h - 1 / 3) * 255)];\n}\nfunction tintRgb(hex, factor) {\n  const [r, g, b] = hexToRgb(hex);\n  const [h, s, l] = rgbToHsl(r, g, b);\n  return hslToRgb(h, s, l + (0.94 - l) * factor);\n}\nfunction relLuminanceRgb([r, g, b]) {\n  const srgb = [r, g, b].map((v) => v / 255).map((v) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)));\n  return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2];\n}\n// Fixed, theme-independent ink/paper pair -- the data fills themselves don't\n// change between themes, so the contrast decision must depend only on the\n// fill's own luminance, never on `t.ink`/`t.pageBg` (which flip meaning\n// between light and dark theme and previously produced near-invisible text\n// on the leaf-level tints in dark mode).\nconst FIXED_DARK_TEXT = \"#1A1A17\";\nconst FIXED_LIGHT_TEXT = \"#FFFDF6\";\nfunction textColorForRgb(rgb) {\n  return relLuminanceRgb(rgb) > 0.45 ? FIXED_DARK_TEXT : FIXED_LIGHT_TEXT;\n}\n\n// --- Sizing + label fitting -------------------------------------------------\nconst MARGIN = { top: 96, right: 24, bottom: 20, left: 24 };\nconst ROW_GUTTER = 5;\nconst COL_GUTTER = 2;\nconst LABEL_FONT_SIZE = [22, 17, 15, 13]; // by depth -- 15/13 for the two deepest levels (was 14/12) so labels hold up better at mobile thumbnail scale\nconst CHAR_WIDTH_RATIO = 0.56;\n\nfunction fmtKB(v) {\n  return `${v.toLocaleString()} KB`;\n}\nfunction fitsLabel(w, h, text, fontSize) {\n  return w - 10 >= text.length * fontSize * CHAR_WIDTH_RATIO && h >= fontSize + 4;\n}\n\nfunction IcicleRect({ x, y, w, h, fill, textFill, label, sublabel, fontSize, tooltip }) {\n  const showLabel = label && fitsLabel(w, h, label, fontSize);\n  const showSub = showLabel && sublabel && h >= fontSize * 2 + 8 && fitsLabel(w, h, sublabel, fontSize - 2);\n  return (\n    <g>\n      <rect x={x} y={y} width={w} height={h} fill={fill} stroke={t.pageBg} strokeWidth={1.5}>\n        <title>{tooltip}</title>\n      </rect>\n      {showLabel && (\n        <text x={x + 7} y={y + fontSize + 3} fontSize={fontSize} fontWeight={showSub ? 600 : 500} fill={textFill} pointerEvents=\"none\">\n          {label}\n        </text>\n      )}\n      {showSub && (\n        <text x={x + 7} y={y + fontSize * 2 + 4} fontSize={fontSize - 2} fill={textFill} opacity={0.85} pointerEvents=\"none\">\n          {sublabel}\n        </text>\n      )}\n    </g>\n  );\n}\n\nfunction Icicle() {\n  // Drawing area comes from MUI X's own DrawingProvider (via ChartContainer's\n  // `margin` prop), the documented composition primitive for a custom mark --\n  // MUI X community has no native Icicle/partition chart type.\n  const { left, top, width, height } = useDrawingArea();\n  partition(TREE, left, left + width, 0);\n  const nodes = flatten(TREE, []);\n  const rowH = height / DEPTH_COUNT;\n\n  return (\n    <g>\n      {nodes.map((node) => {\n        const x = node.x0 + COL_GUTTER / 2;\n        const w = Math.max(0, node.x1 - node.x0 - COL_GUTTER);\n        const y = top + node.depth * rowH + ROW_GUTTER / 2;\n        const h = rowH - ROW_GUTTER;\n        const fontSize = LABEL_FONT_SIZE[node.depth];\n\n        if (node.depth === 0) {\n          return (\n            <IcicleRect\n              key=\"root\"\n              x={x}\n              y={y}\n              w={w}\n              h={h}\n              fill={ROOT_FILL}\n              textFill={textColorForRgb(hexToRgb(ROOT_FILL))}\n              label={`${node.name} · ${fmtKB(node.value)} total`}\n              fontSize={fontSize}\n              tooltip={`${node.name}: ${fmtKB(node.value)}`}\n            />\n          );\n        }\n\n        const tintFactor = (node.depth - 1) * TINT_STEP;\n        const rgb = tintRgb(node.branchColor, tintFactor);\n        const fill = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;\n        const parentValue = findParentValue(TREE, node);\n        const pct = parentValue > 0 ? ((node.value / parentValue) * 100).toFixed(0) : \"0\";\n        return (\n          <IcicleRect\n            key={`${node.depth}-${node.name}-${node.x0.toFixed(1)}`}\n            x={x}\n            y={y}\n            w={w}\n            h={h}\n            fill={fill}\n            textFill={textColorForRgb(rgb)}\n            label={node.name}\n            sublabel={fmtKB(node.value)}\n            fontSize={fontSize}\n            tooltip={`${node.name}: ${fmtKB(node.value)} (${pct}% of parent)`}\n          />\n        );\n      })}\n    </g>\n  );\n}\n\n// Walks the tree to find `target`'s parent value, used for the \"% of parent\"\n// tooltip -- flatten() discards parent links, so this is the cheap way back.\nfunction findParentValue(node, target) {\n  if (!node.children) return node.value;\n  if (node.children.includes(target)) return node.value;\n  for (const c of node.children) {\n    const found = findParentValue(c, target);\n    if (found !== undefined) return found;\n  }\n  return undefined;\n}\n\n// --- Chart (default-exported component -- the harness mounts it) ----------\n// ChartContainer supplies the <ChartsSurface> SVG root and theme context; its\n// `margin` prop drives the DrawingProvider that Icicle() reads back via\n// useDrawingArea(), so the title/plot split uses MUI X's own layout system\n// rather than a parallel hand-rolled offset. The bands themselves are laid\n// out in absolute pixel space via partition() above, so no axis/scale is\n// needed -- xAxis/yAxis are omitted entirely.\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n\n  return (\n    <ChartContainer width={width} height={height} series={[]} margin={MARGIN} skipAnimation>\n      <text x={width / 2} y={40} textAnchor=\"middle\" fontSize={titleFontSize} fontWeight={600} fill={t.ink}>\n        {title}\n      </text>\n      <text x={width / 2} y={68} textAnchor=\"middle\" fontSize={15} fill={t.inkSoft}>\n        Repository file sizes by directory depth · width ∝ size (KB)\n      </text>\n      <Icicle />\n    </ChartContainer>\n  );\n}\n"}