Ver Fonte

feat: 优化hover交互,隐藏无关节点和边

- hover时隐藏(而非灰化)不在路径上的边和节点
- 使用visibility:hidden保持布局不变
- 移除renderEgoGraphEdge中多余的SVG标题

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
yangxiaohui há 5 dias atrás
pai
commit
7032231c79
1 ficheiros alterados com 148 adições e 53 exclusões
  1. 148 53
      script/data_processing/visualize_match_graph.py

+ 148 - 53
script/data_processing/visualize_match_graph.py

@@ -96,7 +96,6 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
             width: 420px;
             display: flex;
             flex-direction: column;
-            background: #1a1a2e;
             border-right: 1px solid #0f3460;
         }}
         #tree-container {{
@@ -123,11 +122,22 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
             right: 5px;
             z-index: 10;
             display: flex;
-            gap: 6px;
+            justify-content: space-between;
             align-items: center;
-            flex-wrap: wrap;
             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 {{
             color: #8892b0;
             display: flex;
@@ -794,6 +804,8 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
                                 </div>
                             </div>
                         </div>
+                        <div class="ego-title-center" id="ego-node-name"></div>
+                        <div class="ego-stats" id="ego-stats"></div>
                     </div>
                 </div>
             </div>
@@ -1711,31 +1723,11 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
                 .attr("class", "ego-center")
                 .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")
                 .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树布局 - 宽度留出边距给标签
             const treeLayout = d3.tree()
                 .size([treeHeight - 50, treeAreaWidth - 70]);
@@ -1836,6 +1828,18 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
                     event.stopPropagation();
                     // 调用共享的节点点击处理函数
                     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);
             }}
 
-            // 点击树背景也取消高亮
-            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();
 
-            // 更新标题
-            d3.select(".ego-container .ego-title").text(`关系图: ${{centerNodeName}}`);
+            // 更新顶部标题和统计
+            document.getElementById("ego-node-name").textContent = centerNodeName;
 
             // 获取层半径
             const radius = 160;  // 固定半径
@@ -3094,15 +3099,8 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
                 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) {{
@@ -3280,6 +3278,16 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
             node.on("click", function(event, d) {{
                 event.stopPropagation();
                 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) {{
             // 显示关系图容器
@@ -3350,8 +3454,9 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
             // 清除旧内容
             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;
 
@@ -3380,16 +3485,6 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
                 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 tgtX = 60, tgtY = 0;