feat: new day new graph

This commit is contained in:
2026-04-06 22:27:02 +02:00
parent d8abe311cd
commit 46185705d7
10 changed files with 1018 additions and 774 deletions

View File

@@ -1,101 +1,106 @@
import type { NetworkNode } from "@/api/client";
import { statusRGBA, type RGBA, type StatusColors } from "./graphColors";
import { statusRGBA, rgbaToHex, type StatusColors } from "./graphColors";
export const SPACE_SIZE = 4096;
const CENTER = SPACE_SIZE / 2;
export interface GraphNode {
id: string;
name: string;
entry: NetworkNode;
type: "self" | "peer" | "interface";
color: string;
size: number;
cluster?: string;
x?: number;
y?: number;
z?: number;
}
export interface BuiltGraph {
entries: NetworkNode[];
positions: Float32Array;
colors: Float32Array;
sizes: Float32Array;
clusterIndices: (number | undefined)[];
clusterPositions: (number | undefined)[];
clusterStrength: Float32Array;
export interface GraphLink {
source: string;
target: string;
}
export interface GraphData {
nodes: GraphNode[];
links: GraphLink[];
clusterNames: string[];
hashToIndex: Map<string, number>;
}
function findParentIface(
entry: NetworkNode,
interfaces: NetworkNode[],
peerIndex: number,
): string | undefined {
): NetworkNode | undefined {
if (entry.interface) {
const iface = interfaces.find(i => i.name === entry.interface);
if (iface) return iface.hash;
if (iface) return iface;
}
if (interfaces.length > 0) return interfaces[peerIndex % interfaces.length].hash;
if (interfaces.length > 0) return interfaces[peerIndex % interfaces.length];
return undefined;
}
export function buildGraphArrays(
export function buildGraphData(
rawNodes: NetworkNode[],
prevHashToIndex: Map<string, number>,
prevPositions: number[],
prevPositions: Map<string, { x: number; y: number; z: number }>,
theme: StatusColors,
): BuiltGraph {
const interfaces = rawNodes.filter(e => e.type === "interface").sort((a, b) => a.name.localeCompare(b.name));
): GraphData {
const interfaces = rawNodes
.filter(e => e.type === "interface")
.sort((a, b) => a.name.localeCompare(b.name));
const selfNode = rawNodes.find(e => e.is_self && e.type !== "interface");
const peers = rawNodes.filter(e => !e.is_self && e.type !== "interface");
const entries = selfNode ? [selfNode, ...peers] : peers;
const selfIndex = selfNode ? 0 : -1;
const n = entries.length;
const nClusters = interfaces.length;
const hashToIndex = new Map<string, number>();
const positions = new Float32Array(n * 2);
const colors = new Float32Array(n * 4);
const sizes = new Float32Array(n);
const clusterIndices: (number | undefined)[] = [];
const clusterStrength = new Float32Array(n);
const nodes: GraphNode[] = [];
const links: GraphLink[] = [];
const clusterMap = new Map<string, number>();
interfaces.forEach((iface, i) => clusterMap.set(iface.name, i));
const selfClusterIndex = interfaces.length;
// Add interface nodes
for (const iface of interfaces) {
const prev = prevPositions.get(iface.hash);
nodes.push({
id: iface.hash,
name: iface.name,
entry: iface,
type: "interface",
color: rgbaToHex(theme.stale),
size: 2,
...(prev ? { x: prev.x, y: prev.y, z: prev.z } : {}),
});
}
const clusterNames = [...interfaces.map(i => i.name), ...(selfNode ? [selfNode.name] : [])];
const clusterPositions: (number | undefined)[] = [];
// Add self node
if (selfNode) {
const prev = prevPositions.get(selfNode.hash);
nodes.push({
id: selfNode.hash,
name: selfNode.name,
entry: selfNode,
type: "self",
color: "#ffffff",
size: 2,
...(prev ? { x: prev.x, y: prev.y, z: prev.z } : {}),
});
}
entries.forEach((entry, i) => {
hashToIndex.set(entry.hash, i);
// Add peer nodes + links to parent interface
peers.forEach((peer, i) => {
const prev = prevPositions.get(peer.hash);
const parentIface = findParentIface(peer, interfaces, i);
nodes.push({
id: peer.hash,
name: peer.name,
entry: peer,
type: "peer",
color: rgbaToHex(statusRGBA(peer, theme)),
size: 2,
cluster: parentIface?.name,
...(prev ? { x: prev.x, y: prev.y, z: prev.z } : {}),
});
// Preserve existing position, or jitter near center for new points
const prevIdx = prevHashToIndex.get(entry.hash);
if (prevIdx !== undefined && prevPositions.length >= (prevIdx + 1) * 2) {
positions[i * 2] = prevPositions[prevIdx * 2]!;
positions[i * 2 + 1] = prevPositions[prevIdx * 2 + 1]!;
} else {
// Tighter initial spread for small graphs so they don't scatter
const spread = n <= 20 ? 0.1 : n <= 100 ? 0.25 : 0.5;
positions[i * 2] = CENTER + (Math.random() - 0.5) * SPACE_SIZE * spread;
positions[i * 2 + 1] = CENTER + (Math.random() - 0.5) * SPACE_SIZE * spread;
if (parentIface) {
links.push({ source: parentIface.hash, target: peer.hash });
}
// Self node = bright white, others by status
const rgba: RGBA = i === selfIndex
? [1, 1, 1, 1]
: statusRGBA(entry, theme);
colors[i * 4 + 0] = rgba[0];
colors[i * 4 + 1] = rgba[1];
colors[i * 4 + 2] = rgba[2];
colors[i * 4 + 3] = rgba[3];
// Scale node size inversely with count: bigger dots when fewer nodes
sizes[i] = n <= 10 ? 8 : n <= 50 ? 5 : n <= 200 ? 4 : 3;
// Cluster assignment
if (i === selfIndex) {
clusterIndices.push(selfClusterIndex);
} else {
const pi = peers.indexOf(entry);
const parentHash = findParentIface(entry, interfaces, pi);
const parentName = parentHash ? interfaces.find(f => f.hash === parentHash)?.name : undefined;
clusterIndices.push(parentName !== undefined ? clusterMap.get(parentName) : undefined);
}
clusterStrength[i] = nClusters > 1 ? (nClusters - (i % nClusters)) / nClusters : 1;
});
return { entries, positions, colors, sizes, clusterIndices, clusterPositions, clusterStrength, clusterNames, hashToIndex };
const clusterNames = interfaces.map(i => i.name);
return { nodes, links, clusterNames };
}