Kaynağa Gözat

feat: 为右边跨层边添加hover和点击交互

- 跨层边添加热区和hover效果(变粗变亮)
- 点击跨层边触发左边圆高亮并联动右边树
- 移除帖子树内部边的点击事件(不需要)

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

Co-Authored-By: Claude <noreply@anthropic.com>
yangxiaohui 4 gün önce
ebeveyn
işleme
f8132f10fd
1 değiştirilmiş dosya ile 89 ekleme ve 28 silme
  1. 89 28
      script/data_processing/visualize_match_graph.py

+ 89 - 28
script/data_processing/visualize_match_graph.py

@@ -2119,19 +2119,22 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
                     return path + "Z";
                 }}
 
-                // 绘制树的边(帖子→维度→点→标签)
+                // 树边路径生成函数
+                const treeLinkPath = d => {{
+                    const sourceY = d.source.data.isRoot ? d.source.y + cardHeight/2 : d.source.y;
+                    return `M${{d.source.x}},${{sourceY}}
+                            C${{d.source.x}},${{(sourceY + d.target.y) / 2}}
+                             ${{d.target.x}},${{(sourceY + d.target.y) / 2}}
+                             ${{d.target.x}},${{d.target.y}}`;
+                }};
+
+                // 绘制树的边(帖子→维度→点→标签)- 无点击事件
                 const treeLinks = postTreeGroup.selectAll(".tree-link")
                     .data(postRoot.links())
                     .enter()
                     .append("path")
                     .attr("class", "tree-link")
-                    .attr("d", d => {{
-                        const sourceY = d.source.data.isRoot ? d.source.y + cardHeight/2 : d.source.y;
-                        return `M${{d.source.x}},${{sourceY}}
-                                C${{d.source.x}},${{(sourceY + d.target.y) / 2}}
-                                 ${{d.target.x}},${{(sourceY + d.target.y) / 2}}
-                                 ${{d.target.x}},${{d.target.y}}`;
-                    }})
+                    .attr("d", treeLinkPath)
                     .attr("fill", "none")
                     .attr("stroke", "#9b59b6")
                     .attr("stroke-width", 1.5)
@@ -2562,33 +2565,91 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
                     .append("g")
                     .attr("class", "graph-link-group");
 
-                graphLinks.append("path")
-                    .attr("class", "graph-link")
-                    .attr("d", d => {{
-                        const src = d.srcNode;
-                        const tgt = d.tgtNode;
-                        if (!src || !tgt) return "";
-
-                        // 如果只有一条边,用默认曲线
-                        if (d._pairCount <= 1) {{
-                            return `M${{src.x}},${{src.y}} C${{src.x}},${{(src.y + tgt.y) / 2}} ${{tgt.x}},${{(src.y + tgt.y) / 2}} ${{tgt.x}},${{tgt.y}}`;
-                        }}
+                // 跨层边路径生成函数
+                const graphLinkPath = d => {{
+                    const src = d.srcNode;
+                    const tgt = d.tgtNode;
+                    if (!src || !tgt) return "";
+
+                    // 如果只有一条边,用默认曲线
+                    if (d._pairCount <= 1) {{
+                        return `M${{src.x}},${{src.y}} C${{src.x}},${{(src.y + tgt.y) / 2}} ${{tgt.x}},${{(src.y + tgt.y) / 2}} ${{tgt.x}},${{tgt.y}}`;
+                    }}
 
-                        // 多条边时,根据索引计算水平偏移
-                        const offsetStep = 25;
-                        const totalOffset = (d._pairCount - 1) * offsetStep;
-                        const offset = d._pairIndex * offsetStep - totalOffset / 2;
+                    // 多条边时,根据索引计算水平偏移
+                    const offsetStep = 25;
+                    const totalOffset = (d._pairCount - 1) * offsetStep;
+                    const offset = d._pairIndex * offsetStep - totalOffset / 2;
 
-                        const midY = (src.y + tgt.y) / 2;
-                        const midX = (src.x + tgt.x) / 2 + offset;
+                    const midY = (src.y + tgt.y) / 2;
+                    const midX = (src.x + tgt.x) / 2 + offset;
 
-                        return `M${{src.x}},${{src.y}} Q${{midX}},${{midY}} ${{tgt.x}},${{tgt.y}}`;
-                    }})
+                    return `M${{src.x}},${{src.y}} Q${{midX}},${{midY}} ${{tgt.x}},${{tgt.y}}`;
+                }};
+
+                // 可见的跨层边
+                graphLinks.append("path")
+                    .attr("class", "graph-link")
+                    .attr("d", graphLinkPath)
                     .attr("fill", "none")
                     .attr("stroke", d => graphEdgeColors[d.type] || "#9b59b6")
                     .attr("stroke-width", 1.5)
                     .attr("stroke-opacity", 0.6)
-                    .attr("stroke-dasharray", d => d.type === "匹配_相似" ? "4,2" : "none");
+                    .attr("stroke-dasharray", d => d.type === "匹配_相似" ? "4,2" : "none")
+                    .style("pointer-events", "none");  // 让事件穿透到热区
+
+                // 跨层边热区(方便点击)
+                graphLinks.append("path")
+                    .attr("class", "graph-link-hitarea")
+                    .attr("d", graphLinkPath)
+                    .attr("fill", "none")
+                    .attr("stroke", "transparent")
+                    .attr("stroke-width", 15)
+                    .style("cursor", "pointer")
+                    .on("mouseover", function(event, d) {{
+                        // 高亮对应的可见边
+                        d3.select(this.parentNode).select(".graph-link")
+                            .attr("stroke-width", 3)
+                            .attr("stroke-opacity", 1);
+                    }})
+                    .on("mouseout", function(event, d) {{
+                        // 恢复边样式
+                        d3.select(this.parentNode).select(".graph-link")
+                            .attr("stroke-width", 1.5)
+                            .attr("stroke-opacity", 0.6);
+                    }})
+                    .on("click", function(event, d) {{
+                        event.stopPropagation();
+                        // 获取边两端节点ID
+                        const srcId = d.source ? (d.source.id || d.source) : null;
+                        const tgtId = d.target ? (d.target.id || d.target) : null;
+                        if (!srcId || !tgtId) return;
+
+                        // 在左边圆中找对应的节点,触发点击
+                        const leftNode = nodes.find(n => n.id === srcId || n.id === tgtId);
+                        if (leftNode) {{
+                            highlightNode(leftNode);
+                            showNodeInfo(leftNode);
+                            if (leftNode.source === "人设") {{
+                                handleNodeClick(leftNode.id, leftNode.节点名称);
+                            }} else {{
+                                // 帖子标签节点
+                                const highlightedNodeIds = new Set([srcId, tgtId]);
+                                const highlightedEdgeKeys = new Set([`${{srcId}}|${{tgtId}}`, `${{tgtId}}|${{srcId}}`]);
+                                g.selectAll(".link-group").each(function(link) {{
+                                    const lSrcId = typeof link.source === "object" ? link.source.id : link.source;
+                                    const lTgtId = typeof link.target === "object" ? link.target.id : link.target;
+                                    if (highlightedNodeIds.has(lSrcId) || highlightedNodeIds.has(lTgtId)) {{
+                                        highlightedNodeIds.add(lSrcId);
+                                        highlightedNodeIds.add(lTgtId);
+                                        highlightedEdgeKeys.add(`${{lSrcId}}|${{lTgtId}}`);
+                                        highlightedEdgeKeys.add(`${{lTgtId}}|${{lSrcId}}`);
+                                    }}
+                                }});
+                                highlightRightTree(highlightedNodeIds, highlightedEdgeKeys);
+                            }}
+                        }}
+                    }});
 
                 // 在匹配边上显示相似度
                 graphLinks.filter(d => d.type && d.type.startsWith("匹配_") && d.similarity > 0)