web view
This commit is contained in:
1 parent
a95d201284
commit
5586f606cd
6 files changed
+376
-102
No files matched your search
@@ -18,6 +18,7 @@
|
|||||||
const layers = {
|
const layers = {
|
||||||
spokes: document.getElementById("layer-spokes"),
|
spokes: document.getElementById("layer-spokes"),
|
||||||
regions: document.getElementById("layer-regions"),
|
regions: document.getElementById("layer-regions"),
|
||||||
|
ports: document.getElementById("layer-ports"),
|
||||||
ends: document.getElementById("layer-ends"),
|
ends: document.getElementById("layer-ends"),
|
||||||
legs: document.getElementById("layer-legs"),
|
legs: document.getElementById("layer-legs"),
|
||||||
nodes: document.getElementById("layer-nodes"),
|
nodes: document.getElementById("layer-nodes"),
|
||||||
@@ -93,6 +94,7 @@
|
|||||||
let focusKey = null;
|
let focusKey = null;
|
||||||
let gesture = C.initialGesture();
|
let gesture = C.initialGesture();
|
||||||
let drag = null;
|
let drag = null;
|
||||||
|
let lastPressed = null;
|
||||||
let tween = null;
|
let tween = null;
|
||||||
const nodeRecords = new Map();
|
const nodeRecords = new Map();
|
||||||
const regionRecords = new Map();
|
const regionRecords = new Map();
|
||||||
@@ -165,6 +167,31 @@
|
|||||||
|
|
||||||
/* ------------------------------------------------------------ drawing: map */
|
/* ------------------------------------------------------------ drawing: map */
|
||||||
|
|
||||||
|
/* One bar of hardware: "CPU 9% · 6C/12T" over a track with a fill. Nodes and networks both use it. */
|
||||||
|
function statBar(parent, width) {
|
||||||
|
const label = svg("text", { class: "st-text" });
|
||||||
|
const name = svg("tspan", { class: "st-name" });
|
||||||
|
const value = svg("tspan", { class: "st-value" });
|
||||||
|
label.appendChild(name);
|
||||||
|
label.appendChild(value);
|
||||||
|
const track = svg("rect", { class: "st-track", height: 5, rx: 2.5 });
|
||||||
|
const fill = svg("rect", { class: "st-fill", height: 5, rx: 2.5 });
|
||||||
|
if (width) put(track, "width", width);
|
||||||
|
[label, track, fill].forEach((part) => parent.appendChild(part));
|
||||||
|
return { label: label, name: name, value: value, track: track, fill: fill };
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintBar(bar, name, usage, width) {
|
||||||
|
putText(bar.name, name + " ");
|
||||||
|
putText(bar.value, usage ? usage.text : "n/a");
|
||||||
|
put(bar.fill, "width", usage && usage.pct ? Math.max((width * usage.pct) / 100, 4) : 0);
|
||||||
|
putClass(bar.fill, "st-fill " + (usage && usage.pct !== null ? C.barLevel(usage.pct) : "ok"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const FOOT_PAD = 14;
|
||||||
|
const FOOT_GAP = 8;
|
||||||
|
const footColumn = (region) => (region.w - 2 * FOOT_PAD - (C.STAT_COUNT - 1) * FOOT_GAP) / C.STAT_COUNT;
|
||||||
|
|
||||||
function regionRecord(region) {
|
function regionRecord(region) {
|
||||||
let record = regionRecords.get(region.id);
|
let record = regionRecords.get(region.id);
|
||||||
if (record) return record;
|
if (record) return record;
|
||||||
@@ -178,8 +205,17 @@
|
|||||||
title.appendChild(count);
|
title.appendChild(count);
|
||||||
[box, grab, dot, title].forEach((part) => group.appendChild(part));
|
[box, grab, dot, title].forEach((part) => group.appendChild(part));
|
||||||
[box, grab].forEach((part) => (part.dataset.net = region.id));
|
[box, grab].forEach((part) => (part.dataset.net = region.id));
|
||||||
|
|
||||||
|
/* The network's hardware added up: the same three bars a node has, across the bottom. */
|
||||||
|
const foot = svg("g", { class: "stats foot" });
|
||||||
|
const rule = svg("line", { class: "foot-rule" });
|
||||||
|
const columns = Array.from({ length: C.STAT_COUNT }, () => statBar(foot));
|
||||||
|
const note = svg("text", { class: "st-foot" });
|
||||||
|
const hint = svg("title");
|
||||||
|
[rule, note, hint].forEach((part) => foot.appendChild(part));
|
||||||
|
group.appendChild(foot);
|
||||||
layers.regions.appendChild(group);
|
layers.regions.appendChild(group);
|
||||||
record = { group: group, box: box, grab: grab, title: title, count: count };
|
record = { group: group, box: box, grab: grab, title: title, count: count, rule: rule, columns: columns, note: note, hint: hint };
|
||||||
regionRecords.set(region.id, record);
|
regionRecords.set(region.id, record);
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
@@ -192,6 +228,28 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Three columns across the bottom band of a network. */
|
||||||
|
function placeFooter(record, region) {
|
||||||
|
const top = region.footer.y - region.y;
|
||||||
|
const column = footColumn(region);
|
||||||
|
put(record.rule, "x1", FOOT_PAD);
|
||||||
|
put(record.rule, "x2", region.w - FOOT_PAD);
|
||||||
|
put(record.rule, "y1", top + 2);
|
||||||
|
put(record.rule, "y2", top + 2);
|
||||||
|
record.columns.forEach((col, index) => {
|
||||||
|
const x = FOOT_PAD + index * (column + FOOT_GAP);
|
||||||
|
put(col.label, "x", x);
|
||||||
|
put(col.label, "y", top + 20);
|
||||||
|
put(col.track, "x", x);
|
||||||
|
put(col.track, "y", top + 26);
|
||||||
|
put(col.track, "width", column);
|
||||||
|
put(col.fill, "x", x);
|
||||||
|
put(col.fill, "y", top + 26);
|
||||||
|
});
|
||||||
|
put(record.note, "x", FOOT_PAD);
|
||||||
|
put(record.note, "y", top + 50);
|
||||||
|
}
|
||||||
|
|
||||||
function drawRegions() {
|
function drawRegions() {
|
||||||
const present = new Set();
|
const present = new Set();
|
||||||
map.regions.forEach((region) => {
|
map.regions.forEach((region) => {
|
||||||
@@ -201,6 +259,7 @@
|
|||||||
put(record.box, "width", region.w);
|
put(record.box, "width", region.w);
|
||||||
put(record.box, "height", region.h);
|
put(record.box, "height", region.h);
|
||||||
put(record.grab, "width", region.w);
|
put(record.grab, "width", region.w);
|
||||||
|
placeFooter(record, region);
|
||||||
const pinnedNet = selectedNet();
|
const pinnedNet = selectedNet();
|
||||||
const state = pinnedNet ? (pinnedNet === region.id ? " selected" : " dim") : "";
|
const state = pinnedNet ? (pinnedNet === region.id ? " selected" : " dim") : "";
|
||||||
putClass(record.group, "region n" + region.color + state + (region.moved ? " moved" : ""));
|
putClass(record.group, "region n" + region.color + state + (region.moved ? " moved" : ""));
|
||||||
@@ -219,8 +278,16 @@
|
|||||||
const dot = svg("circle", { class: "dot" });
|
const dot = svg("circle", { class: "dot" });
|
||||||
const texts = { name: svg("text", { class: "l-name" }), roles: [svg("text", { class: "l-roles" }), svg("text", { class: "l-roles" })], caption: svg("text", { class: "l-caption" }) };
|
const texts = { name: svg("text", { class: "l-name" }), roles: [svg("text", { class: "l-roles" }), svg("text", { class: "l-roles" })], caption: svg("text", { class: "l-caption" }) };
|
||||||
[title, halo, shape, dot, texts.name, texts.roles[0], texts.roles[1], texts.caption].forEach((part) => group.appendChild(part));
|
[title, halo, shape, dot, texts.name, texts.roles[0], texts.roles[1], texts.caption].forEach((part) => group.appendChild(part));
|
||||||
|
|
||||||
|
/* Hardware: three bars with their numbers, and a footer. */
|
||||||
|
const stats = svg("g", { class: "stats" });
|
||||||
|
const rows = Array.from({ length: C.STAT_COUNT }, () => statBar(stats, C.STAT_W));
|
||||||
|
const footer = svg("text", { class: "st-foot" });
|
||||||
|
stats.appendChild(footer);
|
||||||
|
group.appendChild(stats);
|
||||||
|
|
||||||
layers.nodes.appendChild(group);
|
layers.nodes.appendChild(group);
|
||||||
return { group: group, title: title, halo: halo, shape: shape, dot: dot, texts: texts, gateway: node.gateway };
|
return { group: group, title: title, halo: halo, shape: shape, dot: dot, texts: texts, stats: stats, rows: rows, footer: footer, gateway: node.gateway };
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawNodes() {
|
function drawNodes() {
|
||||||
@@ -253,27 +320,69 @@
|
|||||||
put(record.halo, "r", r + 6);
|
put(record.halo, "r", r + 6);
|
||||||
put(record.dot, "r", node.gateway ? 5.5 : 4.5);
|
put(record.dot, "r", node.gateway ? 5.5 : 4.5);
|
||||||
|
|
||||||
/* Labels sit in the box the layout reserved for them. */
|
/* Every node, gateway or not, is the same stack: name, roles, caption, then its stats. */
|
||||||
const lines = C.wrapLabels((node.gateway ? ["gateway"] : []).concat(C.specialRoles(model, info)), 17, 2);
|
const lines = C.wrapLabels((node.gateway ? ["gateway"] : []).concat(C.specialRoles(model, info)), 17, 2);
|
||||||
const side = node.dock ? node.dock.side : null;
|
|
||||||
const anchor = side === "right" ? "end" : side ? "start" : "middle";
|
|
||||||
const x = side === "right" ? node.box.x + node.box.w - node.x : side ? node.box.x - node.x : 0;
|
|
||||||
const first = node.box.y - node.y + 11;
|
const first = node.box.y - node.y + 11;
|
||||||
const place = (element, index, text) => {
|
const place = (element, index, text) => {
|
||||||
put(element, "x", x);
|
put(element, "x", 0);
|
||||||
put(element, "y", first + index * 13);
|
put(element, "y", first + index * 13);
|
||||||
put(element, "text-anchor", anchor);
|
put(element, "text-anchor", "middle");
|
||||||
putText(element, text);
|
putText(element, text);
|
||||||
};
|
};
|
||||||
place(record.texts.name, 0, C.shorten(info.hostname, 15));
|
place(record.texts.name, 0, C.shorten(info.hostname, 15));
|
||||||
record.texts.roles.forEach((element, index) => place(element, 1 + index, lines[index] || ""));
|
record.texts.roles.forEach((element, index) => place(element, 1 + index, lines[index] || ""));
|
||||||
put(record.texts.caption, "x", x);
|
put(record.texts.caption, "x", 0);
|
||||||
put(record.texts.caption, "y", first + (1 + lines.length) * 13);
|
put(record.texts.caption, "y", first + (1 + lines.length) * 13);
|
||||||
put(record.texts.caption, "text-anchor", anchor);
|
put(record.texts.caption, "text-anchor", "middle");
|
||||||
|
|
||||||
|
put(record.stats, "transform", "translate(" + -C.STAT_W / 2 + "," + (node.box.y - node.y + C.STAT_TOP) + ")");
|
||||||
|
record.rows.forEach((row, index) => {
|
||||||
|
put(row.label, "y", index * C.STAT_ROW + 10);
|
||||||
|
put(row.track, "y", index * C.STAT_ROW + 14);
|
||||||
|
put(row.fill, "y", index * C.STAT_ROW + 14);
|
||||||
|
});
|
||||||
|
put(record.footer, "y", C.STAT_COUNT * C.STAT_ROW + 10);
|
||||||
});
|
});
|
||||||
prune(nodeRecords, present);
|
prune(nodeRecords, present);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The tooltip and the screen reader's text carry the numbers too, so they follow them. */
|
||||||
|
function describe(record, info) {
|
||||||
|
const text = C.describeNode(model, info, routes);
|
||||||
|
putText(record.title, text);
|
||||||
|
put(record.group, "aria-label", text.replace(/\n/g, ". ") + ". Show routes from this node.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The numbers move every second and never change the layout, so they are written on their own. */
|
||||||
|
function drawStats() {
|
||||||
|
nodeRecords.forEach((record, key) => {
|
||||||
|
const info = model.byKey.get(key);
|
||||||
|
if (!info) return;
|
||||||
|
if (routes) describe(record, info);
|
||||||
|
const stats = C.describeStats(info.stats);
|
||||||
|
stats.rows.forEach(([name, usage], index) => paintBar(record.rows[index], name, usage, C.STAT_W));
|
||||||
|
putText(record.footer, stats.footer);
|
||||||
|
});
|
||||||
|
|
||||||
|
map.regions.forEach((region) => {
|
||||||
|
const record = regionRecords.get(region.id);
|
||||||
|
const totals = C.networkStats(model, region.id);
|
||||||
|
totals.rows.forEach(([name, usage], index) => paintBar(record.columns[index], name, usage, footColumn(region)));
|
||||||
|
putText(record.note, totals.nodes ? "total of " + C.plural(totals.nodes, "node") + " on " + C.plural(totals.machines, "machine") : totals.footer);
|
||||||
|
putText(record.hint, "Cumulative hardware of " + region.id + ": " + C.describeRows(totals.rows).join(", ") + ". Nodes that report the same totals are counted as one machine.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A gateway's door on the edge of its network, joined to the gateway node inside. */
|
||||||
|
function drawPorts() {
|
||||||
|
layers.ports.replaceChildren();
|
||||||
|
map.nodes.filter((node) => node.gateway).forEach((node) => {
|
||||||
|
const cls = "n" + node.color;
|
||||||
|
layers.ports.appendChild(svg("line", { class: "port-link " + cls, x1: node.port.x, y1: node.port.y, x2: node.x, y2: node.y }));
|
||||||
|
layers.ports.appendChild(svg("rect", { class: "port " + cls, x: node.port.x - 9, y: node.port.y - 9, width: 18, height: 18, rx: 5 }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function drawSpokes() {
|
function drawSpokes() {
|
||||||
layers.spokes.replaceChildren();
|
layers.spokes.replaceChildren();
|
||||||
spokeRecords = map.spokes.map((spoke) => {
|
spokeRecords = map.spokes.map((spoke) => {
|
||||||
@@ -319,9 +428,7 @@
|
|||||||
putClass(record.group, classes.join(" "));
|
putClass(record.group, classes.join(" "));
|
||||||
|
|
||||||
putText(record.texts.caption, trace.captions.get(key) || "");
|
putText(record.texts.caption, trace.captions.get(key) || "");
|
||||||
const text = C.describeNode(model, info, routes);
|
describe(record, info);
|
||||||
putText(record.title, text);
|
|
||||||
put(record.group, "aria-label", text.replace(/\n/g, ". ") + ". Show routes from this node.");
|
|
||||||
put(record.group, "aria-pressed", isPinned(key) ? "true" : "false");
|
put(record.group, "aria-pressed", isPinned(key) ? "true" : "false");
|
||||||
put(record.group, "tabindex", key === focusKey ? "0" : "-1");
|
put(record.group, "tabindex", key === focusKey ? "0" : "-1");
|
||||||
});
|
});
|
||||||
@@ -371,14 +478,16 @@
|
|||||||
function recompute(now) {
|
function recompute(now) {
|
||||||
map = C.computeMap(model, layoutState, overrides, now);
|
map = C.computeMap(model, layoutState, overrides, now);
|
||||||
lastCompute = now;
|
lastCompute = now;
|
||||||
mapSignature = model.signature;
|
|
||||||
drawRegions();
|
drawRegions();
|
||||||
drawNodes();
|
drawNodes();
|
||||||
drawSpokes();
|
drawSpokes();
|
||||||
|
drawPorts();
|
||||||
if (autoFit) fitAll(false);
|
if (autoFit) fitAll(false);
|
||||||
else setView(view, false);
|
else setView(view, false);
|
||||||
drawTrace();
|
drawTrace();
|
||||||
|
drawStats();
|
||||||
updateControls();
|
updateControls();
|
||||||
|
mapSignature = model.signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
function update(payload) {
|
function update(payload) {
|
||||||
@@ -390,10 +499,13 @@
|
|||||||
if (hovered && !model.byKey.has(hovered)) hovered = null;
|
if (hovered && !model.byKey.has(hovered)) hovered = null;
|
||||||
|
|
||||||
drawHeader();
|
drawHeader();
|
||||||
|
C.noteGateways(model, layoutState, now);
|
||||||
const ghosts = layoutState.gateways.size > model.bridges.length;
|
const ghosts = layoutState.gateways.size > model.bridges.length;
|
||||||
if (model.signature !== mapSignature || (ghosts && now - lastCompute > 5000)) {
|
if (model.signature !== mapSignature || (ghosts && now - lastCompute > 5000)) {
|
||||||
recompute(now);
|
recompute(now);
|
||||||
drawReaderList();
|
drawReaderList();
|
||||||
|
} else {
|
||||||
|
drawStats();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,6 +523,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function zoomStep(factor) {
|
function zoomStep(factor) {
|
||||||
|
cancelTween();
|
||||||
setView(C.zoomAt(view, size.w / 2, size.h / 2, factor, map && map.bounds, size), true);
|
setView(C.zoomAt(view, size.w / 2, size.h / 2, factor, map && map.bounds, size), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,7 +531,9 @@
|
|||||||
document.getElementById("zoom-out").addEventListener("click", () => zoomStep(0.8));
|
document.getElementById("zoom-out").addEventListener("click", () => zoomStep(0.8));
|
||||||
document.getElementById("fit").addEventListener("click", () => fitAll(true));
|
document.getElementById("fit").addEventListener("click", () => fitAll(true));
|
||||||
resetButton.addEventListener("click", () => {
|
resetButton.addEventListener("click", () => {
|
||||||
|
const legendWas = overrides.legend;
|
||||||
overrides = C.newOverrides();
|
overrides = C.newOverrides();
|
||||||
|
overrides.legend = legendWas;
|
||||||
layoutState = C.newState();
|
layoutState = C.newState();
|
||||||
store.save(overrides);
|
store.save(overrides);
|
||||||
recompute(Date.now());
|
recompute(Date.now());
|
||||||
@@ -466,7 +581,13 @@
|
|||||||
const same = (a, b) => Boolean(a && b && a.kind === b.kind && a.id === b.id);
|
const same = (a, b) => Boolean(a && b && a.kind === b.kind && a.id === b.id);
|
||||||
const toggle = (next) => select(same(selection, next) ? null : next);
|
const toggle = (next) => select(same(selection, next) ? null : next);
|
||||||
|
|
||||||
|
function cancelTween() {
|
||||||
|
if (tween) cancelAnimationFrame(tween.frame);
|
||||||
|
tween = null;
|
||||||
|
}
|
||||||
|
|
||||||
function perform(action) {
|
function perform(action) {
|
||||||
|
if (["pan", "zoom", "drag-start"].includes(action.type)) cancelTween();
|
||||||
if (action.type === "pan") setView(C.panBy(view, action.dx, action.dy), true);
|
if (action.type === "pan") setView(C.panBy(view, action.dx, action.dy), true);
|
||||||
else if (action.type === "zoom") setView(C.zoomAt(view, action.x, action.y, action.factor, map && map.bounds, size), true);
|
else if (action.type === "zoom") setView(C.zoomAt(view, action.x, action.y, action.factor, map && map.bounds, size), true);
|
||||||
else if (action.type === "fit") {
|
else if (action.type === "fit") {
|
||||||
@@ -489,9 +610,12 @@
|
|||||||
if (target.kind === "region-title") {
|
if (target.kind === "region-title") {
|
||||||
const region = map.regionAt.get(target.key);
|
const region = map.regionAt.get(target.key);
|
||||||
drag = { kind: "region", net: target.key, x: region.x, y: region.y, before: overrides.regions[target.key] };
|
drag = { kind: "region", net: target.key, x: region.x, y: region.y, before: overrides.regions[target.key] };
|
||||||
|
autoFit = false;
|
||||||
|
updateControls();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const node = map.nodeAt.get(target.key);
|
const node = map.nodeAt.get(target.key);
|
||||||
|
if (!node) return;
|
||||||
drag = { kind: target.kind, key: target.key, net: node.net, x: node.x, y: node.y, from: node.cell };
|
drag = { kind: target.kind, key: target.key, net: node.net, x: node.x, y: node.y, from: node.cell };
|
||||||
nodeRecords.get(target.key).group.classList.add("dragging");
|
nodeRecords.get(target.key).group.classList.add("dragging");
|
||||||
}
|
}
|
||||||
@@ -508,7 +632,7 @@
|
|||||||
dragFrame = null;
|
dragFrame = null;
|
||||||
if (drag) recompute(Date.now());
|
if (drag) recompute(Date.now());
|
||||||
});
|
});
|
||||||
} else {
|
} else if (nodeRecords.has(drag.key)) {
|
||||||
nodeRecords.get(drag.key).group.style.transform = "translate(" + drag.x + "px," + drag.y + "px)";
|
nodeRecords.get(drag.key).group.style.transform = "translate(" + drag.x + "px," + drag.y + "px)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -519,7 +643,8 @@
|
|||||||
drag = null;
|
drag = null;
|
||||||
if (dragFrame) cancelAnimationFrame(dragFrame);
|
if (dragFrame) cancelAnimationFrame(dragFrame);
|
||||||
dragFrame = null;
|
dragFrame = null;
|
||||||
if (done.kind !== "region") nodeRecords.get(done.key).group.classList.remove("dragging");
|
if (done.kind !== "region" && nodeRecords.has(done.key)) nodeRecords.get(done.key).group.classList.remove("dragging");
|
||||||
|
if (done.kind !== "region" && !map.nodeAt.has(done.key)) return;
|
||||||
|
|
||||||
if (!keep) {
|
if (!keep) {
|
||||||
if (done.kind === "region") {
|
if (done.kind === "region") {
|
||||||
@@ -534,7 +659,7 @@
|
|||||||
if (other) C.setCell(overrides, done.net, other.key, done.from);
|
if (other) C.setCell(overrides, done.net, other.key, done.from);
|
||||||
}
|
}
|
||||||
} else if (done.kind === "gateway") {
|
} else if (done.kind === "gateway") {
|
||||||
C.setDock(overrides, done.key, C.dockAt(map, done.net, done.x, done.y));
|
C.setDock(overrides, done.key, C.dockAt(map, done.net, done.x, done.y, map.nodeAt.get(done.key).dock.side));
|
||||||
}
|
}
|
||||||
saveSoon();
|
saveSoon();
|
||||||
recompute(Date.now());
|
recompute(Date.now());
|
||||||
@@ -548,8 +673,8 @@
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
/* a synthetic event has no capturable pointer */
|
/* a synthetic event has no capturable pointer */
|
||||||
}
|
}
|
||||||
const selected = selection && selection.kind === "node" ? selection.id : null;
|
lastPressed = targetOf(event.target);
|
||||||
const result = C.reduceGesture(gesture, { type: "down", id: event.pointerId, x: point.x, y: point.y, pointerType: event.pointerType, target: targetOf(event.target), selected: selected });
|
const result = C.reduceGesture(gesture, { type: "down", id: event.pointerId, x: point.x, y: point.y, pointerType: event.pointerType, target: lastPressed, selected: selection ? selection.id : null });
|
||||||
gesture = result.state;
|
gesture = result.state;
|
||||||
result.actions.forEach(perform);
|
result.actions.forEach(perform);
|
||||||
});
|
});
|
||||||
@@ -578,7 +703,8 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
svgRoot.addEventListener("pointerleave", (event) => {
|
svgRoot.addEventListener("pointerleave", (event) => {
|
||||||
if (event.pointerType === "mouse" && hovered && gesture.mode === "idle" && model) {
|
const focused = nodeFromElement(document.activeElement);
|
||||||
|
if (event.pointerType === "mouse" && hovered && gesture.mode === "idle" && model && !(focused && focused.dataset.key === hovered)) {
|
||||||
hovered = null;
|
hovered = null;
|
||||||
drawTrace();
|
drawTrace();
|
||||||
}
|
}
|
||||||
@@ -590,7 +716,8 @@
|
|||||||
result.actions.forEach(perform);
|
result.actions.forEach(perform);
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
svgRoot.addEventListener("dblclick", (event) => {
|
svgRoot.addEventListener("dblclick", (event) => {
|
||||||
C.reduceGesture(gesture, { type: "dblclick", target: targetOf(event.target) }).actions.forEach(perform);
|
/* With the pointer captured, the event's own target is the svg: use what was pressed. */
|
||||||
|
C.reduceGesture(gesture, { type: "dblclick", target: lastPressed || targetOf(event.target) }).actions.forEach(perform);
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ----------------------------------------------------------------- keyboard */
|
/* ----------------------------------------------------------------- keyboard */
|
||||||
@@ -642,7 +769,7 @@
|
|||||||
const region = map.regionAt.get(node.net);
|
const region = map.regionAt.get(node.net);
|
||||||
const col = node.col + dx;
|
const col = node.col + dx;
|
||||||
const row = node.row + dy;
|
const row = node.row + dy;
|
||||||
if (col < 0 || col >= region.cols || row < 0) return;
|
if (col < 0 || col >= region.cols || row < 0 || row > region.rows) return;
|
||||||
const cell = row * region.cols + col;
|
const cell = row * region.cols + col;
|
||||||
const other = map.nodes.find((entry) => entry.net === node.net && entry.cell === cell);
|
const other = map.nodes.find((entry) => entry.net === node.net && entry.cell === cell);
|
||||||
C.setCell(overrides, node.net, node.key, cell);
|
C.setCell(overrides, node.net, node.key, cell);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
<g id="world">
|
<g id="world">
|
||||||
<g id="layer-spokes"></g>
|
<g id="layer-spokes"></g>
|
||||||
<g id="layer-regions"></g>
|
<g id="layer-regions"></g>
|
||||||
|
<g id="layer-ports"></g>
|
||||||
<g id="layer-ends"></g>
|
<g id="layer-ends"></g>
|
||||||
<g id="layer-legs"></g>
|
<g id="layer-legs"></g>
|
||||||
<g id="layer-nodes"></g>
|
<g id="layer-nodes"></g>
|
||||||
|
|||||||
@@ -7,30 +7,34 @@
|
|||||||
(function () {
|
(function () {
|
||||||
const M = typeof require !== "undefined" ? require("./model.js") : globalThis.Colony;
|
const M = typeof require !== "undefined" ? require("./model.js") : globalThis.Colony;
|
||||||
|
|
||||||
const CELL_W = 120;
|
const CELL_W = 168;
|
||||||
const CELL_H = 104;
|
const CELL_H = 196;
|
||||||
const GRID_INSET = 30;
|
const GRID_INSET = 30;
|
||||||
const HEAD = 38;
|
const HEAD = 38;
|
||||||
const REGION_PAD = 14;
|
const REGION_PAD = 14;
|
||||||
const GAP = 56;
|
const GAP = 56;
|
||||||
const MIN_WIDTH = 240;
|
const MIN_WIDTH = 420;
|
||||||
const NODE_R = 14;
|
const NODE_R = 14;
|
||||||
const GATEWAY_R = 19;
|
const GATEWAY_R = 19;
|
||||||
const DOCK_LANE = 136;
|
/* The band at the bottom of every network for its cumulative hardware. */
|
||||||
const DOCK_ROW = 64;
|
const FOOTER = 58;
|
||||||
const DOCK_PITCH = 152;
|
const RING_GAP = 4;
|
||||||
const RESERVE_TOP = 32;
|
|
||||||
const RESERVE_BOTTOM = 70;
|
|
||||||
const NOMINAL = { w: 320, h: 240 };
|
const NOMINAL = { w: 320, h: 240 };
|
||||||
const GATEWAY_HOLD_MS = 60000;
|
const GATEWAY_HOLD_MS = 60000;
|
||||||
const MAX_OVERRIDES = 300;
|
const MAX_OVERRIDES = 300;
|
||||||
const LABEL_W = 104;
|
/* The box every node owns for its text: name, roles and route caption (four lines), then its stats. */
|
||||||
const LABEL_H = 56;
|
const LINE = 13;
|
||||||
|
const BOX_W = 160;
|
||||||
|
const STAT_W = 148;
|
||||||
|
const STAT_COUNT = 3;
|
||||||
|
const STAT_ROW = 24;
|
||||||
|
const STAT_TOP = 4 * LINE + 4;
|
||||||
|
const BOX_H = STAT_TOP + STAT_COUNT * STAT_ROW + 14;
|
||||||
|
|
||||||
/* ---------------------------------------------------------------- state */
|
/* ---------------------------------------------------------------- state */
|
||||||
|
|
||||||
function newState() {
|
function newState() {
|
||||||
return { regions: new Map(), sizes: new Map(), cols: new Map(), cells: new Map(), sides: new Map(), colors: new Map(), gateways: new Map(), nextColor: 0, placeLater: new Set() };
|
return { regions: new Map(), sizes: new Map(), cols: new Map(), cells: new Map(), sides: new Map(), colors: new Map(), age: new Map(), colsFor: new Map(), gateways: new Map(), nextColor: 0, nextAge: 0, placeLater: new Set() };
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Only what the user changed by hand is ever stored. */
|
/* Only what the user changed by hand is ever stored. */
|
||||||
@@ -65,7 +69,7 @@
|
|||||||
if (Object.keys(kept).length) clean.slots[net] = kept;
|
if (Object.keys(kept).length) clean.slots[net] = kept;
|
||||||
});
|
});
|
||||||
Object.entries(table(data.docks)).forEach(([key, dock]) => {
|
Object.entries(table(data.docks)).forEach(([key, dock]) => {
|
||||||
if (dock && ["left", "right", "top", "bottom"].includes(dock.side) && finite(dock.t)) clean.docks[key] = { side: dock.side, t: dock.t };
|
if (dock && ["left", "right", "top"].includes(dock.side) && finite(dock.t)) clean.docks[key] = { side: dock.side, t: dock.t };
|
||||||
});
|
});
|
||||||
if (typeof data.legend === "boolean") clean.legend = data.legend;
|
if (typeof data.legend === "boolean") clean.legend = data.legend;
|
||||||
return limitOverrides(clean);
|
return limitOverrides(clean);
|
||||||
@@ -172,7 +176,7 @@
|
|||||||
|
|
||||||
/* Push apart regions that overlap or sit closer than the gap: the one the user
|
/* Push apart regions that overlap or sit closer than the gap: the one the user
|
||||||
did not place, along the axis with the smaller overlap. */
|
did not place, along the axis with the smaller overlap. */
|
||||||
function resolveOverlaps(rects, fixed, gap) {
|
function resolveOverlaps(rects, fixed, gap, age) {
|
||||||
const ids = Object.keys(rects).sort();
|
const ids = Object.keys(rects).sort();
|
||||||
for (let pass = 0; pass < 40; pass++) {
|
for (let pass = 0; pass < 40; pass++) {
|
||||||
let moved = false;
|
let moved = false;
|
||||||
@@ -182,8 +186,10 @@
|
|||||||
const b = rects[ids[j]];
|
const b = rects[ids[j]];
|
||||||
const o = overlapAmount(a, b, gap);
|
const o = overlapAmount(a, b, gap);
|
||||||
if (o.x <= 0 || o.y <= 0) continue;
|
if (o.x <= 0 || o.y <= 0) continue;
|
||||||
const mover = fixed.has(ids[j]) ? (fixed.has(ids[i]) ? null : ids[i]) : ids[j];
|
/* A network the user placed stays and the other gives way. When both were placed by hand, or neither was, the newer one moves: two hand-placed networks are never left overlapping, so its drawn spot can differ from the saved one. */
|
||||||
if (!mover) continue;
|
let mover;
|
||||||
|
if (fixed.has(ids[i]) !== fixed.has(ids[j])) mover = fixed.has(ids[i]) ? ids[j] : ids[i];
|
||||||
|
else mover = age.get(ids[i]) > age.get(ids[j]) ? ids[i] : ids[j];
|
||||||
const other = mover === ids[j] ? a : b;
|
const other = mover === ids[j] ? a : b;
|
||||||
const self = rects[mover];
|
const self = rects[mover];
|
||||||
if (o.x <= o.y) self.x += self.x + self.w / 2 >= other.x + other.w / 2 ? o.x : -o.x;
|
if (o.x <= o.y) self.x += self.x + self.w / 2 >= other.x + other.w / 2 ? o.x : -o.x;
|
||||||
@@ -203,8 +209,9 @@
|
|||||||
function sideToward(from, to) {
|
function sideToward(from, to) {
|
||||||
const dx = to.x - from.x;
|
const dx = to.x - from.x;
|
||||||
const dy = to.y - from.y;
|
const dy = to.y - from.y;
|
||||||
if (Math.abs(dx) > Math.abs(dy)) return dx > 0 ? "right" : "left";
|
/* Only a neighbour above and more up than across is on top; there is no bottom edge to dock on. */
|
||||||
return dy > 0 ? "bottom" : "top";
|
if (dy <= 0 && Math.abs(dy) >= Math.abs(dx)) return "top";
|
||||||
|
return dx >= 0 ? "right" : "left";
|
||||||
}
|
}
|
||||||
|
|
||||||
const normals = { left: [-1, 0], right: [1, 0], top: [0, -1], bottom: [0, 1] };
|
const normals = { left: [-1, 0], right: [1, 0], top: [0, -1], bottom: [0, 1] };
|
||||||
@@ -261,9 +268,16 @@
|
|||||||
|
|
||||||
const clamp = (value, low, high) => Math.max(low, Math.min(high, value));
|
const clamp = (value, low, high) => Math.max(low, Math.min(high, value));
|
||||||
|
|
||||||
|
/* Remember, on every payload and not only when the map is laid out, who is relaying: that is
|
||||||
|
what the minute of grace for a gateway that went quiet is measured from. */
|
||||||
|
function noteGateways(model, state, now) {
|
||||||
|
model.bridges.forEach((bridge) => state.gateways.set(bridge.key, { time: now, bridge: bridge }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The page also calls noteGateways on every payload; repeating it here covers a fresh state (reset, drag). */
|
||||||
function withGhosts(model, state, now) {
|
function withGhosts(model, state, now) {
|
||||||
const bridges = model.bridges.slice();
|
const bridges = model.bridges.slice();
|
||||||
model.bridges.forEach((bridge) => state.gateways.set(bridge.key, { time: now, bridge: bridge }));
|
noteGateways(model, state, now);
|
||||||
state.gateways.forEach((entry, key) => {
|
state.gateways.forEach((entry, key) => {
|
||||||
if (now - entry.time > GATEWAY_HOLD_MS || !model.byKey.has(key)) state.gateways.delete(key);
|
if (now - entry.time > GATEWAY_HOLD_MS || !model.byKey.has(key)) state.gateways.delete(key);
|
||||||
else if (!model.bridgeAt.has(key)) bridges.push(entry.bridge);
|
else if (!model.bridgeAt.has(key)) bridges.push(entry.bridge);
|
||||||
@@ -311,14 +325,19 @@
|
|||||||
|
|
||||||
nets.forEach((net) => {
|
nets.forEach((net) => {
|
||||||
if (!state.colors.has(net)) state.colors.set(net, state.nextColor++ % M.COLORS);
|
if (!state.colors.has(net)) state.colors.set(net, state.nextColor++ % M.COLORS);
|
||||||
|
if (!state.age.has(net)) state.age.set(net, state.nextAge++);
|
||||||
});
|
});
|
||||||
|
|
||||||
const groups = nets.map((id) => {
|
const groups = nets.map((id) => {
|
||||||
const members = model.nodes.filter((node) => node.net === id).sort(M.compareAddresses);
|
const members = model.nodes.filter((node) => node.net === id).sort(M.compareAddresses);
|
||||||
const plain = members.filter((node) => !bridgeAt.has(node.key));
|
const plain = members.filter((node) => !bridgeAt.has(node.key));
|
||||||
const region = overrides.regions[id] || {};
|
const region = overrides.regions[id] || {};
|
||||||
|
/* Columns are chosen once so nothing reflows, and again only when the network has more than doubled. */
|
||||||
let cols = region.cols || state.cols.get(id);
|
let cols = region.cols || state.cols.get(id);
|
||||||
if (!cols) cols = clamp(Math.ceil(Math.sqrt(1.6 * Math.max(1, plain.length))), 1, 6);
|
if (!cols || (!region.cols && plain.length > 2 * (state.colsFor.get(id) || 1))) {
|
||||||
|
cols = clamp(Math.ceil(Math.sqrt(1.6 * Math.max(1, plain.length))), 1, 6);
|
||||||
|
state.colsFor.set(id, Math.max(1, plain.length));
|
||||||
|
}
|
||||||
state.cols.set(id, cols);
|
state.cols.set(id, cols);
|
||||||
const cells = assignCells(state, id, plain, overrides.slots[id] || {});
|
const cells = assignCells(state, id, plain, overrides.slots[id] || {});
|
||||||
const rows = Math.max(1, Math.ceil((Math.max(-1, ...cells.values()) + 1) / cols));
|
const rows = Math.max(1, Math.ceil((Math.max(-1, ...cells.values()) + 1) / cols));
|
||||||
@@ -347,7 +366,7 @@
|
|||||||
|
|
||||||
/* Sides, then sizes. */
|
/* Sides, then sizes. */
|
||||||
groups.forEach((group) => {
|
groups.forEach((group) => {
|
||||||
group.sides = { left: [], right: [], top: [], bottom: [] };
|
group.sides = { left: [], right: [], top: [] };
|
||||||
group.gateways.forEach((node) => {
|
group.gateways.forEach((node) => {
|
||||||
const bridge = bridgeAt.get(node.key);
|
const bridge = bridgeAt.get(node.key);
|
||||||
const others = Array.from(bridge.touches).filter((id) => id !== group.id);
|
const others = Array.from(bridge.touches).filter((id) => id !== group.id);
|
||||||
@@ -367,16 +386,16 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const count = (side) => group.sides[side].length;
|
const count = (side) => group.sides[side].length;
|
||||||
const reserve = { left: count("left") ? DOCK_LANE : 0, right: count("right") ? DOCK_LANE : 0, top: count("top") ? RESERVE_TOP : 0, bottom: count("bottom") ? RESERVE_BOTTOM : 0 };
|
/* A gateway is a node like any other, in a cell of a strip along the edge it docks on. */
|
||||||
|
const reserve = { left: count("left") ? CELL_W : 0, right: count("right") ? CELL_W : 0, top: count("top") ? CELL_H + 8 : 0 };
|
||||||
const header = 33 + 8.4 * group.id.length + 78;
|
const header = 33 + 8.4 * group.id.length + 78;
|
||||||
const width = Math.max(
|
const width = Math.max(
|
||||||
MIN_WIDTH,
|
MIN_WIDTH,
|
||||||
2 * REGION_PAD + reserve.left + reserve.right + group.cols * CELL_W,
|
2 * REGION_PAD + reserve.left + reserve.right + group.cols * CELL_W,
|
||||||
count("top") ? Math.max(header, reserve.left) + count("top") * DOCK_PITCH + 10 + reserve.right : 0,
|
count("top") ? Math.max(header + 20, reserve.left) + count("top") * CELL_W + reserve.right : 0
|
||||||
count("bottom") ? reserve.left + count("bottom") * DOCK_PITCH + 25 + reserve.right : 0
|
|
||||||
);
|
);
|
||||||
const lane = Math.max(count("left"), count("right"));
|
const lane = Math.max(count("left"), count("right"));
|
||||||
const height = Math.max(HEAD + reserve.top + group.rows * CELL_H + reserve.bottom, lane ? HEAD + 30 + (lane - 1) * DOCK_ROW + LABEL_H + 10 : 0, HEAD + CELL_H);
|
const height = Math.max(HEAD + reserve.top + group.rows * CELL_H, HEAD + lane * CELL_H, HEAD + CELL_H) + FOOTER;
|
||||||
Object.assign(group, { reserve: reserve, header: header, w: width, h: height });
|
Object.assign(group, { reserve: reserve, header: header, w: width, h: height });
|
||||||
state.sizes.set(group.id, { w: width, h: height });
|
state.sizes.set(group.id, { w: width, h: height });
|
||||||
});
|
});
|
||||||
@@ -433,7 +452,7 @@
|
|||||||
}
|
}
|
||||||
rects[group.id] = { x: at.x, y: at.y, w: group.w, h: group.h };
|
rects[group.id] = { x: at.x, y: at.y, w: group.w, h: group.h };
|
||||||
});
|
});
|
||||||
resolveOverlaps(rects, fixed, GAP);
|
resolveOverlaps(rects, fixed, GAP, state.age);
|
||||||
groups.forEach((group) => {
|
groups.forEach((group) => {
|
||||||
state.regions.set(group.id, { x: rects[group.id].x, y: rects[group.id].y });
|
state.regions.set(group.id, { x: rects[group.id].x, y: rects[group.id].y });
|
||||||
});
|
});
|
||||||
@@ -447,6 +466,9 @@
|
|||||||
const inner = group.w - group.reserve.left - group.reserve.right;
|
const inner = group.w - group.reserve.left - group.reserve.right;
|
||||||
const gridX = at.x + group.reserve.left + (inner - group.cols * CELL_W) / 2;
|
const gridX = at.x + group.reserve.left + (inner - group.cols * CELL_W) / 2;
|
||||||
const gridY = at.y + HEAD + group.reserve.top;
|
const gridY = at.y + HEAD + group.reserve.top;
|
||||||
|
/* The lines the legs run along: inside the side strips and just outside the grid, where no label reaches. */
|
||||||
|
const ringLeft = group.reserve.left ? at.x + group.reserve.left : gridX;
|
||||||
|
const ringRight = group.reserve.right ? at.x + group.w - group.reserve.right : gridX + group.cols * CELL_W;
|
||||||
const region = {
|
const region = {
|
||||||
id: group.id,
|
id: group.id,
|
||||||
color: state.colors.get(group.id),
|
color: state.colors.get(group.id),
|
||||||
@@ -456,7 +478,9 @@
|
|||||||
h: group.h,
|
h: group.h,
|
||||||
cols: group.cols,
|
cols: group.cols,
|
||||||
rows: group.rows,
|
rows: group.rows,
|
||||||
grid: { x: gridX, y: gridY, bottom: gridY + group.rows * CELL_H },
|
grid: { x: gridX, y: gridY },
|
||||||
|
ring: { x0: ringLeft, x1: ringRight, y0: gridY - RING_GAP, y1: gridY + group.rows * CELL_H + RING_GAP },
|
||||||
|
footer: { y: at.y + group.h - FOOTER, h: FOOTER },
|
||||||
header: group.header,
|
header: group.header,
|
||||||
count: group.members.length,
|
count: group.members.length,
|
||||||
alive: group.members.filter((node) => node.alive).length,
|
alive: group.members.filter((node) => node.alive).length,
|
||||||
@@ -470,7 +494,7 @@
|
|||||||
const row = Math.floor(cell / group.cols);
|
const row = Math.floor(cell / group.cols);
|
||||||
const x = gridX + col * CELL_W + CELL_W / 2;
|
const x = gridX + col * CELL_W + CELL_W / 2;
|
||||||
const y = gridY + row * CELL_H + GRID_INSET;
|
const y = gridY + row * CELL_H + GRID_INSET;
|
||||||
const item = { key: node.key, net: group.id, color: region.color, x: x, y: y, r: NODE_R, cell: cell, col: col, row: row, gateway: false, dock: null, box: { x: x - LABEL_W / 2, y: y + 18, w: LABEL_W, h: LABEL_H - 4 } };
|
const item = { key: node.key, net: group.id, color: region.color, x: x, y: y, r: NODE_R, cell: cell, col: col, row: row, gateway: false, dock: null, port: null, box: { x: x - BOX_W / 2, y: y + NODE_R + 4, w: BOX_W, h: BOX_H } };
|
||||||
nodes.push(item);
|
nodes.push(item);
|
||||||
nodeAt.set(node.key, item);
|
nodeAt.set(node.key, item);
|
||||||
});
|
});
|
||||||
@@ -480,21 +504,16 @@
|
|||||||
const others = Array.from(bridge.touches).filter((id) => id !== group.id && state.regions.has(id));
|
const others = Array.from(bridge.touches).filter((id) => id !== group.id && state.regions.has(id));
|
||||||
return others.length ? meanPoint(others.map((id) => netCentre(state, id))) : centreOf(region);
|
return others.length ? meanPoint(others.map((id) => netCentre(state, id))) : centreOf(region);
|
||||||
};
|
};
|
||||||
["left", "right", "top", "bottom"].forEach((side) => {
|
["left", "right", "top"].forEach((side) => {
|
||||||
const horizontal = side === "top" || side === "bottom";
|
|
||||||
const ordered = group.sides[side]
|
const ordered = group.sides[side]
|
||||||
.map((node) => ({ node: node, key: own(overrides.docks, node.key) ? overrides.docks[node.key].t : horizontal ? spot(node).x : spot(node).y }))
|
.map((node) => ({ node: node, key: own(overrides.docks, node.key) ? overrides.docks[node.key].t : side === "top" ? spot(node).x : spot(node).y }))
|
||||||
.sort((a, b) => a.key - b.key || a.node.key.localeCompare(b.node.key))
|
.sort((a, b) => a.key - b.key || a.node.key.localeCompare(b.node.key))
|
||||||
.map((entry) => entry.node);
|
.map((entry) => entry.node);
|
||||||
ordered.forEach((node, index) => {
|
ordered.forEach((node, index) => {
|
||||||
const [x, y] = side === "left" || side === "right"
|
const x = side === "top" ? at.x + Math.max(group.header + 20, group.reserve.left) + index * CELL_W + CELL_W / 2 : side === "left" ? at.x + CELL_W / 2 : at.x + group.w - CELL_W / 2;
|
||||||
? [side === "left" ? at.x : at.x + group.w, at.y + HEAD + 30 + index * DOCK_ROW]
|
const y = at.y + HEAD + (side === "top" ? 0 : index * CELL_H) + GRID_INSET;
|
||||||
: [side === "top" ? at.x + Math.max(group.header, group.reserve.left) + 30 + index * DOCK_PITCH : at.x + group.reserve.left + 40 + index * DOCK_PITCH, side === "top" ? at.y : at.y + group.h];
|
const port = side === "left" ? { x: at.x, y: y } : side === "right" ? { x: at.x + group.w, y: y } : { x: x, y: at.y };
|
||||||
const box = side === "left" ? { x: x + 27, y: y + 6, w: LABEL_W, h: LABEL_H }
|
const item = { key: node.key, net: group.id, color: region.color, x: x, y: y, r: GATEWAY_R, cell: null, gateway: true, dock: { side: side, index: index }, port: port, box: { x: x - BOX_W / 2, y: y + GATEWAY_R + 4, w: BOX_W, h: BOX_H } };
|
||||||
: side === "right" ? { x: x - 27 - LABEL_W, y: y + 6, w: LABEL_W, h: LABEL_H }
|
|
||||||
: side === "top" ? { x: x + 27, y: y + 6, w: LABEL_W, h: LABEL_H }
|
|
||||||
: { x: x + 27, y: y - 6 - LABEL_H, w: LABEL_W, h: LABEL_H };
|
|
||||||
const item = { key: node.key, net: group.id, color: region.color, x: x, y: y, r: GATEWAY_R, cell: null, gateway: true, dock: { side: side, index: index }, box: box };
|
|
||||||
nodes.push(item);
|
nodes.push(item);
|
||||||
nodeAt.set(node.key, item);
|
nodeAt.set(node.key, item);
|
||||||
});
|
});
|
||||||
@@ -509,8 +528,8 @@
|
|||||||
bridge.touches.forEach((id) => {
|
bridge.touches.forEach((id) => {
|
||||||
if (id === gateway.net || !regionAt.has(id)) return;
|
if (id === gateway.net || !regionAt.has(id)) return;
|
||||||
const target = regionAt.get(id);
|
const target = regionAt.get(id);
|
||||||
const end = entryPoint(gateway, target);
|
const end = entryPoint(gateway.port, target);
|
||||||
spokes.push({ gateway: bridge.key, to: id, color: target.color, x1: gateway.x, y1: gateway.y, x2: end.x, y2: end.y, d: spokePath(gateway, gateway.dock.side, end, edgeSide(end, target)) });
|
spokes.push({ gateway: bridge.key, to: id, color: target.color, x1: gateway.port.x, y1: gateway.port.y, x2: end.x, y2: end.y, d: spokePath(gateway.port, gateway.dock.side, end, edgeSide(end, target)) });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -526,39 +545,43 @@
|
|||||||
|
|
||||||
/* ------------------------------------------------------------------ routes */
|
/* ------------------------------------------------------------------ routes */
|
||||||
|
|
||||||
|
/* From a gateway's disc out to the ring of gaps around the grid: sideways for a strip on the
|
||||||
|
left or right, and along the gap beside its cell for a strip on top. */
|
||||||
|
function lead(region, dock) {
|
||||||
|
const disc = { x: dock.x, y: dock.y };
|
||||||
|
const side = dock.dock.side;
|
||||||
|
if (side === "left") return [disc, { x: region.ring.x0, y: dock.y }];
|
||||||
|
if (side === "right") return [disc, { x: region.ring.x1, y: dock.y }];
|
||||||
|
const column = dock.x - CELL_W / 2;
|
||||||
|
return [disc, { x: column, y: dock.y }, { x: column, y: region.ring.y0 }];
|
||||||
|
}
|
||||||
|
|
||||||
/* From a member of a network to a gateway docked on it (or back), along the gaps between
|
/* From a member of a network to a gateway docked on it (or back), along the gaps between
|
||||||
cells and the lanes beside them, so the line never crosses a node or a label. It leaves the
|
cells, so the line never crosses a node or a label. It leaves the node upwards (its label
|
||||||
node upwards (its label is below), runs along the gap above its row, then along the edge
|
is below), runs along the gap above its row, then along the ring to the gateway. */
|
||||||
of the grid to the gateway. */
|
|
||||||
function streetPoints(map, plain, dock) {
|
function streetPoints(map, plain, dock) {
|
||||||
const region = map.regionAt.get(plain.net);
|
const region = map.regionAt.get(plain.net);
|
||||||
const gap = region.grid.y + plain.row * CELL_H;
|
const gap = region.grid.y + plain.row * CELL_H;
|
||||||
const start = { x: plain.x, y: plain.y };
|
const start = { x: plain.x, y: plain.y };
|
||||||
const up = { x: plain.x, y: gap };
|
const up = { x: plain.x, y: gap };
|
||||||
const end = { x: dock.x, y: dock.y };
|
const way = lead(region, dock).reverse();
|
||||||
const side = dock.dock.side;
|
const ring = way[0];
|
||||||
|
|
||||||
if (side === "left" || side === "right") {
|
if (dock.dock.side !== "top") return [start, up, { x: ring.x, y: gap }].concat(way);
|
||||||
const edge = side === "left" ? region.grid.x : region.grid.x + region.cols * CELL_W;
|
|
||||||
return [start, up, { x: edge, y: gap }, { x: edge, y: end.y }, end];
|
|
||||||
}
|
|
||||||
const left = region.grid.x + plain.col * CELL_W;
|
const left = region.grid.x + plain.col * CELL_W;
|
||||||
const column = Math.abs(left - end.x) <= Math.abs(left + CELL_W - end.x) ? left : left + CELL_W;
|
const column = Math.abs(left - ring.x) <= Math.abs(left + CELL_W - ring.x) ? left : left + CELL_W;
|
||||||
const lane = side === "top" ? region.grid.y - 4 : region.grid.bottom + 4;
|
return [start, up, { x: column, y: gap }, { x: column, y: ring.y }].concat(way);
|
||||||
return [start, up, { x: column, y: gap }, { x: column, y: lane }, { x: end.x, y: lane }, end];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Between two gateways of one network: out of the first onto the ring of gaps around the
|
/* Between two gateways of one network: out of the first onto the ring of gaps around the
|
||||||
grid, along it, and into the second. */
|
grid, along it, and into the second. */
|
||||||
function ringPoints(map, a, b) {
|
function ringPoints(map, a, b) {
|
||||||
const region = map.regionAt.get(a.net);
|
const region = map.regionAt.get(a.net);
|
||||||
const x0 = region.grid.x;
|
const { x0, x1, y0, y1 } = region.ring;
|
||||||
const x1 = region.grid.x + region.cols * CELL_W;
|
const outA = lead(region, a);
|
||||||
const y0 = region.grid.y - 4;
|
const outB = lead(region, b);
|
||||||
const y1 = region.grid.bottom + 4;
|
const ea = outA[outA.length - 1];
|
||||||
const onRing = (dock) => ({ left: { x: x0, y: dock.y }, right: { x: x1, y: dock.y }, top: { x: dock.x, y: y0 }, bottom: { x: dock.x, y: y1 } })[dock.dock.side];
|
const eb = outB[outB.length - 1];
|
||||||
const ea = onRing(a);
|
|
||||||
const eb = onRing(b);
|
|
||||||
const sa = a.dock.side;
|
const sa = a.dock.side;
|
||||||
const sb = b.dock.side;
|
const sb = b.dock.side;
|
||||||
const length = (points) => points.reduce((sum, point, i) => (i ? sum + Math.hypot(point.x - points[i - 1].x, point.y - points[i - 1].y) : 0), 0);
|
const length = (points) => points.reduce((sum, point, i) => (i ? sum + Math.hypot(point.x - points[i - 1].x, point.y - points[i - 1].y) : 0), 0);
|
||||||
@@ -570,18 +593,11 @@
|
|||||||
const over = [{ x: xa, y: y0 }, { x: xb, y: y0 }];
|
const over = [{ x: xa, y: y0 }, { x: xb, y: y0 }];
|
||||||
const under = [{ x: xa, y: y1 }, { x: xb, y: y1 }];
|
const under = [{ x: xa, y: y1 }, { x: xb, y: y1 }];
|
||||||
middle = length([ea].concat(over, [eb])) <= length([ea].concat(under, [eb])) ? over : under;
|
middle = length([ea].concat(over, [eb])) <= length([ea].concat(under, [eb])) ? over : under;
|
||||||
} else if ((sa === "top" && sb === "bottom") || (sa === "bottom" && sb === "top")) {
|
|
||||||
const ya = sa === "top" ? y0 : y1;
|
|
||||||
const yb = sa === "top" ? y1 : y0;
|
|
||||||
const west = [{ x: x0, y: ya }, { x: x0, y: yb }];
|
|
||||||
const east = [{ x: x1, y: ya }, { x: x1, y: yb }];
|
|
||||||
middle = length([ea].concat(west, [eb])) <= length([ea].concat(east, [eb])) ? west : east;
|
|
||||||
} else if (sa !== sb) {
|
} else if (sa !== sb) {
|
||||||
const horizontal = sa === "left" || sa === "right" ? sa : sb;
|
const side = sa === "top" ? sb : sa;
|
||||||
const vertical = sa === "top" || sa === "bottom" ? sa : sb;
|
middle = [{ x: side === "left" ? x0 : x1, y: y0 }];
|
||||||
middle = [{ x: horizontal === "left" ? x0 : x1, y: vertical === "top" ? y0 : y1 }];
|
|
||||||
}
|
}
|
||||||
return [{ x: a.x, y: a.y }, ea].concat(middle, [eb, { x: b.x, y: b.y }]);
|
return outA.concat(middle, outB.reverse());
|
||||||
}
|
}
|
||||||
|
|
||||||
function legPoints(map, from, to) {
|
function legPoints(map, from, to) {
|
||||||
@@ -620,7 +636,7 @@
|
|||||||
|
|
||||||
const gateway = map.nodeAt.get(route.via);
|
const gateway = map.nodeAt.get(route.via);
|
||||||
const name = model.byKey.get(route.via);
|
const name = model.byKey.get(route.via);
|
||||||
captions.set(key, "via " + (name ? name.hostname : route.via));
|
captions.set(key, "via " + M.shorten(name ? name.hostname : route.via, 20));
|
||||||
if (!gateway) return;
|
if (!gateway) return;
|
||||||
if (gateway !== origin) leg(origin, gateway, origin.color);
|
if (gateway !== origin) leg(origin, gateway, origin.color);
|
||||||
if (gateway.net === target.net) leg(gateway, target, target.color);
|
if (gateway.net === target.net) leg(gateway, target, target.color);
|
||||||
@@ -638,21 +654,31 @@
|
|||||||
const region = map.regionAt.get(net);
|
const region = map.regionAt.get(net);
|
||||||
if (!region) return null;
|
if (!region) return null;
|
||||||
const col = clamp(Math.floor((x - region.grid.x) / CELL_W), 0, region.cols - 1);
|
const col = clamp(Math.floor((x - region.grid.x) / CELL_W), 0, region.cols - 1);
|
||||||
const row = Math.max(0, Math.floor((y - region.grid.y) / CELL_H));
|
const row = clamp(Math.floor((y - region.grid.y) / CELL_H), 0, region.rows);
|
||||||
return row * region.cols + col;
|
return row * region.cols + col;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Which edge of a network a dropped gateway belongs to, and the position that orders it along that edge. */
|
/* Which edge of a network a dropped gateway belongs to, and the position that orders it along that
|
||||||
function dockAt(map, net, x, y) {
|
edge. The edge is the one whose strip is nearest; where two strips meet (a corner) the gateway's
|
||||||
|
current edge wins, so a gateway put back where it was stays put. */
|
||||||
|
function dockAt(map, net, x, y, current) {
|
||||||
const region = map.regionAt.get(net);
|
const region = map.regionAt.get(net);
|
||||||
const side = edgeSide({ x: clamp(x, region.x, region.x + region.w), y: clamp(y, region.y, region.y + region.h) }, region);
|
const top = region.y + HEAD + GRID_INSET;
|
||||||
return { side: side, t: side === "top" || side === "bottom" ? x : y };
|
const bottom = region.footer.y;
|
||||||
|
const away = (x0, y0, x1, y1) => Math.hypot(x - clamp(x, x0, x1), y - clamp(y, y0, y1));
|
||||||
|
const strips = [
|
||||||
|
[away(region.x + CELL_W / 2, top, region.x + CELL_W / 2, bottom), "left"],
|
||||||
|
[away(region.x + region.w - CELL_W / 2, top, region.x + region.w - CELL_W / 2, bottom), "right"],
|
||||||
|
[away(region.x + region.header + 20, top, region.x + region.w - CELL_W / 2, top), "top"],
|
||||||
|
];
|
||||||
|
const side = strips.sort((p, q) => Math.round(p[0]) - Math.round(q[0]) || (q[1] === current) - (p[1] === current))[0][1];
|
||||||
|
return { side: side, t: side === "top" ? x : y };
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
CELL_W, CELL_H, GATEWAY_R, NODE_R, GAP, EMPTY_BOUNDS,
|
CELL_W, CELL_H, GATEWAY_R, NODE_R, GAP, EMPTY_BOUNDS, STAT_W, STAT_COUNT, STAT_TOP, STAT_ROW,
|
||||||
newState, newOverrides, parseOverrides, serializeOverrides, hasOverrides, setRegion, setCell, setDock,
|
newState, newOverrides, parseOverrides, serializeOverrides, hasOverrides, setRegion, setCell, setDock,
|
||||||
gridSlots, orderNetworks, resolveOverlaps, entryPoint, roundedPath, computeMap, traceGeometry, cellAt, dockAt,
|
gridSlots, orderNetworks, resolveOverlaps, entryPoint, roundedPath, computeMap, traceGeometry, cellAt, dockAt, noteGateways,
|
||||||
};
|
};
|
||||||
if (typeof module !== "undefined") module.exports = api;
|
if (typeof module !== "undefined") module.exports = api;
|
||||||
else globalThis.Colony = Object.assign(globalThis.Colony || {}, api);
|
else globalThis.Colony = Object.assign(globalThis.Colony || {}, api);
|
||||||
|
|||||||
@@ -107,6 +107,7 @@
|
|||||||
self: Boolean(member.self),
|
self: Boolean(member.self),
|
||||||
roles: member.roles,
|
roles: member.roles,
|
||||||
declined: member.declined || [],
|
declined: member.declined || [],
|
||||||
|
stats: member.stats || {},
|
||||||
}));
|
}));
|
||||||
const byKey = new Map(nodes.map((node) => [node.key, node]));
|
const byKey = new Map(nodes.map((node) => [node.key, node]));
|
||||||
const states = roleStates(nodes, payload.roles);
|
const states = roleStates(nodes, payload.roles);
|
||||||
@@ -245,12 +246,110 @@
|
|||||||
return text + " · " + held.length + "/" + assigned.length + " roles held";
|
return text + " · " + held.length + "/" + assigned.length + " roles held";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ hardware */
|
||||||
|
|
||||||
|
function formatSize(gb) {
|
||||||
|
if (gb >= 1000) return (gb / 1024).toFixed(1) + " TB";
|
||||||
|
if (gb >= 100) return Math.round(gb) + " GB";
|
||||||
|
return gb.toFixed(1) + " GB";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUptime(hours) {
|
||||||
|
if (hours < 1) return Math.round(hours * 60) + "m";
|
||||||
|
if (hours < 48) return Math.round(hours) + "h";
|
||||||
|
return Math.floor(hours / 24) + "d " + Math.round(hours % 24) + "h";
|
||||||
|
}
|
||||||
|
|
||||||
|
const percent = (used, total) => (total > 0 ? Math.max(0, Math.min(100, Math.round((100 * used) / total))) : null);
|
||||||
|
const barLevel = (pct) => (pct >= 90 ? "crit" : pct >= 75 ? "warn" : "ok");
|
||||||
|
|
||||||
|
/* What one node reports about itself, ready to draw: three bars and a footer. */
|
||||||
|
function describeStats(stats) {
|
||||||
|
stats = stats || {};
|
||||||
|
const num = (key) => (typeof stats[key] === "number" ? stats[key] : null);
|
||||||
|
const usage = (usedKey, totalKey) => {
|
||||||
|
const used = num(usedKey);
|
||||||
|
const total = num(totalKey);
|
||||||
|
return used === null || total === null ? null : { pct: percent(used, total), text: formatSize(used) + " / " + formatSize(total) };
|
||||||
|
};
|
||||||
|
|
||||||
|
const load = num("cpu_percent");
|
||||||
|
const cores = num("cpu_cores");
|
||||||
|
const threads = num("cpu_threads");
|
||||||
|
let cpu = null;
|
||||||
|
if (load !== null) {
|
||||||
|
let text = Math.round(load) + "%";
|
||||||
|
if (cores && threads) text += " · " + cores + "C/" + threads + "T";
|
||||||
|
else if (threads) text += " · " + threads + "T";
|
||||||
|
cpu = { pct: Math.round(load), text: text };
|
||||||
|
}
|
||||||
|
const gpuCount = num("gpu_count");
|
||||||
|
const gpuUsage = usage("gpu_mem_used_gb", "gpu_mem_total_gb");
|
||||||
|
const uptime = num("uptime_hours");
|
||||||
|
|
||||||
|
const ram = usage("mem_used_gb", "mem_total_gb");
|
||||||
|
const disk = usage("disk_used_gb", "disk_total_gb");
|
||||||
|
const gpu = gpuCount && gpuUsage ? (gpuCount > 1 ? gpuCount + "× " : "") + "GPU " + gpuUsage.text : null;
|
||||||
|
const footer = [uptime === null ? null : "up " + formatUptime(uptime), gpu].filter(Boolean).join(" · ");
|
||||||
|
return { rows: [["CPU", cpu], ["RAM", ram], ["DISK", disk]], footer: cpu || ram || disk ? footer : "no stats reported" };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Per machine these are the same for every node on it; the "used" ones move and are averaged. */
|
||||||
|
const TOTAL_KEYS = ["cpu_cores", "cpu_threads", "mem_total_gb", "disk_total_gb", "gpu_count", "gpu_mem_total_gb"];
|
||||||
|
const USED_KEYS = ["mem_used_gb", "disk_used_gb", "gpu_mem_used_gb"];
|
||||||
|
|
||||||
|
const plural = (count, word) => count + " " + word + (count === 1 ? "" : "s");
|
||||||
|
|
||||||
|
/* "CPU 9% · 6C/12T, RAM ..." for the rows that have numbers. */
|
||||||
|
const describeRows = (rows) => rows.filter(([, usage]) => usage).map(([name, usage]) => name + " " + usage.text);
|
||||||
|
|
||||||
|
/* The hardware of one network added up. Nodes that report the same complete totals are one
|
||||||
|
machine (containers on a host all see the host), so a machine is counted once and its
|
||||||
|
numbers are the average of what its nodes report. Load is weighted by threads. */
|
||||||
|
function networkStats(model, net) {
|
||||||
|
const reporting = model.nodes.filter((node) => node.net === net && node.alive && Object.keys(node.stats).length);
|
||||||
|
const machines = new Map();
|
||||||
|
reporting.forEach((node) => {
|
||||||
|
const s = node.stats;
|
||||||
|
const complete = typeof s.mem_total_gb === "number" && typeof s.disk_total_gb === "number";
|
||||||
|
const fingerprint = complete ? TOTAL_KEYS.map((key) => s[key]).join("|") : node.key;
|
||||||
|
if (!machines.has(fingerprint)) machines.set(fingerprint, []);
|
||||||
|
machines.get(fingerprint).push(s);
|
||||||
|
});
|
||||||
|
|
||||||
|
const numbers = (list, key) => list.map((s) => s[key]).filter((value) => typeof value === "number");
|
||||||
|
const mean = (list, key) => {
|
||||||
|
const values = numbers(list, key);
|
||||||
|
return values.length ? values.reduce((a, b) => a + b, 0) / values.length : null;
|
||||||
|
};
|
||||||
|
const stats = {};
|
||||||
|
const add = (key, value) => {
|
||||||
|
if (typeof value === "number") stats[key] = (stats[key] || 0) + value;
|
||||||
|
};
|
||||||
|
let weighted = 0;
|
||||||
|
let weight = 0;
|
||||||
|
machines.forEach((list) => {
|
||||||
|
TOTAL_KEYS.forEach((key) => add(key, numbers(list, key)[0]));
|
||||||
|
USED_KEYS.forEach((key) => add(key, mean(list, key)));
|
||||||
|
const load = mean(list, "cpu_percent");
|
||||||
|
if (load !== null) {
|
||||||
|
const threads = numbers(list, "cpu_threads")[0] || 1;
|
||||||
|
weighted += load * threads;
|
||||||
|
weight += threads;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (weight) stats.cpu_percent = weighted / weight;
|
||||||
|
return Object.assign(describeStats(stats), { machines: machines.size, nodes: reporting.length });
|
||||||
|
}
|
||||||
|
|
||||||
/* The full text of a node: tooltip and screen reader. */
|
/* The full text of a node: tooltip and screen reader. */
|
||||||
function describeNode(model, node, routes) {
|
function describeNode(model, node, routes) {
|
||||||
const lines = [node.hostname + " " + node.key + (node.self ? " (this node)" : "") + (node.alive ? "" : " (down)")];
|
const lines = [node.hostname + " " + node.key + (node.self ? " (this node)" : "") + (node.alive ? "" : " (down)")];
|
||||||
const bridge = model.bridgeAt.get(node.key);
|
const bridge = model.bridgeAt.get(node.key);
|
||||||
if (bridge) lines.push("gateway: relays " + bridge.pairs + " node pair" + (bridge.pairs === 1 ? "" : "s"));
|
if (bridge) lines.push("gateway: relays " + plural(bridge.pairs, "node pair"));
|
||||||
lines.push("roles: " + (node.roles.join(", ") || "none"));
|
lines.push("roles: " + (node.roles.join(", ") || "none"));
|
||||||
|
const hardware = describeStats(node.stats);
|
||||||
|
lines.push(describeRows(hardware.rows).concat(hardware.footer || []).join(" · "));
|
||||||
if (node.declined.length) lines.push("declined: " + node.declined.join(", "));
|
if (node.declined.length) lines.push("declined: " + node.declined.join(", "));
|
||||||
|
|
||||||
const route = routes && node.alive && node.key !== routes.viewKey ? routes.byKey.get(node.key) : null;
|
const route = routes && node.alive && node.key !== routes.viewKey ? routes.byKey.get(node.key) : null;
|
||||||
@@ -267,7 +366,7 @@
|
|||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = { OTHER_NETWORK, COLORS, address, keyOf, networkOf, compareNetworks, compareAddresses, shorten, wrapLabels, analyze, bridgesOf, resolveView, routesFrom, routeSummary, specialRoles, alerts, onDemandHint, summarize, describeNode };
|
const api = { describeStats, describeRows, networkStats, plural, barLevel, OTHER_NETWORK, COLORS, address, keyOf, networkOf, compareNetworks, compareAddresses, shorten, wrapLabels, analyze, bridgesOf, resolveView, routesFrom, routeSummary, specialRoles, alerts, onDemandHint, summarize, describeNode };
|
||||||
if (typeof module !== "undefined") module.exports = api;
|
if (typeof module !== "undefined") module.exports = api;
|
||||||
else globalThis.Colony = Object.assign(globalThis.Colony || {}, api);
|
else globalThis.Colony = Object.assign(globalThis.Colony || {}, api);
|
||||||
})();
|
})();
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
--muted: #6b7686;
|
--muted: #6b7686;
|
||||||
--line: #d3d9e1;
|
--line: #d3d9e1;
|
||||||
--down: #98a2b3;
|
--down: #98a2b3;
|
||||||
|
--track: #dde2ea;
|
||||||
--panel: rgba(255, 255, 255, 0.92);
|
--panel: rgba(255, 255, 255, 0.92);
|
||||||
--warn: #c67a08;
|
--warn: #c67a08;
|
||||||
--crit: #d64545;
|
--crit: #d64545;
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
--muted: #93a0b3;
|
--muted: #93a0b3;
|
||||||
--line: #313a47;
|
--line: #313a47;
|
||||||
--down: #6b7686;
|
--down: #6b7686;
|
||||||
|
--track: #2a3340;
|
||||||
--panel: rgba(27, 33, 41, 0.92);
|
--panel: rgba(27, 33, 41, 0.92);
|
||||||
--warn: #ffb04a;
|
--warn: #ffb04a;
|
||||||
--crit: #ff6b6b;
|
--crit: #ff6b6b;
|
||||||
@@ -119,6 +121,12 @@ p { margin: 4px 0; }
|
|||||||
.node.down .disc { stroke-dasharray: 4 3; }
|
.node.down .disc { stroke-dasharray: 4 3; }
|
||||||
.node.down .dot { fill: var(--down); }
|
.node.down .dot { fill: var(--down); }
|
||||||
.node.gateway .disc { stroke-width: 4; }
|
.node.gateway .disc { stroke-width: 4; }
|
||||||
|
|
||||||
|
/* The door on the network's edge and its link to the gateway node inside. */
|
||||||
|
.port { fill: var(--card); stroke: var(--net); stroke-width: 3.5; vector-effect: non-scaling-stroke; pointer-events: none; }
|
||||||
|
.port-link { stroke: var(--net); stroke-width: 3; stroke-linecap: round; vector-effect: non-scaling-stroke; pointer-events: none; }
|
||||||
|
.foot-rule { stroke: color-mix(in srgb, var(--net) 40%, transparent); stroke-width: 1; vector-effect: non-scaling-stroke; }
|
||||||
|
.foot .st-text { font-weight: 600; }
|
||||||
.node.none .halo, .node.unknown .halo { stroke: var(--muted); stroke-width: 2; stroke-dasharray: 4 3; }
|
.node.none .halo, .node.unknown .halo { stroke: var(--muted); stroke-width: 2; stroke-dasharray: 4 3; }
|
||||||
.node.reached .halo { stroke: var(--net); stroke-width: 2; }
|
.node.reached .halo { stroke: var(--net); stroke-width: 2; }
|
||||||
.node:hover .halo { stroke: var(--muted); stroke-dasharray: none; }
|
.node:hover .halo { stroke: var(--muted); stroke-dasharray: none; }
|
||||||
@@ -131,6 +139,15 @@ p { margin: 4px 0; }
|
|||||||
.l-name { fill: var(--text); font-size: 13px; font-weight: 600; }
|
.l-name { fill: var(--text); font-size: 13px; font-weight: 600; }
|
||||||
.l-roles { fill: var(--muted); font-size: 11px; }
|
.l-roles { fill: var(--muted); font-size: 11px; }
|
||||||
.l-caption { fill: var(--net); font-size: 11px; font-weight: 600; }
|
.l-caption { fill: var(--net); font-size: 11px; font-weight: 600; }
|
||||||
|
|
||||||
|
.st-text, .st-foot { paint-order: stroke; stroke: color-mix(in srgb, var(--net) 9%, var(--bg)); stroke-width: 3px; stroke-linejoin: round; pointer-events: none; font-size: 11px; font-variant-numeric: tabular-nums; }
|
||||||
|
.st-name { fill: var(--muted); }
|
||||||
|
.st-value { fill: var(--text); }
|
||||||
|
.st-foot { fill: var(--muted); }
|
||||||
|
.st-track { fill: color-mix(in srgb, var(--text) 13%, transparent); }
|
||||||
|
.st-fill { fill: var(--net); }
|
||||||
|
.st-fill.warn { fill: var(--warn); }
|
||||||
|
.st-fill.crit { fill: var(--crit); }
|
||||||
.node.none .l-caption, .node.unknown .l-caption { fill: var(--muted); }
|
.node.none .l-caption, .node.unknown .l-caption { fill: var(--muted); }
|
||||||
|
|
||||||
/* Level of detail: one class on the root, decided by the zoom. */
|
/* Level of detail: one class on the root, decided by the zoom. */
|
||||||
@@ -139,7 +156,11 @@ p { margin: 4px 0; }
|
|||||||
.lod0 .region-count { font-size: 24px; }
|
.lod0 .region-count { font-size: 24px; }
|
||||||
.lod0 .region-dot { r: 12px; }
|
.lod0 .region-dot { r: 12px; }
|
||||||
.lod1 .l-roles { display: none; }
|
.lod1 .l-roles { display: none; }
|
||||||
|
.lod0 .stats { display: none; }
|
||||||
|
.lod1 .st-text, .lod1 .st-foot { display: none; }
|
||||||
.lod0 .node.full .l-name, .lod0 .node.full .l-roles, .lod0 .node.full .l-caption, .lod1 .node.full .l-roles { display: inline; }
|
.lod0 .node.full .l-name, .lod0 .node.full .l-roles, .lod0 .node.full .l-caption, .lod1 .node.full .l-roles { display: inline; }
|
||||||
|
.lod0 .node.full .stats { display: inline; }
|
||||||
|
.lod1 .node.full .st-text, .lod1 .node.full .st-foot { display: inline; }
|
||||||
.lod0 .l-caption { display: none; }
|
.lod0 .l-caption { display: none; }
|
||||||
|
|
||||||
.flow { stroke-dasharray: none; }
|
.flow { stroke-dasharray: none; }
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
const K_MAX = 4;
|
const K_MAX = 4;
|
||||||
const K_START_MAX = 1.2;
|
const K_START_MAX = 1.2;
|
||||||
const KEEP = 80;
|
const KEEP = 80;
|
||||||
const LOD_AT = [0.4, 0.9];
|
const LOD_AT = [0.35, 0.7];
|
||||||
const HYSTERESIS = 0.1;
|
const HYSTERESIS = 0.1;
|
||||||
const THRESHOLD = { mouse: 4, pen: 4, touch: 8 };
|
const THRESHOLD = { mouse: 4, pen: 4, touch: 8 };
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user