aboutsummaryrefslogtreecommitdiffstats
path: root/src/api
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-09 01:46:48 +0000
committertv <tv@krebsco.de>2026-11-09 01:46:48 +0000
commit4e99a6e8ec65dbe8229333a3ec6dd03cd46991d7 (patch)
tree7182545420780012a96ea7d7f8e5971f2ad48238 /src/api
parentcb448b8c4c029868d7e3ad32676a533c5c015e58 (diff)
dashboard: move pull slider to link legend
Diffstat (limited to 'src/api')
-rw-r--r--src/api/dashboard/dashboard.html72
1 files changed, 43 insertions, 29 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index 09c7917..3789bb1 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -215,11 +215,6 @@
.map-overlay button, .map-overlay input, .map-overlay label { pointer-events: auto; }
/* How far behind the messages run: pointed at for its tooltip, which says why. */
.map-overlay #replay-delay { pointer-events: auto; cursor: help; }
- /* How hard links pull, beside the layout's toggle while dynamic, as tall. */
- .map-controls .slider { display: inline-flex; align-items: center; gap: 4px;
- font-size: 12px; color: var(--muted); height: 1.4em; }
- .map-controls .slider[hidden] { display: none; }
- .map-controls .slider input { width: 6em; height: 1em; margin: 0; }
/* Hides the legend, and shows it again. */
button.toggle { font: inherit; font-size: 12px; text-transform: none; letter-spacing: 0;
float: right; margin-left: 6px; padding: 0 6px; color: var(--muted); background: none;
@@ -336,9 +331,6 @@
<div class="map-area">
<div class="map-overlay">
<div class="map-controls">
- <label id="pull" class="slider" hidden
- title="How hard the links pull their ends together, against the nodes pushing each other apart">pull
- <input id="pull-slider" type="range" min="-2" max="2" step=".25"></label>
<button id="rtt-toggle" class="toggle">layout: static</button>
<button id="tree-toggle" class="toggle">tree: off</button>
<button id="legend-toggle" class="toggle">hide legend</button>
@@ -978,7 +970,42 @@ function renderLegend(measured) {
items.push(el("span", { class: "writer-tree" }, name(treeWriter()) +
"'s tree: the way its changes go (of the node in focus)"));
}
- document.getElementById("legend").replaceChildren(...items);
+ // Drawn anew only when it changed: the pull slider at its end is dragged meanwhile.
+ const legend = document.getElementById("legend");
+ const signature = items.map(item => item.className + ":" + item.textContent).join("|");
+ if (legend.dataset.signature !== signature) {
+ legend.dataset.signature = signature;
+ legend.replaceChildren(...items, pullControl());
+ }
+}
+// How hard the links pull their ends together, against the nodes pushing each other
+// apart, as the `pull` slider at the end of the links' legend says: 3 to its value
+// (a third to 3 times as hard; once, the middle, at first), kept in this browser. Harder,
+// the links are shorter: their length is what the map's size and members make of it
+// (`idealLength`) divided by how hard. Moved, the map moves to the new balance
+// smoothly (`settle`), static or dynamic.
+const PULL_MOST = 3;
+let pullStrength = 1;
+try { pullStrength = PULL_MOST ** (Number(localStorage.getItem("continuum-pull")) || 0); } catch (_) {}
+pullStrength = Math.min(PULL_MOST, Math.max(1 / PULL_MOST, pullStrength));
+let pullSlider = null;
+function pullControl() {
+ if (pullSlider === null) {
+ const slider = el("input", { type: "range", min: "-1", max: "1", step: ".05",
+ title: "How hard the links pull their ends together: to the right harder, the links" +
+ " shorter (the middle: as the map's size and members make them)" });
+ slider.value = String(Math.log(pullStrength) / Math.log(PULL_MOST));
+ slider.addEventListener("input", () => {
+ pullStrength = PULL_MOST ** Number(slider.value);
+ try { localStorage.setItem("continuum-pull", slider.value); } catch (_) {}
+ if (!state) return;
+ settleAnyway = true;
+ renderGraph();
+ settle();
+ });
+ pullSlider = el("label", { class: "slider" }, "pull", slider);
+ }
+ return pullSlider;
}
function edges() {
@@ -1018,17 +1045,12 @@ function edges() {
// How long a link is, ideally, for `count` nodes on a map of that size.
function idealLength(count, width, height) {
- return Math.min(width, height) / Math.max(2, Math.sqrt(count) * 1.5);
+ return Math.min(width, height) / Math.max(2, Math.sqrt(count) * 1.5) / pullStrength;
}
// `pairs`: [a, b, length, loose], the length the link pulls towards (else
// `idealLength`), and whether it is loose (`lengths`).
// How hard a loose link pulls, against a measured one.
const LOOSE = .1;
-// How hard links pull on a dynamic map, times what they do on a static one: 2 to the
-// `pull` slider's value (ΒΌ to 4; 1 at first), kept in this browser.
-let pullStrength = 1;
-try { pullStrength = 2 ** (Number(localStorage.getItem("continuum-pull")) || 0); } catch (_) {}
-pullStrength = Math.min(4, Math.max(.25, pullStrength));
// `only`, if given: the nodes laid out; the others stay where they are.
function layout(nodes, pairs, width, height, only) {
for (const id of nodes) {
@@ -1071,10 +1093,8 @@ function relax(nodes, pairs, width, height, gain, most, only, placing) {
if (!p || !q) continue;
const dx = q.x - p.x, dy = q.y - p.y, d = Math.max(Math.hypot(dx, dy), 1);
const stretch = d - (length || ideal);
- // A loose link pulls a little, and never pushes; on a dynamic map, as hard as the
- // slider says.
- const strength = byRtt ? pullStrength : 1;
- const pull = (loose ? Math.max(0, stretch) / d / 4 * LOOSE : stretch / d / 4) * strength;
+ // A loose link pulls a little, and never pushes.
+ const pull = loose ? Math.max(0, stretch) / d / 4 * LOOSE : stretch / d / 4;
force.get(a).x += dx * pull; force.get(a).y += dy * pull;
force.get(b).x -= dx * pull; force.get(b).y -= dy * pull;
}
@@ -1141,13 +1161,15 @@ 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 = "";
+// A static map moved to new lengths once (the `pull` slider), as a dynamic one is.
+let settleAnyway = false;
function settle() {
if (settling) return;
settling = true;
let frames = 0;
const step = () => {
// Static again: where they are now.
- if (!byRtt) { settling = false; return; }
+ if (!byRtt && !settleAnyway) { 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);
@@ -1155,6 +1177,7 @@ function settle() {
if (moved > .1 && ++frames < 600 && !document.hidden) requestAnimationFrame(step);
else {
settling = false;
+ settleAnyway = false;
// Hidden: on again with the next render.
if (document.hidden) lastLengths = "";
}
@@ -2204,19 +2227,10 @@ treeToggle.addEventListener("click", () => {
});
// Links as long as their round trips, and back.
const rttToggle = document.getElementById("rtt-toggle");
-const pullSlider = document.getElementById("pull-slider");
-pullSlider.value = String(Math.log2(pullStrength));
-pullSlider.addEventListener("input", () => {
- pullStrength = 2 ** Number(pullSlider.value);
- try { localStorage.setItem("continuum-pull", pullSlider.value); } catch (_) {}
- // Moved to the new balance, smoothly (`settle`).
- if (state && byRtt && settleTo) settle();
-});
function showRttToggle() {
rttToggle.textContent = byRtt ? "layout: dynamic" : "layout: static";
rttToggle.title = "Click for " + (byRtt ? "a static layout"
: "a dynamic one: links as long as their round trips");
- document.getElementById("pull").hidden = !byRtt;
}
showRttToggle();
rttToggle.addEventListener("click", () => {