优化图谱渲染

main
Bifang 2026-06-15 17:50:47 +08:00
parent 43eeb9788c
commit 4dcd27180d
2 changed files with 82 additions and 0 deletions

View File

@ -143,6 +143,20 @@ function renderGraph(payload) {
.map((e) => ({ ...e, sourceNode: nodeById.get(e.source), targetNode: nodeById.get(e.target) }))
.filter((e) => e.sourceNode && e.targetNode);
// ── adjacency map for hover highlight ──
const nodeEdges = new Map(); // nodeId -> Set of edge indices
const nodeNeighbors = new Map(); // nodeId -> Set of neighbor nodeIds
dataNodes.forEach(n => {
nodeEdges.set(n.id, new Set());
nodeNeighbors.set(n.id, new Set());
});
dataEdges.forEach((e, i) => {
nodeEdges.get(e.source).add(i);
nodeEdges.get(e.target).add(i);
nodeNeighbors.get(e.source).add(e.target);
nodeNeighbors.get(e.target).add(e.source);
});
const n = dataNodes.length;
const area = svgW * svgH;
const repulsionStr = Math.max(3000, area / Math.max(n, 1));
@ -203,9 +217,45 @@ function renderGraph(payload) {
text.textContent = truncate(node.label, TRUNCATE_LENGTH);
g.appendChild(text);
mainGroup.appendChild(g);
// ── hover highlight ──
g.addEventListener('mouseenter', () => applyFocus(node));
g.addEventListener('mouseleave', () => { if (!isDragging) clearFocus(); });
return g;
});
// ── focus / dim helpers ──
function applyFocus(hoveredNode) {
const connectedEdges = nodeEdges.get(hoveredNode.id);
const connectedNodes = nodeNeighbors.get(hoveredNode.id);
dataNodes.forEach((n, idx) => {
const el = nodeEls[idx];
if (!el) return;
const isTarget = n.id === hoveredNode.id || (connectedNodes && connectedNodes.has(n.id));
el.classList.toggle('graph-dim', !isTarget);
el.classList.toggle('graph-focus', isTarget);
});
dataEdges.forEach((e, idx) => {
const el = edgeEls[idx];
if (!el) return;
const isConnected = connectedEdges && connectedEdges.has(idx);
el.classList.toggle('graph-dim', !isConnected);
el.classList.toggle('graph-focus', isConnected);
});
}
function clearFocus() {
dataNodes.forEach((n, idx) => {
const el = nodeEls[idx];
if (el) { el.classList.remove('graph-dim', 'graph-focus'); }
});
dataEdges.forEach((e, idx) => {
const el = edgeEls[idx];
if (el) { el.classList.remove('graph-dim', 'graph-focus'); }
});
}
function syncDom() {
for (let i = 0; i < dataEdges.length; i++) {
const edge = dataEdges[i], el = edgeEls[i];
@ -330,6 +380,7 @@ function renderGraph(payload) {
dragOccurred = false;
dragNode = dataNodes.find((n) => n.id === target.dataset.nodeId);
if (dragNode) {
applyFocus(dragNode);
const r = graphSvg.getBoundingClientRect();
dragOffX = (e.clientX - r.left - zoomTransform.x) / zoomTransform.k - dragNode.x;
dragOffY = (e.clientY - r.top - zoomTransform.y) / zoomTransform.k - dragNode.y;
@ -367,6 +418,7 @@ function renderGraph(payload) {
if (el) el.style.cursor = "grab";
dragNode.vx = 0;
dragNode.vy = 0;
clearFocus();
wakeSim();
}
isDragging = false;

View File

@ -862,6 +862,17 @@ textarea {
box-shadow: var(--shadow-lg);
}
.detail-modal {
border: none;
border-radius: 12px;
padding: 0;
width: min(640px, 90vw);
max-height: 80vh;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
inset: 0;
margin: auto;
}
.detail-modal::backdrop {
background: rgba(30, 41, 59, 0.3);
}
@ -1143,6 +1154,25 @@ textarea {
.graph-node:hover circle { opacity: 0.8; }
/* ── hover / drag dim + focus ── */
.graph-dim { opacity: 0.2 !important; transition: opacity 0.2s; }
.edge-wrap { transition: opacity 0.2s; }
.graph-focus circle {
filter: drop-shadow(0 0 8px rgba(6, 182, 212, 0.5));
}
.graph-focus text[data-type="node-label"] {
font-weight: 700;
}
.edge-wrap.graph-focus .graph-edge {
stroke: var(--gray-500);
stroke-width: 2.5;
}
.edge-wrap.graph-focus text[data-type="edge-label"] {
fill: var(--gray-700);
font-weight: 600;
}
.graph-node text {
font-size: 11px;
font-weight: 500;