<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>D3 Flamegraph (Root Bottom + Short Boxes)</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/dark.min.css">
<style>
:root {
--bg-color: #121212;
--text-color: #f0f0f0;
--tooltip-bg: rgba(248, 248, 248, 0.8);
--tooltip-text: #242424;
--legend-bg: #1e1e1e;
}
html,
body {
margin: 0;
height: 100%;
background-color: var(--bg-color);
color: var(--text-color);
font-family: sans-serif;
}
#outer {
padding: 50px;
box-sizing: border-box;
height: 100%;
width: 100%;
}
#container {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
}
.tooltip {
position: absolute;
background-color: var(--tooltip-bg);
color: var(--tooltip-text);
position: absolute;
bottom: 0px;
right: 0px;
}
svg {
flex: 1;
height: 100%;
}
.legend {
font-size: 14px;
cursor: pointer;
}
.legend rect {
width: 16px;
height: 16px;
stroke: black;
stroke-width: 1px;
}
.legend text {
fill: var(--text-color);
user-select: none;
}
.line-numbers.current {
color: red;
background-color: yellow;
}
</style>
</head>
<body>
<div id="outer">
<div id="container">
<svg id="chart"></svg>
</div>
</div>
<div class="tooltip">
aaa
</div>
<script type="module">
import hljs from 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/es/highlight.min.js'
import rust from 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/es/languages/rust.min.js';
hljs.registerLanguage('rust', rust);
hljs.highlightAll();
const data = { undefined };
const svg = d3.select("#chart");
const container = document.getElementById("container");
const fullWidth = container.clientWidth;
const fullHeight = container.clientHeight;
const legendWidth = 200;
const chartWidth = fullWidth - legendWidth;
const chartHeight = fullHeight;
const boxHeight = 20;
svg
.attr("viewBox", `0 0 ${fullWidth} ${chartHeight}`)
.attr("preserveAspectRatio", "xMinYMin meet");
const zoomLayer = svg.append("g");
const tooltip = d3.select(".tooltip");
const root = d3.hierarchy(data, d => d.children);
root.each(d => d.value = d.data.allocation);
const depth = root.height + 1;
const graphHeight = depth * boxHeight;
const svgHeight = chartHeight; const scaleY = graphHeight > svgHeight ? svgHeight / graphHeight : 1;
const partition = d3.partition().size([chartWidth, graphHeight]);
partition(root);
root.each(d => {
d.y0 = graphHeight - (d.depth + 1) * boxHeight;
d.y1 = graphHeight - d.depth * boxHeight;
});
zoomLayer.attr("transform", `scale(1, ${scaleY})`);
const categories = [...new Set(root.descendants().map(d => d.data.category))];
categories.sort((a, b) => a.localeCompare(b));
const colorScale = d3.scaleOrdinal([
"#d62728",
"#ff7f0e",
"#1f77b4",
"#2ca02c",
"#9467bd",
"#8c564b",
"#e377c2",
"#7f7f7f",
"#bcbd22",
"#17becf",
]).domain(categories);
const rects = zoomLayer.selectAll("rect")
.data(root.descendants())
.enter().append("rect")
.attr("class", "node")
.attr("x", d => d.x0)
.attr("y", d => d.y0)
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0)
.attr("fill", d => colorScale(d.data.category))
.attr("stroke", "#fff")
.on("mouseover", (event, d) => {
let code = ''
if (d.data.key.file_content) {
code = `<pre style="display: flex;">
<span style="align-items: center; display: flex; flex-direction: column;" id="line-numbers"></span><code class="language-rust">
${d.data.key.file_content.before.join('\n')}
<span class="highlighted-line">${d.data.key.file_content.highlighted}</span>
${d.data.key.file_content.after.join('\n')}
</code></pre>`
}
tooltip.style("opacity", 1)
.html(`
<strong>${d.data.key.filename}</strong><br>
lineno: ${d.data.key.lineno}<br>
fn_name: ${d.data.key.fn_name}<br>
Allocation: ${d.data.allocation} bytes (count ${d.data.allocation_count})<br>
Deallocation: ${d.data.deallocation} bytes (count ${d.data.deallocation_count})<br>
Allocation diff: ${Number(d.data.allocation) - Number(d.data.deallocation)}<br>
Category: ${d.data.category}
${code}
`);
if (d.data.key.file_content) {
hljs.highlightAll();
const myCodeBlock = document.getElementById('line-numbers');
const before = document.createElement('span');
before.className = 'line-numbers';
before.innerText = ' ';
myCodeBlock.appendChild(before);
const before2 = document.createElement('span');
before2.className = 'line-numbers';
before2.innerText = ' ';
myCodeBlock.appendChild(before2);
let line_numbers = d.data.key.lineno - d.data.key.file_content.before.length;
for (let i = 0; i < d.data.key.file_content.before.length; i++) {
const lineNumber = document.createElement('span');
lineNumber.className = 'line-numbers';
lineNumber.innerText = (line_numbers++);
myCodeBlock.appendChild(lineNumber);
}
const lineNumber = document.createElement('span');
lineNumber.className = 'line-numbers current';
lineNumber.innerText = (line_numbers++);
myCodeBlock.appendChild(lineNumber);
for (let i = 0; i < d.data.key.file_content.after.length; i++) {
const lineNumber = document.createElement('span');
lineNumber.className = 'line-numbers';
lineNumber.innerText = (line_numbers++);
myCodeBlock.appendChild(lineNumber);
}
}
})
.on("mousemove", (event) => {
if (event.pageX < window.innerWidth / 2) {
tooltip.style("right", 10 + "px")
.style("left", "auto")
.style("bottom", 10 + "px");
} else {
tooltip.style("left", 10 + "px")
.style("right", "auto")
.style("bottom", 10 + "px");
}
})
.on("mouseout", () => {
tooltip.style("opacity", 0);
});
zoomLayer.selectAll("text")
.data(root.descendants())
.enter()
.append("text")
.attr("x", d => (d.x0 + d.x1) / 2)
.attr("y", d => (d.y0 + d.y1) / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.style("fill", "#fff")
.style("pointer-events", "auto")
.style("user-select", "text")
.text(d => {
const width = d.x1 - d.x0;
const text = `${d.data.key.filename}:${d.data.key.lineno}`
return width > 10 * text.length ? text : "";
});
const legend = svg.append("g")
.attr("class", "legend")
.attr("transform", `translate(${chartWidth + 20}, 20)`);
categories.forEach((category, i) => {
const legendRow = legend.append("g")
.attr("transform", `translate(0, ${i * 25})`)
.on("click", () => highlightCategory(category));
legendRow.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 16)
.attr("height", 16)
.attr("fill", colorScale(category));
legendRow.append("text")
.attr("x", 24)
.attr("y", 12)
.text(category)
.style("font-size", "14px")
.style("alignment-baseline", "middle");
});
let activeCategory = null;
function highlightCategory(category) {
if (activeCategory === category) {
rects.transition().duration(300).style("opacity", 1);
activeCategory = null;
} else {
rects.transition().duration(300)
.style("opacity", d => d.data.category === category ? 1 : 0.3);
activeCategory = category;
}
}
</script>
</body>
</html>