aboutsummaryrefslogtreecommitdiffstats
path: root/src/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/api')
-rw-r--r--src/api/dashboard/dashboard.html92
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;