|
@@ -96,7 +96,6 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
width: 420px;
|
|
width: 420px;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
- background: #1a1a2e;
|
|
|
|
|
border-right: 1px solid #0f3460;
|
|
border-right: 1px solid #0f3460;
|
|
|
}}
|
|
}}
|
|
|
#tree-container {{
|
|
#tree-container {{
|
|
@@ -123,11 +122,22 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
right: 5px;
|
|
right: 5px;
|
|
|
z-index: 10;
|
|
z-index: 10;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- gap: 6px;
|
|
|
|
|
|
|
+ justify-content: space-between;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- flex-wrap: wrap;
|
|
|
|
|
font-size: 10px;
|
|
font-size: 10px;
|
|
|
}}
|
|
}}
|
|
|
|
|
+ .ego-title-center {{
|
|
|
|
|
+ color: #e94560;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ }}
|
|
|
|
|
+ .ego-stats {{
|
|
|
|
|
+ color: #8892b0;
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }}
|
|
|
.ego-controls label {{
|
|
.ego-controls label {{
|
|
|
color: #8892b0;
|
|
color: #8892b0;
|
|
|
display: flex;
|
|
display: flex;
|
|
@@ -794,6 +804,8 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div class="ego-title-center" id="ego-node-name"></div>
|
|
|
|
|
+ <div class="ego-stats" id="ego-stats"></div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -1711,31 +1723,11 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
.attr("class", "ego-center")
|
|
.attr("class", "ego-center")
|
|
|
.attr("transform", `translate(200, 210)`);
|
|
.attr("transform", `translate(200, 210)`);
|
|
|
|
|
|
|
|
- // 关系图标题
|
|
|
|
|
- egoCenterGroup.append("text")
|
|
|
|
|
- .attr("class", "ego-title")
|
|
|
|
|
- .attr("y", -egoDisplayRadius - 10)
|
|
|
|
|
- .attr("text-anchor", "middle")
|
|
|
|
|
- .attr("fill", "rgba(255,255,255,0.5)")
|
|
|
|
|
- .attr("font-size", "13px")
|
|
|
|
|
- .attr("font-weight", "bold")
|
|
|
|
|
- .text("关系图");
|
|
|
|
|
|
|
|
|
|
// 关系图内容组
|
|
// 关系图内容组
|
|
|
egoCenterGroup.append("g")
|
|
egoCenterGroup.append("g")
|
|
|
.attr("class", "ego-graph-content");
|
|
.attr("class", "ego-graph-content");
|
|
|
|
|
|
|
|
- // 绘制背景矩形
|
|
|
|
|
- treeGroup.append("rect")
|
|
|
|
|
- .attr("x", -5)
|
|
|
|
|
- .attr("y", -10)
|
|
|
|
|
- .attr("width", treeAreaWidth - 20)
|
|
|
|
|
- .attr("height", treeHeight - 20)
|
|
|
|
|
- .attr("rx", 8)
|
|
|
|
|
- .attr("fill", "rgba(100, 100, 100, 0.08)")
|
|
|
|
|
- .attr("stroke", "rgba(150, 150, 150, 0.2)")
|
|
|
|
|
- .attr("stroke-width", 1);
|
|
|
|
|
-
|
|
|
|
|
// D3树布局 - 宽度留出边距给标签
|
|
// D3树布局 - 宽度留出边距给标签
|
|
|
const treeLayout = d3.tree()
|
|
const treeLayout = d3.tree()
|
|
|
.size([treeHeight - 50, treeAreaWidth - 70]);
|
|
.size([treeHeight - 50, treeAreaWidth - 70]);
|
|
@@ -1836,6 +1828,18 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
event.stopPropagation();
|
|
event.stopPropagation();
|
|
|
// 调用共享的节点点击处理函数
|
|
// 调用共享的节点点击处理函数
|
|
|
handleNodeClick(d.data.节点ID, d.data.节点名称 || d.data.name);
|
|
handleNodeClick(d.data.节点ID, d.data.节点名称 || d.data.name);
|
|
|
|
|
+ }})
|
|
|
|
|
+ .on("mouseenter", (event, d) => {{
|
|
|
|
|
+ // 悬停高亮:在关系图中高亮到激活节点的路径
|
|
|
|
|
+ if (!currentEgoCenterNodeId) return;
|
|
|
|
|
+ const hoveredNodeId = d.data.节点ID;
|
|
|
|
|
+ if (!hoveredNodeId || hoveredNodeId === currentEgoCenterNodeId) return;
|
|
|
|
|
+ highlightPathInEgoGraph(hoveredNodeId);
|
|
|
|
|
+ }})
|
|
|
|
|
+ .on("mouseleave", (event, d) => {{
|
|
|
|
|
+ // 移出时恢复关系图
|
|
|
|
|
+ if (!currentEgoCenterNodeId) return;
|
|
|
|
|
+ resetEgoGraphHighlight();
|
|
|
}});
|
|
}});
|
|
|
|
|
|
|
|
// 节点形状:根节点/维度=圆形,分类=正方形,标签=圆形
|
|
// 节点形状:根节点/维度=圆形,分类=正方形,标签=圆形
|
|
@@ -1910,13 +1914,14 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
g.selectAll(".link-group").classed("dimmed", false).classed("highlighted", false);
|
|
g.selectAll(".link-group").classed("dimmed", false).classed("highlighted", false);
|
|
|
}}
|
|
}}
|
|
|
|
|
|
|
|
- // 点击树背景也取消高亮
|
|
|
|
|
- treeGroup.select("rect").on("click", function(event) {{
|
|
|
|
|
- event.stopPropagation();
|
|
|
|
|
- resetTreeHighlight();
|
|
|
|
|
- resetGraphHighlight();
|
|
|
|
|
- clearEgoGraph();
|
|
|
|
|
- closeDetailPanel();
|
|
|
|
|
|
|
+ // 点击树SVG背景取消高亮
|
|
|
|
|
+ treeSvg.on("click", function(event) {{
|
|
|
|
|
+ if (event.target === this) {{
|
|
|
|
|
+ resetTreeHighlight();
|
|
|
|
|
+ resetGraphHighlight();
|
|
|
|
|
+ clearEgoGraph();
|
|
|
|
|
+ closeDetailPanel();
|
|
|
|
|
+ }}
|
|
|
}});
|
|
}});
|
|
|
|
|
|
|
|
// 创建边的容器
|
|
// 创建边的容器
|
|
@@ -3026,8 +3031,8 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
// 获取当前层级配置
|
|
// 获取当前层级配置
|
|
|
const levelConfigs = getLevelConfigs();
|
|
const levelConfigs = getLevelConfigs();
|
|
|
|
|
|
|
|
- // 更新标题
|
|
|
|
|
- d3.select(".ego-container .ego-title").text(`关系图: ${{centerNodeName}}`);
|
|
|
|
|
|
|
+ // 更新顶部标题和统计
|
|
|
|
|
+ document.getElementById("ego-node-name").textContent = centerNodeName;
|
|
|
|
|
|
|
|
// 获取层半径
|
|
// 获取层半径
|
|
|
const radius = 160; // 固定半径
|
|
const radius = 160; // 固定半径
|
|
@@ -3094,15 +3099,8 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
actualRadius = Math.max(minEgoRadius, Math.min(maxEgoRadius, calcR));
|
|
actualRadius = Math.max(minEgoRadius, Math.min(maxEgoRadius, calcR));
|
|
|
}}
|
|
}}
|
|
|
|
|
|
|
|
- // 显示节点名称作为标题(包含层级和节点数)
|
|
|
|
|
- egoGroup.append("text")
|
|
|
|
|
- .attr("class", "ego-title")
|
|
|
|
|
- .attr("y", -actualRadius - 15)
|
|
|
|
|
- .attr("text-anchor", "middle")
|
|
|
|
|
- .attr("fill", "#e94560")
|
|
|
|
|
- .attr("font-size", "12px")
|
|
|
|
|
- .attr("font-weight", "bold")
|
|
|
|
|
- .text(`${{centerNodeName}} (${{depth}}级, ${{nodeCount}}节点, ${{relatedEdges.length}}边)`);
|
|
|
|
|
|
|
+ // 更新顶部统计信息
|
|
|
|
|
+ document.getElementById("ego-stats").textContent = `${{nodeCount}}节点 ${{relatedEdges.length}}边`;
|
|
|
|
|
|
|
|
// 如果只有中心节点,只渲染中心节点
|
|
// 如果只有中心节点,只渲染中心节点
|
|
|
if (nodes.length === 1) {{
|
|
if (nodes.length === 1) {{
|
|
@@ -3280,6 +3278,16 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
node.on("click", function(event, d) {{
|
|
node.on("click", function(event, d) {{
|
|
|
event.stopPropagation();
|
|
event.stopPropagation();
|
|
|
handleNodeClick(d.id, d.name);
|
|
handleNodeClick(d.id, d.name);
|
|
|
|
|
+ }})
|
|
|
|
|
+ .on("mouseenter", function(event, d) {{
|
|
|
|
|
+ // 悬停高亮:高亮到激活节点的路径
|
|
|
|
|
+ if (!currentEgoCenterNodeId || d.id === currentEgoCenterNodeId) return;
|
|
|
|
|
+ highlightPathInEgoGraph(d.id);
|
|
|
|
|
+ }})
|
|
|
|
|
+ .on("mouseleave", function(event, d) {{
|
|
|
|
|
+ // 移出时恢复
|
|
|
|
|
+ if (!currentEgoCenterNodeId) return;
|
|
|
|
|
+ resetEgoGraphHighlight();
|
|
|
}});
|
|
}});
|
|
|
|
|
|
|
|
// 边点击事件(直接调用共享函数)
|
|
// 边点击事件(直接调用共享函数)
|
|
@@ -3333,6 +3341,102 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
}}
|
|
}}
|
|
|
}}
|
|
}}
|
|
|
|
|
|
|
|
|
|
+ // 在关系图中高亮从悬停节点到激活节点的路径
|
|
|
|
|
+ function highlightPathInEgoGraph(hoveredNodeId) {{
|
|
|
|
|
+ const egoGroup = d3.select(".ego-graph-content");
|
|
|
|
|
+ if (egoGroup.empty()) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 获取关系图中的边数据
|
|
|
|
|
+ const egoEdges = egoGroup.selectAll(".ego-edge");
|
|
|
|
|
+ const egoNodes = egoGroup.selectAll(".ego-node");
|
|
|
|
|
+ if (egoEdges.empty()) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 构建邻接表用于路径查找
|
|
|
|
|
+ const adjacency = {{}};
|
|
|
|
|
+ const edgeMap = {{}};
|
|
|
|
|
+ egoEdges.each(function(d) {{
|
|
|
|
|
+ const srcId = d.source.id || d.source;
|
|
|
|
|
+ const tgtId = d.target.id || d.target;
|
|
|
|
|
+ if (!adjacency[srcId]) adjacency[srcId] = [];
|
|
|
|
|
+ if (!adjacency[tgtId]) adjacency[tgtId] = [];
|
|
|
|
|
+ adjacency[srcId].push(tgtId);
|
|
|
|
|
+ adjacency[tgtId].push(srcId);
|
|
|
|
|
+ // 记录边(双向)
|
|
|
|
|
+ const key1 = `${{srcId}}|${{tgtId}}`;
|
|
|
|
|
+ const key2 = `${{tgtId}}|${{srcId}}`;
|
|
|
|
|
+ edgeMap[key1] = d;
|
|
|
|
|
+ edgeMap[key2] = d;
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // BFS找路径
|
|
|
|
|
+ const visited = new Set();
|
|
|
|
|
+ const parent = {{}};
|
|
|
|
|
+ const queue = [currentEgoCenterNodeId];
|
|
|
|
|
+ visited.add(currentEgoCenterNodeId);
|
|
|
|
|
+ let found = false;
|
|
|
|
|
+
|
|
|
|
|
+ while (queue.length > 0 && !found) {{
|
|
|
|
|
+ const curr = queue.shift();
|
|
|
|
|
+ const neighbors = adjacency[curr] || [];
|
|
|
|
|
+ for (const next of neighbors) {{
|
|
|
|
|
+ if (!visited.has(next)) {{
|
|
|
|
|
+ visited.add(next);
|
|
|
|
|
+ parent[next] = curr;
|
|
|
|
|
+ queue.push(next);
|
|
|
|
|
+ if (next === hoveredNodeId) {{
|
|
|
|
|
+ found = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ if (!found) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 回溯路径
|
|
|
|
|
+ const pathNodes = new Set();
|
|
|
|
|
+ const pathEdgeKeys = new Set();
|
|
|
|
|
+ let curr = hoveredNodeId;
|
|
|
|
|
+ while (curr !== undefined) {{
|
|
|
|
|
+ pathNodes.add(curr);
|
|
|
|
|
+ const prev = parent[curr];
|
|
|
|
|
+ if (prev !== undefined) {{
|
|
|
|
|
+ pathEdgeKeys.add(`${{prev}}|${{curr}}`);
|
|
|
|
|
+ pathEdgeKeys.add(`${{curr}}|${{prev}}`);
|
|
|
|
|
+ }}
|
|
|
|
|
+ curr = prev;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 隐藏不在路径上的边(保持布局不变)
|
|
|
|
|
+ egoEdges
|
|
|
|
|
+ .style("visibility", function(d) {{
|
|
|
|
|
+ const srcId = d.source.id || d.source;
|
|
|
|
|
+ const tgtId = d.target.id || d.target;
|
|
|
|
|
+ const key = `${{srcId}}|${{tgtId}}`;
|
|
|
|
|
+ return pathEdgeKeys.has(key) ? "visible" : "hidden";
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 隐藏不在路径上的节点(保持布局不变)
|
|
|
|
|
+ egoNodes
|
|
|
|
|
+ .style("visibility", function(d) {{
|
|
|
|
|
+ return pathNodes.has(d.id) ? "visible" : "hidden";
|
|
|
|
|
+ }});
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ // 恢复关系图的高亮状态
|
|
|
|
|
+ function resetEgoGraphHighlight() {{
|
|
|
|
|
+ const egoGroup = d3.select(".ego-graph-content");
|
|
|
|
|
+ if (egoGroup.empty()) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 恢复边的可见性
|
|
|
|
|
+ egoGroup.selectAll(".ego-edge")
|
|
|
|
|
+ .style("visibility", "visible");
|
|
|
|
|
+
|
|
|
|
|
+ // 恢复节点的可见性
|
|
|
|
|
+ egoGroup.selectAll(".ego-node")
|
|
|
|
|
+ .style("visibility", "visible");
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
// 渲染单条边和两个节点(点击树边时调用)
|
|
// 渲染单条边和两个节点(点击树边时调用)
|
|
|
function renderEgoGraphEdge(edgeData, sourceNode, targetNode) {{
|
|
function renderEgoGraphEdge(edgeData, sourceNode, targetNode) {{
|
|
|
// 显示关系图容器
|
|
// 显示关系图容器
|
|
@@ -3350,8 +3454,9 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
// 清除旧内容
|
|
// 清除旧内容
|
|
|
egoGroup.selectAll("*").remove();
|
|
egoGroup.selectAll("*").remove();
|
|
|
|
|
|
|
|
- // 更新标题
|
|
|
|
|
- d3.select(".ego-container .ego-title").text(`关系图: ${{edgeData.边类型}}`);
|
|
|
|
|
|
|
+ // 更新顶部标题
|
|
|
|
|
+ document.getElementById("ego-node-name").textContent = edgeData.边类型;
|
|
|
|
|
+ document.getElementById("ego-stats").textContent = "2节点 1边";
|
|
|
|
|
|
|
|
const radius = 160;
|
|
const radius = 160;
|
|
|
|
|
|
|
@@ -3380,16 +3485,6 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
return "#888";
|
|
return "#888";
|
|
|
}}
|
|
}}
|
|
|
|
|
|
|
|
- // 标题
|
|
|
|
|
- egoGroup.append("text")
|
|
|
|
|
- .attr("class", "ego-title")
|
|
|
|
|
- .attr("y", -80)
|
|
|
|
|
- .attr("text-anchor", "middle")
|
|
|
|
|
- .attr("fill", edgeColors[edgeData.边类型] || "#666")
|
|
|
|
|
- .attr("font-size", "12px")
|
|
|
|
|
- .attr("font-weight", "bold")
|
|
|
|
|
- .text(`${{edgeData.边类型}}`);
|
|
|
|
|
-
|
|
|
|
|
// 两个节点的位置
|
|
// 两个节点的位置
|
|
|
const srcX = -60, srcY = 0;
|
|
const srcX = -60, srcY = 0;
|
|
|
const tgtX = 60, tgtY = 0;
|
|
const tgtX = 60, tgtY = 0;
|