{"spec_id":"flamegraph-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// flamegraph-basic: Flame Graph for Performance Profiling\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-08\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst theme = window.ANYPLOT_THEME;\nconst { width, height } = window.ANYPLOT_SIZE;\n\nconst margin = { top: 110, right: 50, bottom: 78, left: 50 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: synthetic CPU profile (semicolon stacks, sample counts) ---------\n// Models a typical web-service profile: HTTP request handler, background job\n// worker, and the runtime garbage collector. `stack` is the call path from\n// `main` to the leaf frame; `value` is how many CPU samples were observed in\n// that exact stack frame (i.e. self-time of the leaf).\nconst samples = [\n  { stack: \"main;http_handler;parse_request;tokenize_headers\", value: 38 },\n  { stack: \"main;http_handler;parse_request;decode_body;json_parse\", value: 64 },\n  { stack: \"main;http_handler;parse_request;decode_body\", value: 12 },\n  { stack: \"main;http_handler;route_match;regex_compile\", value: 22 },\n  { stack: \"main;http_handler;route_match;regex_exec\", value: 31 },\n  { stack: \"main;http_handler;auth_middleware;verify_jwt;hmac_sha256\", value: 54 },\n  { stack: \"main;http_handler;auth_middleware;verify_jwt\", value: 9 },\n  { stack: \"main;http_handler;auth_middleware;load_session;redis_get\", value: 41 },\n  { stack: \"main;http_handler;dispatch;get_user;db_query;pool_checkout\", value: 18 },\n  { stack: \"main;http_handler;dispatch;get_user;db_query;execute;deserialize_rows\", value: 112 },\n  { stack: \"main;http_handler;dispatch;get_user;db_query;execute\", value: 47 },\n  { stack: \"main;http_handler;dispatch;get_user;render_template;escape_html\", value: 28 },\n  { stack: \"main;http_handler;dispatch;get_user;render_template\", value: 11 },\n  { stack: \"main;http_handler;dispatch;post_order;validate;schema_check\", value: 24 },\n  { stack: \"main;http_handler;dispatch;post_order;db_transaction;pool_checkout\", value: 16 },\n  { stack: \"main;http_handler;dispatch;post_order;db_transaction;execute;deserialize_rows\", value: 76 },\n  { stack: \"main;http_handler;dispatch;post_order;db_transaction;execute\", value: 35 },\n  { stack: \"main;http_handler;dispatch;post_order;publish_event;kafka_send\", value: 22 },\n  { stack: \"main;http_handler;serialize_response;json_encode\", value: 44 },\n  { stack: \"main;http_handler;serialize_response\", value: 8 },\n  { stack: \"main;worker;poll_queue;sqs_receive\", value: 36 },\n  { stack: \"main;worker;process_job;load_payload;decompress;zstd_decode\", value: 29 },\n  { stack: \"main;worker;process_job;load_payload\", value: 8 },\n  { stack: \"main;worker;process_job;aggregate_metrics;groupby;hash_combine\", value: 41 },\n  { stack: \"main;worker;process_job;aggregate_metrics;groupby\", value: 9 },\n  { stack: \"main;worker;process_job;aggregate_metrics;compute_stats\", value: 38 },\n  { stack: \"main;worker;process_job;persist_result;db_query;execute\", value: 27 },\n  { stack: \"main;worker;process_job;persist_result;s3_put\", value: 19 },\n  { stack: \"main;runtime_gc;mark_phase;trace_objects\", value: 48 },\n  { stack: \"main;runtime_gc;mark_phase;rescan_remembered_set\", value: 14 },\n  { stack: \"main;runtime_gc;sweep_phase;free_unreachable\", value: 23 },\n  { stack: \"main;runtime_gc;sweep_phase;compact_arena\", value: 11 },\n];\n\n// --- Build nested tree from semicolon-delimited stack paths ----------------\nfunction buildTree(rows) {\n  const root = { name: \"main\", children: [], _idx: {}, value: 0 };\n  for (const { stack, value } of rows) {\n    const parts = stack.split(\";\");\n    let node = root;\n    for (let i = 1; i < parts.length; i++) {\n      const part = parts[i];\n      if (!node._idx[part]) {\n        const child = { name: part, children: [], _idx: {}, value: 0 };\n        node._idx[part] = child;\n        node.children.push(child);\n      }\n      node = node._idx[part];\n    }\n    node.value += value;\n  }\n  (function strip(n) {\n    delete n._idx;\n    if (!n.children.length) delete n.children;\n    else n.children.forEach(strip);\n  })(root);\n  return root;\n}\n\nconst treeRoot = buildTree(samples);\n\n// d3.hierarchy + .sum: each node's value = self_value + Σ children.value.\n// d3.partition then lays children horizontally inside the parent's range. When\n// children's sum < parent.value (parent has self-time), the parent's bar on\n// the row above shows a \"gap\" — that gap is the parent's self-time.\nconst rootH = d3.hierarchy(treeRoot)\n  .sum((d) => d.value || 0)\n  .sort((a, b) => b.value - a.value);\n\nd3.partition().size([iw, 1])(rootH);\n\nconst nodes = rootH.descendants();\nconst maxDepth = d3.max(nodes, (d) => d.depth);\nconst rowGap = 3;\nconst rowH = Math.floor(ih / (maxDepth + 1));\n\n// --- Color by top-level module (depth-1 ancestor) --------------------------\nfunction topModule(node) {\n  let n = node;\n  while (n.depth > 1) n = n.parent;\n  return n.data.name;\n}\n\n// Modules sorted by total samples (desc) so the biggest gets palette[0].\nconst modules = (rootH.children || [])\n  .slice()\n  .sort((a, b) => b.value - a.value)\n  .map((c) => c.data.name);\nconst colorScale = d3.scaleOrdinal().domain(modules).range(t.palette);\n\n// \"main\" root bar — outside the categorical pool; use the muted anchor.\nconst mutedAnchor = theme === \"dark\" ? \"#A8A79F\" : \"#6B6A63\";\n\n// Per-fill text-color choice tuned for the Imprint palette (luminance-based\n// alone misclassifies ochre and cyan at the edges).\nconst TEXT_DARK = \"#1A1A17\";\nconst TEXT_LIGHT = \"#F0EFE8\";\nconst FILL_TO_TEXT = {\n  \"#009E73\": TEXT_LIGHT, // brand green\n  \"#C475FD\": TEXT_DARK,  // lavender\n  \"#4467A3\": TEXT_LIGHT, // blue\n  \"#BD8233\": TEXT_LIGHT, // ochre\n  \"#AE3030\": TEXT_LIGHT, // matte red\n  \"#2ABCCD\": TEXT_DARK,  // cyan\n  \"#954477\": TEXT_LIGHT, // rose\n  \"#99B314\": TEXT_DARK,  // lime\n};\nconst pickTextColor = (fill) => FILL_TO_TEXT[fill] || TEXT_LIGHT;\n\n// --- SVG mount -------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Bars ------------------------------------------------------------------\nconst cell = g.selectAll(\".cell\").data(nodes).join(\"g\")\n  .attr(\"class\", \"cell\")\n  .attr(\"transform\", (d) => {\n    const x = d.x0;\n    const y = ih - (d.depth + 1) * rowH;\n    return `translate(${x},${y})`;\n  });\n\ncell.append(\"rect\")\n  .attr(\"width\", (d) => Math.max(0, d.x1 - d.x0 - 1))\n  .attr(\"height\", rowH - rowGap)\n  .attr(\"fill\", (d) => d.depth === 0 ? mutedAnchor : colorScale(topModule(d)))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 0.8);\n\n// Function-name labels — only rendered when the bar is wide enough to fit at\n// least 3 characters at the chosen monospace font; otherwise omitted entirely.\nconst charW = 8.6;\nconst padX = 10;\nconst fontPx = 15;\ncell.append(\"text\")\n  .attr(\"x\", padX)\n  .attr(\"y\", (rowH - rowGap) / 2 + 1)\n  .attr(\"dominant-baseline\", \"middle\")\n  .style(\"font-size\", `${fontPx}px`)\n  .style(\"font-family\", \"ui-monospace, SFMono-Regular, Menlo, Consolas, monospace\")\n  .style(\"font-weight\", \"500\")\n  .attr(\"fill\", (d) => pickTextColor(d.depth === 0 ? mutedAnchor : colorScale(topModule(d))))\n  .text((d) => {\n    const w = d.x1 - d.x0;\n    const maxChars = Math.floor((w - 2 * padX) / charW);\n    if (maxChars < 3) return \"\";\n    const name = d.data.name;\n    if (name.length <= maxChars) return name;\n    return name.slice(0, maxChars - 1) + \"…\";\n  });\n\n// --- X axis (sample counts) ------------------------------------------------\nconst totalSamples = rootH.value;\nconst xScale = d3.scaleLinear().domain([0, totalSamples]).range([0, iw]);\nconst xAxisG = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih + 6})`)\n  .call(d3.axisBottom(xScale).ticks(10).tickFormat(d3.format(\",d\")).tickSizeOuter(0));\nxAxisG.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nxAxisG.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nxAxisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 18)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\")\n  .text(`Cumulative CPU samples (${d3.format(\",d\")(totalSamples)} total) — bar width ∝ time in stack`);\n\n// --- Module legend (centered, below title) ---------------------------------\nconst swatchW = 24, swatchH = 16, swatchGap = 12, itemGap = 56;\n// Pre-measure each label so the legend can be centered tightly.\nconst tmpText = svg.append(\"text\").style(\"font-size\", \"16px\").style(\"visibility\", \"hidden\");\nconst measure = (s) => { tmpText.text(s); return tmpText.node().getComputedTextLength(); };\nconst itemWidths = modules.map((m) => swatchW + swatchGap + measure(m));\ntmpText.remove();\nconst legendTotalW = itemWidths.reduce((a, b) => a + b, 0) + itemGap * (modules.length - 1);\nlet cursor = Math.round((width - legendTotalW) / 2);\nconst legendY = 78;\n\nmodules.forEach((m, i) => {\n  const grp = svg.append(\"g\").attr(\"transform\", `translate(${cursor},${legendY})`);\n  grp.append(\"rect\")\n    .attr(\"x\", 0).attr(\"y\", -12)\n    .attr(\"width\", swatchW).attr(\"height\", swatchH)\n    .attr(\"fill\", colorScale(m));\n  grp.append(\"text\")\n    .attr(\"x\", swatchW + swatchGap).attr(\"y\", 0)\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\")\n    .text(m);\n  cursor += itemWidths[i] + itemGap;\n});\n\n// --- Title -----------------------------------------------------------------\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n  .text(\"flamegraph-basic · javascript · d3 · anyplot.ai\");\n"}