{"spec_id":"treemap-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// treemap-basic: Basic Treemap\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-08-24\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\nconst title = \"treemap-basic · javascript · muix · anyplot.ai\";\nconst titleFontSize = Math.max(18, Math.min(34, Math.round(34 * Math.min(1, 67 / title.length))));\n\n// --- Data: a company expense breakdown by department (category) and cost\n// center / project (subcategory), in $ thousands -- one of the spec's listed\n// applications. 6 departments, 20 cost centers (26 rectangles total). -------\nconst DEPARTMENTS = [\n  {\n    name: \"Engineering\",\n    items: [\n      { name: \"Platform Infra\", value: 820 },\n      { name: \"Mobile Apps\", value: 540 },\n      { name: \"DevOps\", value: 310 },\n      { name: \"QA & Testing\", value: 210 },\n    ],\n  },\n  {\n    name: \"Sales\",\n    items: [\n      { name: \"Enterprise\", value: 610 },\n      { name: \"SMB\", value: 380 },\n      { name: \"Partnerships\", value: 190 },\n    ],\n  },\n  {\n    name: \"Marketing\",\n    items: [\n      { name: \"Digital Ads\", value: 430 },\n      { name: \"Content\", value: 260 },\n      { name: \"Events\", value: 150 },\n      { name: \"Brand\", value: 120 },\n    ],\n  },\n  {\n    name: \"R&D\",\n    items: [\n      { name: \"Applied Research\", value: 480 },\n      { name: \"Prototyping\", value: 260 },\n      { name: \"Patents & Compliance\", value: 140 },\n    ],\n  },\n  {\n    name: \"Operations\",\n    items: [\n      { name: \"Facilities\", value: 340 },\n      { name: \"IT Support\", value: 220 },\n      { name: \"Logistics\", value: 180 },\n    ],\n  },\n  {\n    name: \"Customer Success\",\n    items: [\n      { name: \"Support\", value: 300 },\n      { name: \"Onboarding\", value: 180 },\n      { name: \"Training\", value: 110 },\n    ],\n  },\n];\nconst departments = DEPARTMENTS.map((d) => ({\n  ...d,\n  value: d.items.reduce((sum, i) => sum + i.value, 0),\n}));\nconst grandTotal = departments.reduce((sum, d) => sum + d.value, 0);\n\n// --- Squarified treemap layout (Bruls, Huizing & van Wijk, 1999) -----------\n// Lays out `items` (each with a numeric `.value`) inside `container`\n// ({x, y, w, h}), returning each item with added `.x/.y/.w/.h`, minimizing\n// aspect ratio so rectangles stay close to square rather than thin slivers.\nfunction worstRatio(row, length) {\n  if (row.length === 0) return Infinity;\n  const sum = row.reduce((a, r) => a + r.area, 0);\n  const rmax = Math.max(...row.map((r) => r.area));\n  const rmin = Math.min(...row.map((r) => r.area));\n  return Math.max((length * length * rmax) / (sum * sum), (sum * sum) / (length * length * rmin));\n}\n\nfunction layoutRow(row, rect) {\n  const sum = row.reduce((a, r) => a + r.area, 0);\n  const { x, y, w, h } = rect;\n  const placed = [];\n  if (w >= h) {\n    const stripW = sum / h;\n    let cy = y;\n    row.forEach((item) => {\n      const itemH = item.area / stripW;\n      placed.push({ ...item, x, y: cy, w: stripW, h: itemH });\n      cy += itemH;\n    });\n    return { placed, rest: { x: x + stripW, y, w: w - stripW, h } };\n  }\n  const stripH = sum / w;\n  let cx = x;\n  row.forEach((item) => {\n    const itemW = item.area / stripH;\n    placed.push({ ...item, x: cx, y, w: itemW, h: stripH });\n    cx += itemW;\n  });\n  return { placed, rest: { x, y: y + stripH, w, h: h - stripH } };\n}\n\nfunction squarify(items, container) {\n  const total = items.reduce((s, i) => s + i.value, 0);\n  const area = Math.max(container.w, 0) * Math.max(container.h, 0);\n  const sorted = items.map((i) => ({ ...i, area: total > 0 ? (i.value / total) * area : 0 })).sort((a, b) => b.area - a.area);\n\n  const results = [];\n  let rect = container;\n  let row = [];\n  let remaining = sorted;\n  while (remaining.length > 0) {\n    const candidate = remaining[0];\n    const shortSide = Math.min(rect.w, rect.h);\n    const nextRow = row.concat(candidate);\n    if (row.length === 0 || worstRatio(nextRow, shortSide) <= worstRatio(row, shortSide)) {\n      row = nextRow;\n      remaining = remaining.slice(1);\n    } else {\n      const { placed, rest } = layoutRow(row, rect);\n      results.push(...placed);\n      rect = rest;\n      row = [];\n    }\n  }\n  if (row.length > 0) results.push(...layoutRow(row, rect).placed);\n  return results;\n}\n\nfunction inset(r, px) {\n  const w = Math.max(0, r.w - 2 * px);\n  const h = Math.max(0, r.h - 2 * px);\n  return { ...r, x: r.x + px, y: r.y + px, w, h };\n}\n\n// --- Color: distinct Imprint hue per department; cost centers within a\n// department get progressively lighter tints of that hue (largest = base\n// hue, smallest = lightest), so shading intensity communicates both the\n// category identity and the nesting depth, per the spec's Notes. -----------\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}\nfunction textColorForRgb(rgb) {\n  return relLuminanceRgb(rgb) > 0.45 ? \"#1A1A17\" : \"#FAF8F1\";\n}\nfunction textColorFor(hex) {\n  return textColorForRgb(hexToRgb(hex));\n}\n\n// Keyed by department name (not array position) so color assignment stays\n// correct even if DEPARTMENTS is reordered or rebalanced -- squarify() sorts\n// its output by value, so indexing colors by position would silently\n// misassign hues once input order no longer matches value order.\nconst DEPT_COLOR_BY_NAME = new Map(DEPARTMENTS.map((d, i) => [d.name, t.palette[i % t.palette.length]]));\n\n// --- Layout: departments squarified over the chart area, then each\n// department's cost centers squarified again inside a header-inset rect --\nconst MARGIN = { top: 130, right: 32, bottom: 24, left: 32 };\nconst GUTTER = 3;\nconst HEADER_H = 30;\nconst LABEL_FONT_SIZE = 15;\nconst CHAR_WIDTH_RATIO = 0.56;\n\nfunction fmtK(v) {\n  return `$${v.toLocaleString()}K`;\n}\nfunction fitsLabel(w, h, text, fontSize) {\n  return w - 12 >= text.length * fontSize * CHAR_WIDTH_RATIO && h >= fontSize + 6;\n}\n\nfunction TreemapRect({ x, y, w, h, fill, stroke, strokeWidth, label, sublabel, fontSize, textFill, tooltip }) {\n  const showLabel = label && fitsLabel(w, h, label, fontSize);\n  const showSub = showLabel && sublabel && h >= fontSize * 2 + 10;\n  return (\n    <g>\n      <rect x={x} y={y} width={w} height={h} fill={fill} stroke={stroke} strokeWidth={strokeWidth}>\n        <title>{tooltip}</title>\n      </rect>\n      {showLabel && (\n        <text x={x + 8} y={y + fontSize + 4} fontSize={fontSize} fontWeight={500} fill={textFill} pointerEvents=\"none\">\n          {label}\n        </text>\n      )}\n      {showSub && (\n        <text x={x + 8} y={y + fontSize * 2 + 6} fontSize={fontSize - 2} fill={textFill} opacity={0.85} pointerEvents=\"none\">\n          {sublabel}\n        </text>\n      )}\n    </g>\n  );\n}\n\nfunction Treemap() {\n  // The drawing area comes from MUI X's own DrawingProvider (via\n  // ChartContainer's `margin` prop) rather than a hand-rolled offset -- the\n  // same layout primitive MUI X's own axis/legend components rely on.\n  const { left, top, width, height } = useDrawingArea();\n  const chartArea = { x: left, y: top, w: width, h: height };\n  const deptRects = squarify(departments, chartArea).map((d) => ({ ...d, color: DEPT_COLOR_BY_NAME.get(d.name) }));\n\n  return (\n    <g>\n      {deptRects.map((dept) => {\n        const outer = inset(dept, GUTTER);\n        const pct = ((dept.value / grandTotal) * 100).toFixed(1);\n        const canShowHeader = outer.h >= HEADER_H + 40 && outer.w >= 70;\n        const headerText = textColorFor(dept.color);\n\n        if (!canShowHeader) {\n          // Too small for a nested breakdown -- fill flat with the base hue.\n          return (\n            <TreemapRect\n              key={dept.name}\n              x={outer.x}\n              y={outer.y}\n              w={outer.w}\n              h={outer.h}\n              fill={dept.color}\n              stroke={t.pageBg}\n              strokeWidth={1.5}\n              label={dept.name}\n              fontSize={LABEL_FONT_SIZE - 1}\n              textFill={headerText}\n              tooltip={`${dept.name}: ${fmtK(dept.value)} (${pct}%)`}\n            />\n          );\n        }\n\n        const innerArea = { x: outer.x, y: outer.y + HEADER_H, w: outer.w, h: outer.h - HEADER_H };\n        const itemRects = squarify(dept.items, innerArea);\n        const maxItemArea = Math.max(...itemRects.map((x) => x.area));\n\n        return (\n          <g key={dept.name}>\n            <rect x={outer.x} y={outer.y} width={outer.w} height={HEADER_H} fill={dept.color}>\n              <title>{`${dept.name}: ${fmtK(dept.value)} (${pct}% of total)`}</title>\n            </rect>\n            <text x={outer.x + 8} y={outer.y + HEADER_H / 2 + 5} fontSize={LABEL_FONT_SIZE} fontWeight={700} fill={headerText}>\n              {dept.name} · {fmtK(dept.value)}\n            </text>\n            <rect x={outer.x} y={outer.y} width={outer.w} height={outer.h} fill=\"none\" stroke={t.pageBg} strokeWidth={GUTTER} />\n            {itemRects.map((item) => {\n              const r = inset(item, 2);\n              // Largest cost center keeps the department's base hue; smaller\n              // ones tint progressively lighter, so shade intensity echoes\n              // relative size within the department (nesting-depth cue).\n              const tintFactor = itemRects.length <= 1 ? 0 : (1 - item.area / maxItemArea) * 0.75;\n              const rgb = tintRgb(dept.color, tintFactor);\n              const fill = `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;\n              const itemPct = ((item.value / dept.value) * 100).toFixed(0);\n              return (\n                <TreemapRect\n                  key={item.name}\n                  x={r.x}\n                  y={r.y}\n                  w={r.w}\n                  h={r.h}\n                  fill={fill}\n                  stroke={t.pageBg}\n                  strokeWidth={1.5}\n                  label={item.name}\n                  sublabel={fmtK(item.value)}\n                  fontSize={LABEL_FONT_SIZE - 2}\n                  textFill={textColorForRgb(rgb)}\n                  tooltip={`${dept.name} / ${item.name}: ${fmtK(item.value)} (${itemPct}% of ${dept.name})`}\n                />\n              );\n            })}\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nconst topDept = departments.reduce((a, b) => (b.value > a.value ? b : a));\nconst topDeptPct = ((topDept.value / grandTotal) * 100).toFixed(0);\nconst insight = `${topDept.name} leads at ${fmtK(topDept.value)} — ${topDeptPct}% of total budget`;\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 Treemap() reads back via\n// useDrawingArea(), so the top-chrome/plot split is expressed through MUI X's\n// own layout system rather than a parallel hand-rolled offset. The treemap\n// body itself is laid out in absolute pixel space via squarify() above, so no\n// axis/scale is 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={48} textAnchor=\"middle\" fontSize={titleFontSize} fontWeight={600} fill={t.ink}>\n        {title}\n      </text>\n      <text x={width / 2} y={78} textAnchor=\"middle\" fontSize={14} fill={t.inkSoft}>\n        Company expense breakdown by department and cost center · values in $K, area ∝ budget\n      </text>\n      <text x={width / 2} y={102} textAnchor=\"middle\" fontSize={13} fontStyle=\"italic\" fill={DEPT_COLOR_BY_NAME.get(topDept.name)}>\n        {insight}\n      </text>\n      <Treemap />\n    </ChartContainer>\n  );\n}\n"}