diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/dashboard/dashboard.html | 92 |
1 files changed, 62 insertions, 30 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index 1c03892..cd0f418 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -60,9 +60,9 @@ #figures-box { grid-area: figures; } #document-box { grid-area: document; } svg { flex: 1; min-height: 0; width: 100%; display: block; cursor: default; } - /* Dragged to pan, also by touch. */ + /* Dragged to pan or to move a node, also by touch. */ #graph { touch-action: none; } - #graph.panning { cursor: grabbing; } + #graph.dragging, #graph.dragging * { cursor: grabbing; } svg circle { cursor: pointer; } .legend { color: var(--muted); font-size: 12px; display: flex; gap: 1em; flex-wrap: wrap; flex: none; } @@ -141,7 +141,8 @@ <span class="node silent">reports none (no LINKS_REPLY)</span> <span class="node dormant-node">dormant (to this node)</span> <span class="node focus">in focus (click a node)</span> - <span class="hint">drag to pan, wheel to zoom, double-click to reset</span> + <span class="hint">drag to pan, wheel to zoom, double-click to reset; drag a node to + pin it, double-click it to let go</span> </div> <div class="legend" id="legend"></div> <svg id="graph"><g id="view"></g></svg> @@ -337,6 +338,8 @@ function renderStats() { // --- Swarm map: a small force layout, positions kept across renders --- const positions = new Map(); +// Nodes put somewhere by hand (dragged): the layout leaves them there. +const pinned = new Set(); const transportColour = new Map([["mycelium", "#2563eb"], ["libp2p", "#9333ea"], ["tor", "#0891b2"]]); const SPARE = ["#ca8a04", "#db2777", "#65a30d"]; @@ -428,7 +431,8 @@ function layout(nodes, pairs, width, height) { force.get(b).x -= dx * pull; force.get(b).y -= dy * pull; } for (const id of nodes) { - const p = positions.get(id), f = force.get(id); + if (pinned.has(id)) continue; + const p = positions.get(id), f = force.get(id); p.x = Math.min(width - 40, Math.max(40, p.x + Math.max(-10, Math.min(10, f.x)))); p.y = Math.min(height - 20, Math.max(20, p.y + Math.max(-10, Math.min(10, f.y)))); } @@ -499,7 +503,9 @@ function renderGraph() { // Laid out anew only when who's there or who links to whom changed. const shape = nodes.join(",") + "|" + [...intended.keys(), ...measured.keys()].sort().join(","); if (shape !== lastShape) { - for (const id of [...positions.keys()]) if (!nodes.includes(id)) positions.delete(id); + for (const id of [...positions.keys()]) { + if (!nodes.includes(id)) { positions.delete(id); pinned.delete(id); } + } layout(nodes, pairs, width, height); lastShape = shape; } @@ -607,7 +613,8 @@ function renderGraph() { fill: "var(--self)", "fill-opacity": shown ? .35 : .15, stroke: "none" })); } const pressed = element => { - element.addEventListener("pointerdown", event => { event.stopPropagation(); focus(id); }); + element.addEventListener("pointerdown", event => { event.stopPropagation(); drag(event, id); }); + element.addEventListener("dblclick", () => unpin(id)); hoverable(element, id); }; // Dormant as this node sees it (`/peers`): a red outline. @@ -1011,34 +1018,59 @@ graph.addEventListener("wheel", event => { view.k = k; moveView(); }, { passive: false }); -// Pressed beside the nodes: dragged, it pans; let go where pressed (a click), back to -// this node. -let panning = null; -graph.addEventListener("pointerdown", event => { - if (event.target !== graph) return; - const at = pointer(event); - panning = { from: at, view: { ...view }, moved: false }; +// A press on the map: dragged (more than a few pixels), it `move`s by as much as the +// pointer did; let go where pressed, it's a `click`. +let press = null; +function pressing(event, move, click) { + press = { from: pointer(event), moved: false, move, click }; graph.setPointerCapture(event.pointerId); -}); +} graph.addEventListener("pointermove", event => { - if (!panning) return; + if (!press) return; const at = pointer(event); - const dx = at.x - panning.from.x, dy = at.y - panning.from.y; - if (!panning.moved && Math.hypot(dx, dy) < 4) return; - panning.moved = true; - graph.classList.add("panning"); - view.x = panning.view.x + dx; - view.y = panning.view.y + dy; - moveView(); + const dx = at.x - press.from.x, dy = at.y - press.from.y; + if (!press.moved && Math.hypot(dx, dy) < 4) return; + press.moved = true; + graph.classList.add("dragging"); + press.move(dx, dy); }); -const panned = () => { - const click = panning && !panning.moved; - panning = null; - graph.classList.remove("panning"); - if (click) focus(null); -}; -graph.addEventListener("pointerup", panned); -graph.addEventListener("pointercancel", () => { if (panning) panning.moved = true; panned(); }); +graph.addEventListener("pointerup", () => { + const click = press && !press.moved ? press.click : null; + press = null; + graph.classList.remove("dragging"); + if (click) click(); +}); +graph.addEventListener("pointercancel", () => { + press = null; + graph.classList.remove("dragging"); +}); +// Beside the nodes: dragged, it pans; a click, back to this node. +graph.addEventListener("pointerdown", event => { + if (event.target !== graph) return; + const from = { ...view }; + pressing(event, (dx, dy) => { + view.x = from.x + dx; + view.y = from.y + dy; + moveView(); + }, () => focus(null)); +}); +// A node: dragged, it stays where it is put (`pinned`), and the layout places the others +// around it; a click, it is in focus. +function drag(event, id) { + const p = positions.get(id), from = { x: p.x, y: p.y }; + pressing(event, (dx, dy) => { + p.x = from.x + dx / view.k; + p.y = from.y + dy / view.k; + pinned.add(id); + renderGraph(); + }, () => focus(id)); +} +// A node let go: the layout places it again. +function unpin(id) { + if (!pinned.delete(id)) return; + lastShape = ""; + renderGraph(); +} // A double-click beside the nodes: the whole map again. graph.addEventListener("dblclick", event => { if (event.target !== graph) return; |
