diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-24 21:15:20 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-24 21:15:20 +0200 |
| commit | 93d587a6f5ae453907de3d5556866b60bac405cb (patch) | |
| tree | e177a5758b486e75fc66552fb0874b95bb145726 /internal/flamegraph/svgwriter_jscode.go | |
| parent | 8361fd22d45e4fbf6b24309aaa1b6d49d9010759 (diff) | |
flamegraph: improve interactive zoom and serve svg over embedded http
Diffstat (limited to 'internal/flamegraph/svgwriter_jscode.go')
| -rw-r--r-- | internal/flamegraph/svgwriter_jscode.go | 155 |
1 files changed, 146 insertions, 9 deletions
diff --git a/internal/flamegraph/svgwriter_jscode.go b/internal/flamegraph/svgwriter_jscode.go index 52f8818..3ac00fd 100644 --- a/internal/flamegraph/svgwriter_jscode.go +++ b/internal/flamegraph/svgwriter_jscode.go @@ -7,16 +7,33 @@ const fg = { frames: [], info: null, matchColor: "rgb(220, 30, 70)", + zoomStack: [], + zoomRange: null, + rootWidth: 0, }; function fgInit() { fg.frames = Array.from(document.querySelectorAll("g.frame")); fg.info = document.getElementById("fg-info"); + fg.rootWidth = fgDetectRootWidth(); fg.frames.forEach((frame) => { - frame.addEventListener("click", (ev) => fgZoom(ev.currentTarget)); + fgSnapshotOriginalGeometry(frame); + frame.addEventListener("click", (ev) => { + if (ev.detail > 1) return; + ev.stopPropagation(); + fgZoom(ev.currentTarget); + }); + frame.addEventListener("dblclick", (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + fgResetZoom(); + }); frame.addEventListener("mouseenter", (ev) => fgHover(ev.currentTarget)); }); - document.addEventListener("dblclick", () => fgResetZoom()); + document.addEventListener("dblclick", (ev) => { + ev.preventDefault(); + fgResetZoom(); + }); } function fgHover(frame) { @@ -26,15 +43,42 @@ function fgHover(frame) { } function fgZoom(frame) { - const x = Number(frame.dataset.x || "0"); - const w = Number(frame.dataset.w || "0"); + const x = fgOriginalX(frame); + const w = fgOriginalW(frame); if (w <= 0) return; - const end = x + w; + if (fg.zoomRange) { + fg.zoomStack.push(fg.zoomRange); + } + fg.zoomRange = { x: x, w: w, depth: Number(frame.dataset.depth || "0") }; + fgApplyZoom(); +} + +function fgApplyZoom() { + if (!fg.zoomRange) { + fg.frames.forEach((frame) => { + frame.style.display = ""; + }); + return; + } + const x = fg.zoomRange.x; + const end = x + fg.zoomRange.w; + const width = fg.zoomRange.w; + const minDepth = fg.zoomRange.depth; + const eps = 1e-6; + const scale = fg.rootWidth / width; fg.frames.forEach((other) => { - const ox = Number(other.dataset.x || "0"); - const ow = Number(other.dataset.w || "0"); - const sameBand = Number(other.dataset.depth || "0") >= Number(frame.dataset.depth || "0"); - if (sameBand && ox >= x && ox + ow <= end) { + const ox = fgOriginalX(other); + const ow = fgOriginalW(other); + const depth = Number(other.dataset.depth || "0"); + const inSelectedRange = ox >= x-eps && ox+ow <= end+eps; + const isAncestor = depth < minDepth && ox <= x+eps && ox+ow >= end-eps; + + if (isAncestor || (depth >= minDepth && inSelectedRange)) { + if (isAncestor) { + fgSetFrameGeometry(other, 0, fg.rootWidth); + } else { + fgSetFrameGeometry(other, (ox-x)*scale, ow*scale); + } other.style.display = ""; } else { other.style.display = "none"; @@ -42,8 +86,20 @@ function fgZoom(frame) { }); } +function fgUndoZoom() { + if (fg.zoomStack.length === 0) { + fgResetZoom(); + return; + } + fg.zoomRange = fg.zoomStack.pop(); + fgApplyZoom(); +} + function fgResetZoom() { + fg.zoomStack = []; + fg.zoomRange = null; fg.frames.forEach((frame) => { + fgRestoreFrameGeometry(frame); frame.style.display = ""; }); } @@ -73,5 +129,86 @@ function fgResetSearch() { }); } +function fgDetectRootWidth() { + let maxEnd = 0; + fg.frames.forEach((frame) => { + const x = Number(frame.dataset.x || "0"); + const w = Number(frame.dataset.w || "0"); + maxEnd = Math.max(maxEnd, x + w); + }); + return maxEnd; +} + +function fgSnapshotOriginalGeometry(frame) { + const rect = frame.querySelector("rect"); + const text = frame.querySelector("text"); + frame.dataset.ox = frame.dataset.x || "0"; + frame.dataset.ow = frame.dataset.w || "0"; + if (rect) { + rect.dataset.ox = rect.getAttribute("x") || "0"; + rect.dataset.ow = rect.getAttribute("width") || "0"; + } + if (text) { + text.dataset.ox = text.getAttribute("x") || "0"; + text.dataset.hidden = text.style.display === "none" ? "1" : "0"; + text.dataset.full = text.textContent || frame.dataset.name || ""; + } +} + +function fgOriginalX(frame) { + return Number(frame.dataset.ox || frame.dataset.x || "0"); +} + +function fgOriginalW(frame) { + return Number(frame.dataset.ow || frame.dataset.w || "0"); +} + +function fgSetFrameGeometry(frame, x, w) { + const rect = frame.querySelector("rect"); + const text = frame.querySelector("text"); + if (rect) { + rect.setAttribute("x", String(x)); + rect.setAttribute("width", String(w)); + } + if (text) { + text.setAttribute("x", String(x + 3)); + fgFitLabel(text, w); + } +} + +function fgRestoreFrameGeometry(frame) { + const rect = frame.querySelector("rect"); + const text = frame.querySelector("text"); + if (rect) { + rect.setAttribute("x", rect.dataset.ox || "0"); + rect.setAttribute("width", rect.dataset.ow || "0"); + } + if (text) { + text.setAttribute("x", text.dataset.ox || "0"); + if (text.dataset.hidden === "1") { + text.style.display = "none"; + text.textContent = text.dataset.full || ""; + } else { + fgFitLabel(text, Number(rect ? (rect.dataset.ow || "0") : "0")); + } + } +} + +function fgFitLabel(text, width) { + const full = text.dataset.full || text.textContent || ""; + const maxChars = Math.floor((width - 6) / 7); + if (maxChars < 3) { + text.style.display = "none"; + text.textContent = full; + return; + } + text.style.display = ""; + if (full.length <= maxChars) { + text.textContent = full; + return; + } + text.textContent = full.slice(0, maxChars - 1) + "…"; +} + window.addEventListener("DOMContentLoaded", fgInit); ` |
