{"spec_id":"treemap-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// treemap-basic: Basic Treemap\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-08-24\n\n//# anyplot-orientation: landscape\n\n// Only the core Highcharts bundle is loaded (no `treemap` module), so the\n// rectangles are computed with a squarified-treemap algorithm (Bruls,\n// Huizing & van Wijk) and drawn by hand with `chart.renderer.rect()` — the\n// same hand-drawn technique sunburst-basic / sankey-basic / heatmap-basic\n// use for series types the core bundle doesn't ship. Hover uses a native\n// SVG `<title>` per rectangle (same trick as sunburst-basic), which covers\n// the whole tile — not just an inscribed circle.\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: annual department budget by cost center (USD thousands) --------\nconst DEPARTMENTS = [\n  {\n    name: \"Engineering\",\n    items: [\n      { name: \"Cloud Infrastructure\", value: 850 },\n      { name: \"Contractor Dev\", value: 420 },\n      { name: \"R&D Prototypes\", value: 260 },\n      { name: \"Tooling & Licenses\", value: 180 },\n    ],\n  },\n  {\n    name: \"Sales & Marketing\",\n    items: [\n      { name: \"Digital Advertising\", value: 610 },\n      { name: \"Sales Commissions\", value: 480 },\n      { name: \"Trade Shows\", value: 240 },\n      { name: \"Content & Creative\", value: 190 },\n    ],\n  },\n  {\n    name: \"Operations\",\n    items: [\n      { name: \"Customer Support\", value: 340 },\n      { name: \"Facilities\", value: 320 },\n      { name: \"Logistics\", value: 275 },\n    ],\n  },\n  {\n    name: \"G&A\",\n    items: [\n      { name: \"HR & Recruiting\", value: 210 },\n      { name: \"Legal & Compliance\", value: 150 },\n      { name: \"Finance & Accounting\", value: 130 },\n    ],\n  },\n];\n\nDEPARTMENTS.forEach((dept, i) => {\n  dept.color = t.palette[i]; // canonical order — abstract departments, no semantic hue\n  dept.total = dept.items.reduce((s, d) => s + d.value, 0);\n});\nconst GRAND_TOTAL = DEPARTMENTS.reduce((s, d) => s + d.total, 0);\n// Largest single cost center overall — gets a bolder border below as the\n// chart's one focal-point emphasis beyond the standard size/color/tint encoding.\nconst TOP_LEAF_VALUE = Math.max(...DEPARTMENTS.flatMap((d) => d.items.map((i) => i.value)));\n\n// --- Squarified treemap layout (values -> rectangles) -----------------------\nfunction worstRatio(row, side) {\n  const sum = row.reduce((s, d) => s + d.area, 0);\n  const maxA = Math.max(...row.map((d) => d.area));\n  const minA = Math.min(...row.map((d) => d.area));\n  return Math.max((side * side * maxA) / (sum * sum), (sum * sum) / (side * side * minA));\n}\n\nfunction squarify(items, x0, y0, x1, y1, out) {\n  if (!items.length) return;\n  const w = x1 - x0;\n  const h = y1 - y0;\n  const side = Math.min(w, h);\n  let row = [items[0]];\n  let i = 1;\n  while (i < items.length) {\n    const nextRow = row.concat(items[i]);\n    if (worstRatio(nextRow, side) <= worstRatio(row, side)) {\n      row = nextRow;\n      i++;\n    } else {\n      break;\n    }\n  }\n  const rowSum = row.reduce((s, d) => s + d.area, 0);\n  if (w >= h) {\n    const stripW = rowSum / h;\n    let cy = y0;\n    row.forEach((d) => {\n      const rectH = d.area / stripW;\n      out.push({ item: d, x: x0, y: cy, w: stripW, h: rectH });\n      cy += rectH;\n    });\n    squarify(items.slice(row.length), x0 + stripW, y0, x1, y1, out);\n  } else {\n    const stripH = rowSum / w;\n    let cx = x0;\n    row.forEach((d) => {\n      const rectW = d.area / stripH;\n      out.push({ item: d, x: cx, y: y0, w: rectW, h: stripH });\n      cx += rectW;\n    });\n    squarify(items.slice(row.length), x0, y0 + stripH, x1, y1, out);\n  }\n}\n\nfunction layout(entries, x0, y0, x1, y1) {\n  const total = entries.reduce((s, d) => s + d.value, 0);\n  const scale = ((x1 - x0) * (y1 - y0)) / total;\n  const sized = entries\n    .slice()\n    .sort((a, b) => b.value - a.value)\n    .map((d) => ({ ...d, area: d.value * scale }));\n  const out = [];\n  squarify(sized, x0, y0, x1, y1, out);\n  return out;\n}\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 money = (v) => `$${v.toLocaleString(\"en-US\")}K`;\n\n// Shrink-to-fit: returns the largest fontSize in [min, nominal] whose\n// estimated text width fits maxWidth, or null if even `min` overflows —\n// callers treat null as \"omit the label\" (small tiles stay unlabeled).\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\n// --- Title (fontsize scaled off the 67-char baseline) -----------------------\nconst TITLE_TEXT = \"Annual Department Budget · treemap-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 treemap 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: 40,\n    marginLeft: 40,\n    marginRight: 40,\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 annual operating budget by department and cost center — tile area ∝ spend (USD thousands)\",\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 DEPT_GAP = 10;\nconst LEAF_GAP = 4;\nconst HEADER_H = 42;\nconst HEADER_MIN_W = 90;\nconst HEADER_MIN_H = 90;\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\nconst g = chart.renderer.g(\"treemap\").add();\n\n// --- Level 1: department blocks ---------------------------------------------\nconst deptItems = DEPARTMENTS.map((d) => ({ name: d.name, value: d.total, dept: d }));\nconst deptRects = layout(deptItems, 0, 0, PLOT_W, PLOT_H);\n\ndeptRects.forEach(({ item, x, y, w, h }) => {\n  const dept = item.dept;\n  const bx = chart.plotLeft + x + DEPT_GAP / 2;\n  const by = chart.plotTop + y + DEPT_GAP / 2;\n  const bw = w - DEPT_GAP;\n  const bh = h - DEPT_GAP;\n  const deptPct = ((dept.total / GRAND_TOTAL) * 100).toFixed(1);\n\n  const showHeader = bw >= HEADER_MIN_W && bh >= HEADER_MIN_H;\n  const border = chart.renderer\n    .rect(bx, by, bw, bh, 3)\n    .attr({ fill: \"none\", stroke: dept.color, \"stroke-width\": 3, zIndex: 2 })\n    .add(g);\n  addTooltip(border, `${dept.name}\\n${money(dept.total)} total (${deptPct}% of budget)`);\n\n  let leafY0 = by + LEAF_GAP;\n  if (showHeader) {\n    const headerEl = chart.renderer\n      .rect(bx, by, bw, HEADER_H, 3)\n      .attr({ fill: dept.color, zIndex: 3 })\n      .add(g);\n    addTooltip(headerEl, `${dept.name}\\n${money(dept.total)} total (${deptPct}% of budget)`);\n\n    const headerLabel = `${dept.name} · ${money(dept.total)}`;\n    const headerSize = fitFontSize(headerLabel, bw - 20, 18, 12);\n    const text = headerSize ? headerLabel : dept.name;\n    const finalSize = headerSize || fitFontSize(dept.name, bw - 20, 18, 12);\n    if (finalSize) {\n      chart.renderer\n        .text(text, bx + 10, by + HEADER_H / 2 + finalSize * 0.35)\n        .attr({ align: \"left\", zIndex: 4 })\n        .css({ color: labelColorFor(dept.color), fontSize: `${finalSize}px`, fontWeight: \"600\", pointerEvents: \"none\" })\n        .add(g);\n    }\n    leafY0 = by + HEADER_H + LEAF_GAP;\n  }\n\n  const innerX0 = bx + LEAF_GAP;\n  const innerY0 = leafY0;\n  const innerX1 = bx + bw - LEAF_GAP;\n  const innerY1 = by + bh - LEAF_GAP;\n  if (innerX1 <= innerX0 || innerY1 <= innerY0) return;\n\n  const leafRects = layout(dept.items, innerX0, innerY0, innerX1, innerY1);\n  leafRects.forEach((leaf, rank) => {\n    const tint = Math.min(0.15 + rank * 0.09, 0.6);\n    const fill = mix(dept.color, \"#FFFFFF\", tint);\n    const pct = ((leaf.item.value / GRAND_TOTAL) * 100).toFixed(1);\n    const isTopLeaf = leaf.item.value === TOP_LEAF_VALUE;\n\n    const tileEl = chart.renderer\n      .rect(leaf.x, leaf.y, leaf.w, leaf.h)\n      .attr({\n        fill,\n        stroke: isTopLeaf ? t.ink : t.pageBg,\n        \"stroke-width\": isTopLeaf ? 3 : 2,\n        zIndex: isTopLeaf ? 4 : 3,\n      })\n      .add(g);\n    addTooltip(\n      tileEl,\n      `${dept.name} → ${leaf.item.name}\\n${money(leaf.item.value)} (${pct}% of budget)${isTopLeaf ? \" — largest line item\" : \"\"}`,\n    );\n\n    const nameSize = fitFontSize(leaf.item.name, leaf.w - 16, 15, 10);\n    if (nameSize && leaf.h >= 30) {\n      const showValue = leaf.h >= 52 && leaf.w >= 60;\n      const nameY = showValue ? leaf.y + leaf.h / 2 - 6 : leaf.y + leaf.h / 2 + nameSize * 0.35;\n      chart.renderer\n        .text(leaf.item.name, leaf.x + 8, nameY)\n        .attr({ align: \"left\", zIndex: 5 })\n        .css({ color: labelColorFor(fill), fontSize: `${nameSize}px`, fontWeight: \"600\", pointerEvents: \"none\" })\n        .add(g);\n      if (showValue) {\n        chart.renderer\n          .text(money(leaf.item.value), leaf.x + 8, leaf.y + leaf.h / 2 + 14)\n          .attr({ align: \"left\", zIndex: 5 })\n          .css({ color: labelColorFor(fill), fontSize: \"12px\", pointerEvents: \"none\" })\n          .add(g);\n      }\n    } else {\n      // Fallback for tiles too small to fit the cost-center name: fall back to\n      // a value-only label (smaller min font) so no tile ships completely blank.\n      const valueText = money(leaf.item.value);\n      const valueSize = fitFontSize(valueText, leaf.w - 10, 12, 7);\n      if (valueSize && leaf.h >= 16) {\n        chart.renderer\n          .text(valueText, leaf.x + leaf.w / 2, leaf.y + leaf.h / 2 + valueSize * 0.35)\n          .attr({ align: \"center\", zIndex: 5 })\n          .css({ color: labelColorFor(fill), fontSize: `${valueSize}px`, fontWeight: \"600\", pointerEvents: \"none\" })\n          .add(g);\n      }\n    }\n  });\n});\n"}