diff options
Diffstat (limited to 'src/api/dashboard/dashboard.html')
| -rw-r--r-- | src/api/dashboard/dashboard.html | 95 |
1 files changed, 68 insertions, 27 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index b586834..d872e9b 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -941,6 +941,10 @@ function renderStats() { const positions = new Map(); // Nodes put somewhere by hand (dragged): the layout leaves them there. const pinned = new Set(); +// The lines of the measured links as drawn (`renderGraph`), by pair key: the end whose +// report is drawn (`a`, the way its bows go) and how far each transport's line bows out +// at the middle (`sides`; the one in use 0, straight). +const routes = new Map(); const transportColour = new Map([["mycelium", "#2563eb"], ["libp2p", "#9333ea"], ["tor", "#0891b2"]]); const SPARE = ["#ca8a04", "#db2777", "#65a30d"]; @@ -1366,6 +1370,7 @@ function renderGraph() { tooltip(hit(key, p, q, 1), name(a) + " – " + name(b) + ": intended (member list), not measured"); } const ms = rtt => rtt < 10 ? rtt.toFixed(1) : String(Math.round(rtt)); + routes.clear(); for (const [key, { a, b, link, secsAgo }] of measured) { const p = positions.get(a), q = positions.get(b); if (!p || !q) continue; @@ -1399,6 +1404,10 @@ function renderGraph() { // far out); `aside(t)`, how far out at `t` along. const side = i === 0 ? 0 : (i % 2 ? 1 : -1) * Math.ceil(i / 2) * 6; const aside = t => 4 * t * (1 - t) * side; + // Where it bows, for the messages that go over it (`route`). + const route = routes.get(key) || { a, sides: new Map() }; + if (line.transport && !route.sides.has(line.transport)) route.sides.set(line.transport, side); + routes.set(key, route); const cx = (p.x + q.x) / 2 + nx * side * 2, cy = (p.y + q.y) / 2 + ny * side * 2; const attrs = { d: "M " + p.x + " " + p.y + " Q " + cx + " " + cy + " " + q.x + " " + q.y, fill: "none", stroke: colourOf(line.transport), @@ -2899,6 +2908,34 @@ graph.addEventListener("mouseleave", () => hover(null)); // (`FLASH_GLOW_MS`). Dots are kept above everything else on the map (`flows`, after // what `show` places), flashes over the lines but below their labels and the nodes. --- const FLOW_SPREAD_MS = 2000, FLOWS_AT_MOST = 6; +// The line from `from` to `to` a message went over, as drawn: over `transport`'s +// line, else (none known, or none drawn for it) the one in use. `at(t)`: the point `t` +// along (0 at `from`, 1 at `to`) and the direction there (a unit vector); `length`, +// about how long it is. Null while an end isn't on the map. +function route(from, to, transport) { + const p = positions.get(from), q = positions.get(to); + if (!p || !q) return null; + const drawnRoute = routes.get(pairKey(from, to)); + const known = drawnRoute && transport && drawnRoute.sides.get(transport); + // Bowing out to the left of the way from `a`: to the right of the other way. + const side = (known || 0) * (drawnRoute && drawnRoute.a !== from ? -1 : 1); + const chord = Math.max(Math.hypot(q.x - p.x, q.y - p.y), 1); + const nx = -(q.y - p.y) / chord, ny = (q.x - p.x) / chord; + // The quadratic curve the line is: its control point twice as far out as it bows. + const c = { x: (p.x + q.x) / 2 + nx * side * 2, y: (p.y + q.y) / 2 + ny * side * 2 }; + const length = (chord + Math.hypot(c.x - p.x, c.y - p.y) + Math.hypot(q.x - c.x, q.y - c.y)) / 2; + const at = t => { + const u = 1 - t; + const dx = 2 * u * (c.x - p.x) + 2 * t * (q.x - c.x); + const dy = 2 * u * (c.y - p.y) + 2 * t * (q.y - c.y); + const d = Math.max(Math.hypot(dx, dy), 1e-6); + return { x: u * u * p.x + 2 * u * t * c.x + t * t * q.x, + y: u * u * p.y + 2 * u * t * c.y + t * t * q.y, ux: dx / d, uy: dy / d }; + }; + return { length, at }; +} +// The transport an address is of (`mycelium://…` is Mycelium's), as the links name it. +const schemeOf = address => address ? address.split(":")[0] : null; // A flash's afterglow, after it arrived; and how long it takes over a link whose round // trip isn't known. const FLASH_GLOW_MS = 700, FLASH_UNKNOWN_MS = 50; @@ -3105,18 +3142,20 @@ function remoteMessagesArrived(id, entry, list) { const now = performance.now(); const links = new Map(); for (const message of kept) { - const key = message.from + " " + message.to; - const link = links.get(key) || { from: message.from, to: message.to, kinds: {}, total: 0 }; + const transport = schemeOf(message.address); + const key = message.from + " " + message.to + " " + (transport || ""); + const link = links.get(key) || + { from: message.from, to: message.to, transport, kinds: {}, total: 0 }; const kind = link.kinds[message.counted] || (link.kinds[message.counted] = { messages: 0, bytes: 0 }); kind.messages += 1; kind.bytes += message.bytes; link.total += 1; links.set(key, link); } - for (const { from, to, kinds, total } of links.values()) { + for (const { from, to, transport, kinds, total } of links.values()) { const shown = dotsFor(kinds, total); shown.forEach((dot, i) => { - flows.push({ from, to, start: now + i * FLOW_SPREAD_MS / shown.length, + flows.push({ from, to, transport, start: now + i * FLOW_SPREAD_MS / shown.length, kind: dot.kind, r: dotRadius(dot.bytes) }); }); } @@ -3368,47 +3407,49 @@ function showConsole() { function drawFlows() { flowFrame = null; const now = performance.now(); - // Where each is: as a dot, how far along its link (0 to 1) as long as the link is now; - // as a flash, its head and its afterglow's tail (each 0 to 1) and how bright. Arrived - // (a flash: its afterglow gone), or its link gone, it goes. + // Where each is, along the line it went over (`route`): as a dot, how far along (0 + // to 1) as long as the line is now; as a flash, its head and its afterglow's tail + // (each 0 to 1) and how bright. Arrived (a flash: its afterglow gone), or its link + // gone, it goes. const where = flow => { - const p = positions.get(flow.from), q = positions.get(flow.to); - if (!p || !q) return null; - const length = Math.max(Math.hypot(q.x - p.x, q.y - p.y), 1); + const line = route(flow.from, flow.to, flow.transport); + if (!line) return null; if (flowsMode === "flash") { const travel = flow.travel || (flow.travel = Math.max(1, oneWayMs(flow.from, flow.to))); const age = now - flow.start, glow = Math.max(0, (age - travel) / FLASH_GLOW_MS); - return { p, q, length, done: glow >= 1, head: Math.min(1, age / travel), + return { line, done: glow >= 1, head: Math.min(1, age / travel), tail: 1 - (1 - Math.min(1, glow)) ** 2, opacity: 1 - Math.min(1, glow) }; } - const t = (now - flow.start) * flowSpeed / length; - return { p, q, length, done: t >= 1, t }; + const t = (now - flow.start) * flowSpeed / line.length; + return { line, done: t >= 1, t }; }; for (let i = flows.length - 1; i >= 0; i--) { const at = where(flows[i]); if (at === null || at.done) flows.splice(i, 1); } const dots = [], streaks = []; + const f = n => n.toFixed(1); for (const flow of flows) { if (now < flow.start) continue; const at = where(flow); - const { p, q } = at; const r = flow.r * dotScale, colour = kindColour(flow.kind); if (flowsMode === "flash") { - // From the tail, a point, widening to the head, rounded: brightest where the - // message is, drawing back toward it as it fades. - const ux = (q.x - p.x) / at.length, uy = (q.y - p.y) / at.length; - const hx = p.x + (q.x - p.x) * at.head, hy = p.y + (q.y - p.y) * at.head; - const tx = p.x + (q.x - p.x) * Math.min(at.tail, at.head); - const ty = p.y + (q.y - p.y) * Math.min(at.tail, at.head); - const w = r * .8, nx = -uy * w, ny = ux * w; - const f = n => n.toFixed(1); - streaks.push({ colour, opacity: at.opacity, d: "M" + f(tx) + " " + f(ty) + - "L" + f(hx + nx) + " " + f(hy + ny) + - "A" + f(w) + " " + f(w) + " 0 0 0 " + f(hx - nx) + " " + f(hy - ny) + "Z" }); + // From the tail, a point, widening to the head, rounded, along the line: brightest + // where the message is, drawing back toward it as it fades. + const head = at.head, tail = Math.min(at.tail, head), w = r * .8, steps = 8; + const left = [], right = []; + for (let k = 0; k <= steps; k++) { + const s = tail + (head - tail) * k / steps, point = at.line.at(s); + const width = w * k / steps; + left.push(f(point.x - point.uy * width) + " " + f(point.y + point.ux * width)); + right.push(f(point.x + point.uy * width) + " " + f(point.y - point.ux * width)); + } + streaks.push({ colour, opacity: at.opacity, d: "M" + left.join("L") + + "A" + f(w) + " " + f(w) + " 0 0 0 " + right[steps] + "L" + + right.slice(0, steps).reverse().join("L") + "Z" }); } else { - dots.push({ x: p.x + (q.x - p.x) * at.t, y: p.y + (q.y - p.y) * at.t, r, colour, - opacity: Math.sin(Math.PI * at.t) }); + const point = at.line.at(at.t); + dots.push({ x: point.x, y: point.y, r, colour, opacity: Math.sin(Math.PI * at.t) }); } } const layer = (id, count, tag) => { |
