yangxiaohui 5 päivää sitten
vanhempi
commit
42dee7df54
1 muutettua tiedostoa jossa 158 lisäystä ja 3 poistoa
  1. 158 3
      script/data_processing/visualize_match_graph.py

+ 158 - 3
script/data_processing/visualize_match_graph.py

@@ -129,6 +129,59 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
             display: flex;
             flex-direction: column;
         }}
+        .match-list-panel {{
+            margin-bottom: 15px;
+            border-bottom: 1px solid #0f3460;
+            padding-bottom: 10px;
+        }}
+        .match-list-panel h3 {{
+            font-size: 13px;
+            margin-bottom: 8px;
+            color: #e94560;
+        }}
+        .match-list {{
+            max-height: 200px;
+            overflow-y: auto;
+        }}
+        .match-item {{
+            display: flex;
+            align-items: center;
+            padding: 6px 8px;
+            margin-bottom: 4px;
+            background: rgba(233, 69, 96, 0.1);
+            border-radius: 4px;
+            cursor: pointer;
+            font-size: 11px;
+            gap: 8px;
+        }}
+        .match-item:hover {{
+            background: rgba(233, 69, 96, 0.2);
+        }}
+        .match-item.active {{
+            background: rgba(233, 69, 96, 0.3);
+            border: 1px solid #e94560;
+        }}
+        .match-item .score {{
+            background: #e94560;
+            color: #fff;
+            padding: 2px 6px;
+            border-radius: 3px;
+            font-weight: bold;
+            min-width: 40px;
+            text-align: center;
+        }}
+        .match-item .score.high {{
+            background: #27ae60;
+        }}
+        .match-item .score.medium {{
+            background: #f39c12;
+        }}
+        .match-item .names {{
+            flex: 1;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+        }}
         #persona-tree-panel {{
             flex: 1;
             overflow-y: auto;
@@ -538,6 +591,11 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
             <div id="sidebar">
                 <h1>匹配图谱</h1>
 
+                <div class="match-list-panel">
+                    <h3>匹配关系 <span id="matchCount">(0)</span></h3>
+                    <div id="matchList" class="match-list"></div>
+                </div>
+
                 <div class="detail-panel active" id="detailPanel">
                     <h3 id="detailTitle">点击节点或边查看详情</h3>
                     <div id="detailContent">
@@ -842,6 +900,9 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
             const expandedNodes = nodes.filter(n => n.source === "人设" && n.是否扩展);
             const matchLinks = links.filter(l => l.type === "匹配");
 
+            // 更新匹配列表(按分数降序)
+            updateMatchList(matchLinks, nodes);
+
             // 构建帖子节点到人设节点的映射
             const postToPersona = {{}};
             const personaToPost = {{}};
@@ -1634,7 +1695,16 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
             // 绘制可见的边
             const link = linkG.append("line")
                 .attr("class", d => "link " + getEdgeClass(d.type))
-                .attr("stroke-width", d => d.type === "匹配" ? 2.5 : 1.5);
+                .attr("stroke-width", d => d.type === "匹配" ? 2.5 : 1.5)
+                .attr("stroke-dasharray", d => {{
+                    // 匹配边根据相似度设置虚实线
+                    if (d.type === "匹配" && d.边详情 && d.边详情.相似度 !== undefined) {{
+                        const score = d.边详情.相似度;
+                        if (score >= 0.8) return null;  // >= 0.8 实线
+                        if (score >= 0.5) return "6,4";  // 0.5-0.8 虚线
+                    }}
+                    return null;  // 默认实线
+                }});
 
             // 判断是否为跨层边(根据源和目标节点的层级)- 赋值给全局变量
             isCrossLayerEdge = function(d) {{
@@ -1644,9 +1714,11 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
                 return getNodeLayer(sourceNode) !== getNodeLayer(targetNode);
             }};
 
-            // 设置跨层边的初始可见性(默认隐藏)
+            // 设置跨层边的初始可见性(匹配边始终显示,其他跨层边默认隐藏)
             linkG.each(function(d) {{
-                if (isCrossLayerEdge(d) && !showCrossLayerEdges) {{
+                if (d.type === "匹配") {{
+                    d3.select(this).style("display", "block");  // 匹配边始终显示
+                }} else if (isCrossLayerEdge(d) && !showCrossLayerEdges) {{
                     d3.select(this).style("display", "none");
                 }}
             }});
@@ -2203,6 +2275,89 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
             document.getElementById("detailPanel").classList.remove("active");
         }}
 
+        // 更新匹配列表
+        function updateMatchList(matchLinks, nodes) {{
+            const listEl = document.getElementById("matchList");
+            const countEl = document.getElementById("matchCount");
+
+            // 构建节点ID到名称的映射
+            const nodeNames = {{}};
+            nodes.forEach(n => {{
+                nodeNames[n.id] = n.节点名称 || n.id;
+            }});
+
+            // 按分数降序排序
+            const sortedLinks = [...matchLinks].sort((a, b) => {{
+                const scoreA = a.边详情?.相似度 ?? 0;
+                const scoreB = b.边详情?.相似度 ?? 0;
+                return scoreB - scoreA;
+            }});
+
+            countEl.textContent = `(${{sortedLinks.length}})`;
+
+            // 生成列表HTML
+            let html = "";
+            sortedLinks.forEach((link, index) => {{
+                const score = link.边详情?.相似度;
+                const scoreText = score !== undefined ? score.toFixed(2) : "N/A";
+                const scoreClass = score >= 0.8 ? "high" : (score >= 0.5 ? "medium" : "");
+
+                const srcId = typeof link.source === "object" ? link.source.id : link.source;
+                const tgtId = typeof link.target === "object" ? link.target.id : link.target;
+                const srcName = nodeNames[srcId] || srcId;
+                const tgtName = nodeNames[tgtId] || tgtId;
+
+                html += `
+                    <div class="match-item" data-index="${{index}}" data-src="${{srcId}}" data-tgt="${{tgtId}}">
+                        <span class="score ${{scoreClass}}">${{scoreText}}</span>
+                        <span class="names" title="${{srcName}} → ${{tgtName}}">${{srcName}} → ${{tgtName}}</span>
+                    </div>
+                `;
+            }});
+
+            listEl.innerHTML = html;
+
+            // 添加点击事件
+            listEl.querySelectorAll(".match-item").forEach(item => {{
+                item.addEventListener("click", () => {{
+                    const srcId = item.dataset.src;
+                    const tgtId = item.dataset.tgt;
+                    const index = parseInt(item.dataset.index);
+
+                    // 高亮对应的边
+                    highlightMatchEdge(srcId, tgtId);
+
+                    // 更新列表项样式
+                    listEl.querySelectorAll(".match-item").forEach(el => el.classList.remove("active"));
+                    item.classList.add("active");
+                }});
+            }});
+        }}
+
+        // 高亮匹配边
+        function highlightMatchEdge(srcId, tgtId) {{
+            if (!g) return;
+
+            // 重置所有
+            g.selectAll(".node").classed("dimmed", true).classed("highlighted", false);
+            g.selectAll(".link-group").classed("dimmed", true).classed("highlighted", false);
+
+            // 高亮对应的边和节点
+            g.selectAll(".link-group").each(function(d) {{
+                const dSrc = typeof d.source === "object" ? d.source.id : d.source;
+                const dTgt = typeof d.target === "object" ? d.target.id : d.target;
+                if ((dSrc === srcId && dTgt === tgtId) || (dSrc === tgtId && dTgt === srcId)) {{
+                    d3.select(this).classed("dimmed", false).classed("highlighted", true);
+                }}
+            }});
+
+            g.selectAll(".node").each(function(d) {{
+                if (d.id === srcId || d.id === tgtId) {{
+                    d3.select(this).classed("dimmed", false).classed("highlighted", true);
+                }}
+            }});
+        }}
+
         // 获取节点颜色(全局版本,根据节点数据判断维度)
         function getTreeNodeColor(d) {{
             const dimColors = {{ "灵感点": "#f39c12", "目的点": "#3498db", "关键点": "#9b59b6" }};