diff --git a/ui/app.js b/ui/app.js
index 7a69138..f41c17c 100644
--- a/ui/app.js
+++ b/ui/app.js
@@ -11,6 +11,7 @@
laneGuides: [],
labelAnchors: [],
bloomPetals: [],
+ constellationCharts: [],
subnet: "all",
model: null,
groupHint: null,
@@ -252,6 +253,7 @@
state.laneGuides = [];
state.labelAnchors = [];
state.bloomPetals = [];
+ state.constellationCharts = [];
setTargets(nodes);
}
@@ -283,6 +285,7 @@
state.laneGuides = [];
state.labelAnchors = [];
state.bloomPetals = [];
+ state.constellationCharts = [];
setTargets(nodes);
}
@@ -336,78 +339,100 @@
state.laneGuides = [];
state.labelAnchors = anchors;
state.bloomPetals = [];
+ state.constellationCharts = [];
setTargets(nodes);
}
- function layoutBloom(devices, cx, cy, w, h) {
+ function layoutConstellation(devices, cx, cy, w, h) {
const groups = groupBySubnet(devices);
const nodes = [];
const anchors = [];
- const petals = [];
- const R = Math.min(w, h) * 0.38;
+ const charts = [];
+ const orbitR = Math.min(w, h) * 0.3;
const omeHub = { x: cx, y: cy };
const nGroups = Math.max(groups.length, 1);
+ const golden = Math.PI * (3 - Math.sqrt(5));
groups.forEach((g, si) => {
const a = (si / nGroups) * Math.PI * 2 - Math.PI / 2;
+ const hx = cx + Math.cos(a) * orbitR;
+ const hy = cy + Math.sin(a) * orbitR;
const info = subnetLabelInfo(g.cidr);
- const tipX = cx + Math.cos(a) * R;
- const tipY = cy + Math.sin(a) * R;
- const midX = cx + Math.cos(a) * R * 0.52;
- const midY = cy + Math.sin(a) * R * 0.52;
- const petalWidth = 22 + Math.min(48, g.members.length * 2.8);
+ const localR = 22 + Math.min(72, Math.sqrt(Math.max(g.members.length, 1)) * 15);
+ const pts = [];
- petals.push({
- angle: a,
- tipX,
- tipY,
- midX,
- midY,
- width: petalWidth,
+ g.members.forEach((n, i) => {
+ const r = localR * Math.sqrt((i + 0.55) / Math.max(g.members.length, 1));
+ const ang = i * golden + si * 0.35;
+ const jx = Math.sin((n.id || i) * 0.17) * 2.8;
+ const jy = Math.cos((n.id || i) * 0.13) * 2.8;
+ const x = hx + Math.cos(ang) * r + jx;
+ const y = hy + Math.sin(ang) * r + jy;
+ pts.push({ x, y, id: n.id });
+ nodes.push({
+ ...n,
+ tx: x,
+ ty: y,
+ r: nodeRadius(n),
+ depth: 1,
+ constellation: si,
+ });
+ });
+
+ // Constellation lines: each star to its 2 nearest neighbors
+ const edgeSet = new Set();
+ const edges = [];
+ for (let i = 0; i < pts.length; i++) {
+ const ranked = pts
+ .map((p, j) => ({
+ j,
+ d: i === j ? Infinity : (p.x - pts[i].x) ** 2 + (p.y - pts[i].y) ** 2,
+ }))
+ .sort((aa, bb) => aa.d - bb.d);
+ for (const nb of ranked.slice(0, Math.min(2, ranked.length))) {
+ if (!Number.isFinite(nb.d)) continue;
+ const lo = Math.min(i, nb.j);
+ const hi = Math.max(i, nb.j);
+ const key = lo + ":" + hi;
+ if (edgeSet.has(key)) continue;
+ edgeSet.add(key);
+ edges.push({
+ x1: pts[lo].x,
+ y1: pts[lo].y,
+ x2: pts[hi].x,
+ y2: pts[hi].y,
+ });
+ }
+ }
+
+ charts.push({
+ x: hx,
+ y: hy,
+ localR,
color: info.color,
cidr: g.cidr,
label: info.label,
+ edges,
count: g.members.length,
});
-
anchors.push(
makeLabelAnchor({
- x: tipX,
- y: tipY,
+ x: hx,
+ y: hy,
cidr: g.cidr,
- localR: 10,
- dirX: Math.cos(a),
- dirY: Math.sin(a),
- pipeExtra: 28,
+ localR,
+ dirX: hx - omeHub.x,
+ dirY: hy - omeHub.y,
+ pipeExtra: 34,
})
);
-
- g.members.forEach((n, i) => {
- const t = g.members.length <= 1 ? 0.55 : 0.18 + (i / Math.max(g.members.length - 1, 1)) * 0.72;
- const alongX = cx + Math.cos(a) * R * t;
- const alongY = cy + Math.sin(a) * R * t;
- // Petal bulge: widest mid-petal
- const bulge = Math.sin(Math.min(1, Math.max(0, (t - 0.12) / 0.76)) * Math.PI);
- const lane = (i % 2 === 0 ? 1 : -1) * (0.4 + ((i * 7) % 5) * 0.11);
- const px = -Math.sin(a);
- const py = Math.cos(a);
- const spread = bulge * petalWidth * lane;
- nodes.push({
- ...n,
- tx: alongX + px * spread,
- ty: alongY + py * spread,
- r: nodeRadius(n),
- depth: 0.65 + t * 0.55,
- bloom: si,
- bloomT: t,
- });
- });
});
state.clusterHubs = [];
state.laneGuides = [];
state.labelAnchors = anchors;
- state.bloomPetals = petals;
+ state.bloomPetals = [];
+ state.constellationCharts = charts;
setTargets(nodes);
}
@@ -452,6 +477,8 @@
state.clusterHubs = [];
state.laneGuides = guides;
state.labelAnchors = anchors;
+ state.bloomPetals = [];
+ state.constellationCharts = [];
// hub left side
state.hub = { x: 48, y: cy, r: 26 };
setTargets(nodes);
@@ -495,6 +522,8 @@
state.clusterHubs = [];
state.laneGuides = [];
state.labelAnchors = anchors;
+ state.bloomPetals = [];
+ state.constellationCharts = [];
setTargets(nodes);
}
@@ -514,8 +543,8 @@
case "clusters":
layoutClusters(devices, cx, cy, w, h);
break;
- case "bloom":
- layoutBloom(devices, cx, cy, w, h);
+ case "constellation":
+ layoutConstellation(devices, cx, cy, w, h);
break;
case "lanes":
layoutLanes(devices, cx, cy, w, h);
@@ -705,60 +734,51 @@
ctx.globalAlpha = 1;
drawPulseLink(hub.x, hub.y, ch.x, ch.y, true, t, (ch.label || "").length * 17, 0.55);
}
- } else if (mode === "bloom") {
- // Soft petal outlines + spine pulse
- for (const p of state.bloomPetals || []) {
- const rgb = hexToRgb(p.color || "#00a8e8");
- const a = p.angle || 0;
- const nx = -Math.sin(a);
- const ny = Math.cos(a);
- // Petal path: hub → left bulge → tip → right bulge → hub
- const c1x = hub.x + Math.cos(a) * (Math.hypot(p.midX - hub.x, p.midY - hub.y) || 80);
- const c1y = hub.y + Math.sin(a) * (Math.hypot(p.midX - hub.x, p.midY - hub.y) || 80);
+ } else if (mode === "constellation") {
+ // Celestial guide rings
+ for (let i = 1; i <= 4; i++) {
ctx.beginPath();
- ctx.moveTo(hub.x, hub.y);
- ctx.quadraticCurveTo(
- c1x + nx * p.width,
- c1y + ny * p.width,
- p.tipX,
- p.tipY
- );
- ctx.quadraticCurveTo(
- c1x - nx * p.width,
- c1y - ny * p.width,
- hub.x,
- hub.y
- );
- ctx.closePath();
- ctx.fillStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.06)`;
- ctx.fill();
- ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.28)`;
- ctx.lineWidth = 1.2;
+ ctx.arc(hub.x, hub.y, 55 * i * 0.55, 0, Math.PI * 2);
+ ctx.strokeStyle = canvasTheme().decor(i);
+ ctx.lineWidth = 1;
+ ctx.setLineDash(i % 2 ? [2, 6] : []);
ctx.stroke();
-
- // Spine
- ctx.beginPath();
- ctx.moveTo(hub.x, hub.y);
- ctx.lineTo(p.tipX, p.tipY);
- ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.35)`;
- ctx.lineWidth = 1.4;
- ctx.stroke();
-
- // Traveling bloom spark along spine
- const u = (t * 0.012 + (p.angle || 0)) % 1;
- const sx = hub.x + (p.tipX - hub.x) * u;
- const sy = hub.y + (p.tipY - hub.y) * u;
- ctx.beginPath();
- ctx.arc(sx, sy, 2.6, 0, Math.PI * 2);
- ctx.fillStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.85)`;
- ctx.fill();
+ ctx.setLineDash([]);
+ }
+ // Radial tick marks
+ for (let i = 0; i < 12; i++) {
+ const ang = (i / 12) * Math.PI * 2;
+ ctx.beginPath();
+ ctx.moveTo(hub.x + Math.cos(ang) * 36, hub.y + Math.sin(ang) * 36);
+ ctx.lineTo(hub.x + Math.cos(ang) * 48, hub.y + Math.sin(ang) * 48);
+ ctx.strokeStyle = canvasTheme().decor(2);
+ ctx.lineWidth = 1;
+ ctx.stroke();
+ }
+ for (const ch of state.constellationCharts || []) {
+ const rgb = hexToRgb(ch.color || "#00a8e8");
+ // Soft halo around constellation
+ ctx.beginPath();
+ ctx.arc(ch.x, ch.y, (ch.localR || 40) + 14, 0, Math.PI * 2);
+ ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.2)`;
+ ctx.lineWidth = 1;
+ ctx.stroke();
+ // Star-chart edges
+ for (const e of ch.edges || []) {
+ ctx.beginPath();
+ ctx.moveTo(e.x1, e.y1);
+ ctx.lineTo(e.x2, e.y2);
+ ctx.strokeStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.38)`;
+ ctx.lineWidth = 1;
+ ctx.stroke();
+ }
+ // Dim center marker
+ ctx.beginPath();
+ ctx.arc(ch.x, ch.y, 3, 0, Math.PI * 2);
+ ctx.fillStyle = `rgba(${rgb.r},${rgb.g},${rgb.b},0.55)`;
+ ctx.fill();
+ drawPulseLink(hub.x, hub.y, ch.x, ch.y, true, t, (ch.label || "").length * 19, 0.4);
}
- // Inner halo
- ctx.beginPath();
- ctx.arc(hub.x, hub.y, 42, 0, Math.PI * 2);
- ctx.strokeStyle = canvasTheme().decor(2);
- ctx.lineWidth = 1;
- ctx.stroke();
} else if (mode === "lanes") {
for (const g of state.laneGuides || []) {
ctx.beginPath();
@@ -902,15 +922,14 @@
}
return;
}
- if (mode === "bloom") {
- // Node → petal mid (spine), plus faint hub link
+ if (mode === "constellation") {
+ // Soft hub link to each star; constellation edges already in decor
for (const n of state.nodes) {
- const petal = (state.bloomPetals || [])[n.bloom];
- if (petal) {
- drawPulseLink(petal.midX, petal.midY, n.x, n.y, n.connected, t, n.id, 0.75);
- if (n.connected) drawPulseLink(hub.x, hub.y, petal.midX, petal.midY, true, t, n.id + 9000, 0.35);
+ const ch = (state.constellationCharts || [])[n.constellation];
+ if (ch) {
+ drawPulseLink(ch.x, ch.y, n.x, n.y, n.connected, t, n.id, 0.55);
} else {
- drawPulseLink(hub.x, hub.y, n.x, n.y, n.connected, t, n.id, 0.8);
+ drawPulseLink(hub.x, hub.y, n.x, n.y, n.connected, t, n.id, 0.7);
}
}
return;
@@ -1677,6 +1696,8 @@
}
if (state.hub) expand(state.hub.x, state.hub.y, (state.hub.r || 28) + 16);
for (const ch of state.clusterHubs || []) expand(ch.x, ch.y, 56);
+ for (const c of state.constellationCharts || []) expand(c.x, c.y, (c.localR || 40) + 24);
+ for (const a of state.labelAnchors || []) expand(a.x, a.y, 48);
const bw = Math.max(80, maxX - minX);
const bh = Math.max(80, maxY - minY);
diff --git a/ui/index.html b/ui/index.html
index 05d6d55..c8dc70d 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -7,7 +7,7 @@
-
+
-
+
diff --git a/ui/styles.css b/ui/styles.css
index 0b0dfed..19e3274 100644
--- a/ui/styles.css
+++ b/ui/styles.css
@@ -752,9 +752,6 @@ html[data-theme="light"] .inspector-body .kv-row .v { color: #122033; }
gap: 0.08rem;
min-height: 64px;
}
-.viz-card:nth-child(5) {
- grid-column: 1 / -1;
-}
.viz-card:hover {
border-color: var(--dell-bright);
color: var(--text);