aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/api/dashboard/dashboard.html49
1 files changed, 36 insertions, 13 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index ff55da4..7fb7ef8 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -471,8 +471,8 @@ function layout(nodes, pairs, width, height) {
// One round of the layout: every node moved by `gain` of the forces on it, at most
// `most` px. Returns how far the furthest moved. A gain of 1 overshoots where a node
// has several links (the pulls add up): it gets close fast, then swings about its
-// place; well below 1, it comes to rest.
-function relax(nodes, pairs, width, height, gain, most) {
+// place; well below 1, it comes to rest. `only`, if given: the nodes that move.
+function relax(nodes, pairs, width, height, gain, most, only) {
const ideal = idealLength(nodes.length, width, height);
{
const force = new Map(nodes.map(id => [id, { x: 0, y: 0 }]));
@@ -495,7 +495,7 @@ function relax(nodes, pairs, width, height, gain, most) {
}
let moved = 0;
for (const id of nodes) {
- if (pinned.has(id)) continue;
+ if (pinned.has(id) || (only && !only.has(id))) continue;
const p = positions.get(id), f = force.get(id);
const fx = f.x * gain, fy = f.y * gain;
const x = Math.min(width - 40, Math.max(40, p.x + Math.max(-most, Math.min(most, fx))));
@@ -530,20 +530,24 @@ function lengths(intended, measured, count, width, height) {
}
// What `settle` moves the map towards: the latest render's nodes and lengths.
let settling = false, settleTo = null, lastLengths = "";
+// Nodes let go (`unpin`) on a static map: the only ones `settle` moves there, to
+// their places among the others.
+const freed = new Set();
function settle() {
if (settling) return;
settling = true;
let frames = 0;
const step = () => {
- // Static again: where they are now.
- if (!byRtt) { settling = false; return; }
+ // Static again: where they are now (but those let go).
+ if (!byRtt && freed.size === 0) { settling = false; return; }
const { nodes, pairs, width, height } = settleTo;
// Damped: the swings of the layout's rounds die down instead of going on.
- const moved = relax(nodes, pairs, width, height, .15, 2);
+ const moved = relax(nodes, pairs, width, height, .15, 2, byRtt ? null : freed);
renderGraph();
if (moved > .1 && ++frames < 600 && !document.hidden) requestAnimationFrame(step);
else {
settling = false;
+ freed.clear();
// Hidden: on again with the next render.
if (document.hidden) lastLengths = "";
}
@@ -764,7 +768,6 @@ function renderGraph() {
}
const pressed = element => {
element.addEventListener("pointerdown", event => { event.stopPropagation(); drag(event, id); });
- element.addEventListener("dblclick", () => unpin(id));
hoverable(element, id);
};
// A clock off the swarm's: a dashed orange ring.
@@ -784,10 +787,14 @@ function renderGraph() {
(dormant ? " (dormant)" : "") +
(clock && clock.off ? " (clock " + seconds(clock.offsetMs) + " off the swarm's, ±" +
clock.withinMs + " ms)" : ""));
+ // Pinned (dragged there): a dot in the middle.
+ const pin = pinned.has(id) ? [drawing("pin " + id, "circle", { cx: p.x, cy: p.y, r: 2,
+ fill: member.self || reported ? "var(--panel)" : "var(--fg)",
+ "pointer-events": "none" })] : [];
const label = drawing("name " + id, "text", upright({ x: p.x + 10, y: p.y + 4,
"font-size": 12, fill: "var(--fg)", cursor: "pointer" }), pressed);
text(label, name(id));
- children.push(circle, label);
+ children.push(circle, ...pin, label);
}
show(document.getElementById("view"), children);
}
@@ -1299,7 +1306,11 @@ graph.addEventListener("pointerdown", event => {
}, () => 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.
+// around it; a click, it is in focus; a second one soon after, it's let go (`unpin`).
+// Told apart here: the map holds the pointer while pressed (`pressing`), so the
+// browser's clicks and double-clicks go to the map, not the node.
+let lastNodeClick = { id: null, at: 0 };
+const DOUBLE_CLICK_MS = 400;
function drag(event, id) {
const p = positions.get(id), from = { x: p.x, y: p.y };
pressing(event, (dx, dy) => {
@@ -1309,17 +1320,29 @@ function drag(event, id) {
p.y = from.y + y / view.k;
pinned.add(id);
renderGraph();
- }, () => focus(id));
+ }, () => {
+ const now = performance.now();
+ if (lastNodeClick.id === id && now - lastNodeClick.at < DOUBLE_CLICK_MS) {
+ lastNodeClick = { id: null, at: now };
+ unpin(id);
+ } else {
+ lastNodeClick = { id, at: now };
+ focus(id);
+ }
+ });
}
-// A node let go: the layout places it again.
+// A node let go: it moves to its place among the others, smoothly (`settle`); on a
+// static map, only it moves.
function unpin(id) {
if (!pinned.delete(id)) return;
- lastShape = "";
+ freed.add(id);
renderGraph();
+ settle();
}
// A double-click beside the nodes: the whole map again.
graph.addEventListener("dblclick", event => {
- if (event.target !== graph) return;
+ // Not one on a node (`drag`), which the browser gives the map.
+ if (event.target !== graph || performance.now() - lastNodeClick.at < DOUBLE_CLICK_MS) return;
Object.assign(view, { x: 0, y: 0, k: 1, turn: 0 });
moveView();
if (state) renderGraph();