feat: form and proxy call
This commit is contained in:
@@ -2,7 +2,8 @@ function renderGraph(containerSelector, graph, options = {}) {
|
||||
const container = document.querySelector(containerSelector);
|
||||
container.innerHTML = '';
|
||||
const width = container.clientWidth || 800;
|
||||
const height = container.clientHeight || 500;
|
||||
// Use viewport height as baseline; allow large canvases
|
||||
const height = Math.max(window.innerHeight - 100, container.clientHeight || 500, 600);
|
||||
const paddingX = 60;
|
||||
const paddingY = 30;
|
||||
|
||||
@@ -10,7 +11,7 @@ function renderGraph(containerSelector, graph, options = {}) {
|
||||
.append('svg')
|
||||
.attr('viewBox', [0, 0, width, height])
|
||||
.attr('width', '100%')
|
||||
.attr('height', '100%');
|
||||
.attr('height', height);
|
||||
|
||||
const g = svg.append('g');
|
||||
|
||||
@@ -71,15 +72,87 @@ function renderGraph(containerSelector, graph, options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
// Node hover -> highlight connected links
|
||||
node
|
||||
.on('mouseenter', (event, d) => {
|
||||
link.classed('highlighted', l => {
|
||||
const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
|
||||
const targetId = typeof l.target === 'object' ? l.target.id : l.target;
|
||||
return sourceId === d.id || targetId === d.id;
|
||||
});
|
||||
})
|
||||
.on('mouseleave', () => {
|
||||
link.classed('highlighted', false);
|
||||
});
|
||||
|
||||
// Build adjacency map for each node (which links it's part of)
|
||||
const nodeLinks = new Map();
|
||||
for (const n of nodes) {
|
||||
nodeLinks.set(n.id, new Set());
|
||||
}
|
||||
for (const l of links) {
|
||||
const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
|
||||
const targetId = typeof l.target === 'object' ? l.target.id : l.target;
|
||||
nodeLinks.get(sourceId)?.add(l);
|
||||
nodeLinks.get(targetId)?.add(l);
|
||||
}
|
||||
|
||||
const sim = simulation();
|
||||
sim.nodes(nodes).on('tick', ticked);
|
||||
sim.force('link').links(links);
|
||||
|
||||
function distanceToLineSegment(px, py, x1, y1, x2, y2) {
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const len2 = dx * dx + dy * dy;
|
||||
if (len2 === 0) {
|
||||
const dx2 = px - x1;
|
||||
const dy2 = py - y1;
|
||||
return Math.sqrt(dx2 * dx2 + dy2 * dy2);
|
||||
}
|
||||
const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / len2));
|
||||
const projX = x1 + t * dx;
|
||||
const projY = y1 + t * dy;
|
||||
const dx2 = px - projX;
|
||||
const dy2 = py - projY;
|
||||
return Math.sqrt(dx2 * dx2 + dy2 * dy2);
|
||||
}
|
||||
|
||||
function ticked() {
|
||||
// Clamp nodes vertically to avoid drifting too far down/up
|
||||
const minY = paddingY + nodeRadius;
|
||||
const maxY = height - paddingY - nodeRadius - labelOffset - 2;
|
||||
|
||||
// Push nodes away from links they don't belong to
|
||||
const minDistFromLink = nodeRadius + 8; // minimum distance from unrelated links
|
||||
for (const n of nodes) {
|
||||
const connectedLinks = nodeLinks.get(n.id) || new Set();
|
||||
for (const l of links) {
|
||||
if (connectedLinks.has(l)) continue; // skip links this node belongs to
|
||||
|
||||
const sourceId = typeof l.source === 'object' ? l.source.id : l.source;
|
||||
const targetId = typeof l.target === 'object' ? l.target.id : l.target;
|
||||
const sx = typeof l.source === 'object' ? l.source.x : (nodes.find(nn => nn.id === sourceId)?.x || 0);
|
||||
const sy = typeof l.source === 'object' ? l.source.y : (nodes.find(nn => nn.id === sourceId)?.y || 0);
|
||||
const tx = typeof l.target === 'object' ? l.target.x : (nodes.find(nn => nn.id === targetId)?.x || 0);
|
||||
const ty = typeof l.target === 'object' ? l.target.y : (nodes.find(nn => nn.id === targetId)?.y || 0);
|
||||
|
||||
const dist = distanceToLineSegment(n.x, n.y, sx, sy, tx, ty);
|
||||
if (dist < minDistFromLink) {
|
||||
// Push node perpendicular to the link
|
||||
const dx = tx - sx;
|
||||
const dy = ty - sy;
|
||||
const len = Math.sqrt(dx * dx + dy * dy);
|
||||
if (len > 0) {
|
||||
const perpX = -dy / len;
|
||||
const perpY = dx / len;
|
||||
const push = (minDistFromLink - dist) * 0.3;
|
||||
n.vx = (n.vx || 0) + perpX * push;
|
||||
n.vy = (n.vy || 0) + perpY * push;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (n.y < minY) n.y = minY;
|
||||
else if (n.y > maxY) n.y = maxY;
|
||||
}
|
||||
@@ -135,22 +208,32 @@ function renderGraph(containerSelector, graph, options = {}) {
|
||||
const nodeIds = new Set(nodes.map(n => n.id));
|
||||
const depth = new Map();
|
||||
let maxDepth = 0;
|
||||
|
||||
// Root "/" is always at depth 0
|
||||
if (nodeIds.has('/')) {
|
||||
depth.set('/', 0);
|
||||
}
|
||||
|
||||
for (const ep of endpoints) {
|
||||
const p = (ep && ep.path) || '';
|
||||
const segs = p.split('/').filter(Boolean);
|
||||
// Each segment is at depth i+1 (since root "/" is at depth 0)
|
||||
for (let i = 0; i < segs.length; i++) {
|
||||
const seg = segs[i];
|
||||
if (!nodeIds.has(seg)) continue;
|
||||
const segDepth = i + 1;
|
||||
const current = depth.get(seg);
|
||||
if (current === undefined || i < current) {
|
||||
depth.set(seg, i);
|
||||
if (current === undefined || segDepth < current) {
|
||||
depth.set(seg, segDepth);
|
||||
}
|
||||
if (i > maxDepth) maxDepth = i;
|
||||
if (segDepth > maxDepth) maxDepth = segDepth;
|
||||
}
|
||||
}
|
||||
// Any nodes not appearing in endpoints get depth 0
|
||||
// Any nodes not appearing in endpoints get depth 1 (after root) or 0 if it's the root
|
||||
for (const id of nodeIds) {
|
||||
if (!depth.has(id)) depth.set(id, 0);
|
||||
if (!depth.has(id)) {
|
||||
depth.set(id, id === '/' ? 0 : 1);
|
||||
}
|
||||
}
|
||||
return { depths: depth, maxDepth };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user