Просмотр исходного кода

feat: 添加层间排斥力,保持四层节点分离

- 新增 forceLayerSeparation 自定义力函数
- 不同层的节点如果Y距离小于阈值(12%高度)则相互排斥
- 根据节点层级编号(0-3)判断排斥方向
- 确保四层结构清晰分离不重叠

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

Co-Authored-By: Claude <noreply@anthropic.com>
yangxiaohui 6 дней назад
Родитель
Сommit
13114f5c37
1 измененных файлов с 59 добавлено и 2 удалено
  1. 59 2
      script/data_processing/visualize_match_graph.py

+ 59 - 2
script/data_processing/visualize_match_graph.py

@@ -756,6 +756,14 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
                 return baseY + tilt;
             }}
 
+            // 获取节点的层级编号(用于层间排斥)
+            function getNodeLayer(d) {{
+                if (d.source === "帖子") {{
+                    return d.节点类型 === "点" ? 0 : 1;  // 点=0, 标签=1
+                }}
+                return d.是否扩展 ? 3 : 2;  // 人设标签=2, 分类=3
+            }}
+
             // 获取节点的基准Y(四层布局)
             function getNodeBaseY(d) {{
                 if (d.source === "帖子") {{
@@ -767,6 +775,53 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
                 return personaBaseY;
             }}
 
+            // 自定义层间排斥力
+            function forceLayerSeparation(strength) {{
+                let nodes;
+                const layerGap = height * 0.12;  // 最小层间距
+
+                function force(alpha) {{
+                    for (let i = 0; i < nodes.length; i++) {{
+                        const nodeA = nodes[i];
+                        const layerA = getNodeLayer(nodeA);
+                        const baseYA = getNodeBaseY(nodeA);
+
+                        for (let j = i + 1; j < nodes.length; j++) {{
+                            const nodeB = nodes[j];
+                            const layerB = getNodeLayer(nodeB);
+
+                            // 只对不同层的节点施加排斥力
+                            if (layerA === layerB) continue;
+
+                            const dy = nodeB.y - nodeA.y;
+                            const absDy = Math.abs(dy);
+
+                            // 如果Y方向距离小于层间距,施加排斥力
+                            if (absDy < layerGap) {{
+                                const baseYB = getNodeBaseY(nodeB);
+                                // 根据基准Y判断谁应该在上面
+                                const shouldABeAbove = baseYA < baseYB;
+                                const pushStrength = strength * alpha * (layerGap - absDy) / layerGap;
+
+                                if (shouldABeAbove) {{
+                                    nodeA.vy -= pushStrength;
+                                    nodeB.vy += pushStrength;
+                                }} else {{
+                                    nodeA.vy += pushStrength;
+                                    nodeB.vy -= pushStrength;
+                                }}
+                            }}
+                        }}
+                    }}
+                }}
+
+                force.initialize = function(_) {{
+                    nodes = _;
+                }};
+
+                return force;
+            }}
+
             // 力导向模拟
             simulation = d3.forceSimulation(nodes)
                 .force("link", d3.forceLink(links).id(d => d.id)
@@ -786,7 +841,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
                     }}))
                 // X方向:拉向目标位置,但允许被推开
                 .force("x", d3.forceX(d => nodeTargetX[d.id] || width / 2).strength(0.15))
-                // Y方向力:层布局+倾斜
+                // Y方向力:层布局+倾斜
                 .force("y", d3.forceY(d => {{
                     const baseY = getNodeBaseY(d);
                     return getTiltedY(baseY, d.x || width / 2);
@@ -796,7 +851,9 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
                         // 帖子节点碰撞半径更大
                         if (d.source === "帖子") return 60;
                         return 40;
-                    }}));
+                    }}))
+                // 层间排斥力:不同层节点相互远离
+                .force("layerSeparation", forceLayerSeparation(8));
 
             // 边类型到CSS类的映射
             const edgeTypeClass = {{