diff options
Diffstat (limited to 'src/api/dashboard/dashboard.html')
| -rw-r--r-- | src/api/dashboard/dashboard.html | 72 |
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", () => { |
