|
|
@@ -1051,24 +1051,6 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
-
|
|
|
- <div class="controls">
|
|
|
- <div class="control-label">视图控制</div>
|
|
|
- <button onclick="resetZoom()">重置视图</button>
|
|
|
- <button onclick="toggleLabels()">切换标签</button>
|
|
|
- <button onclick="toggleCrossLayerEdges()" id="crossLayerBtn">显示跨层边</button>
|
|
|
- <div class="control-group">
|
|
|
- <div class="control-label">人设树配置</div>
|
|
|
- <button onclick="toggleTreeTags()" id="treeTagBtn" class="active">显示标签</button>
|
|
|
- <select id="treeDepthSelect" onchange="setTreeDepth(this.value)">
|
|
|
- <option value="0">全部层级</option>
|
|
|
- <option value="2">2层</option>
|
|
|
- <option value="3">3层</option>
|
|
|
- <option value="4">4层</option>
|
|
|
- <option value="5">5层</option>
|
|
|
- </select>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -1379,6 +1361,16 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
return {{ root: rootNode, nodeCount: totalNodeCount, tagMap: tagMap }};
|
|
|
}}
|
|
|
|
|
|
+ // 统一提取边的分数字段(全局函数)
|
|
|
+ function getEdgeScore(e) {{
|
|
|
+ if (!e.边详情) return null;
|
|
|
+ // 优先级:相似度 > Jaccard相似度 > 重叠系数
|
|
|
+ if (e.边详情.相似度 !== undefined) return e.边详情.相似度;
|
|
|
+ if (e.边详情.Jaccard相似度 !== undefined) return e.边详情.Jaccard相似度;
|
|
|
+ if (e.边详情.重叠系数 !== undefined) return e.边详情.重叠系数;
|
|
|
+ return null;
|
|
|
+ }}
|
|
|
+
|
|
|
// 显示帖子详情模态框
|
|
|
function showPostDetail(postData) {{
|
|
|
if (!postData) return;
|
|
|
@@ -1494,16 +1486,25 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
}}
|
|
|
|
|
|
// 过滤掉引用帖子点节点的边
|
|
|
- const links = data.edges
|
|
|
+ const rawLinks = data.edges
|
|
|
.filter(e => !isPostPointId(e.源节点ID) && !isPostPointId(e.目标节点ID))
|
|
|
.map(e => ({{
|
|
|
...e,
|
|
|
source: e.源节点ID,
|
|
|
target: e.目标节点ID,
|
|
|
type: e.边类型,
|
|
|
- similarity: e.边详情 ? e.边详情.相似度 : 0
|
|
|
+ score: getEdgeScore(e)
|
|
|
}}));
|
|
|
|
|
|
+ // 去重:同一对节点之间相同类型的边只保留一条
|
|
|
+ const seenEdgeKeys = new Set();
|
|
|
+ const links = rawLinks.filter(e => {{
|
|
|
+ const edgeKey = [e.source, e.target].sort().join("|") + "|" + e.type;
|
|
|
+ if (seenEdgeKeys.has(edgeKey)) return false;
|
|
|
+ seenEdgeKeys.add(edgeKey);
|
|
|
+ return true;
|
|
|
+ }});
|
|
|
+
|
|
|
// 分离节点类型
|
|
|
const postNodes = nodes.filter(n => n.source === "帖子");
|
|
|
const personaNodes = nodes.filter(n => n.source === "人设" && !n.是否扩展);
|
|
|
@@ -2061,9 +2062,21 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
return Math.max(minSep, defaultSep);
|
|
|
}});
|
|
|
|
|
|
- // 过滤掉人设节点,只保留树结构
|
|
|
+ // 从原始节点数据中构建权重映射(标签节点有权重字段)
|
|
|
+ const nodeWeightMap = {{}};
|
|
|
+ nodes.forEach(n => {{
|
|
|
+ if (n.权重 !== undefined) {{
|
|
|
+ nodeWeightMap[n.id] = n.权重;
|
|
|
+ }}
|
|
|
+ }});
|
|
|
+
|
|
|
+ // 过滤掉人设节点,只保留树结构,并补充权重
|
|
|
function filterTreeData(node) {{
|
|
|
const filtered = {{ ...node }};
|
|
|
+ // 补充权重信息
|
|
|
+ if (node.id && nodeWeightMap[node.id] !== undefined) {{
|
|
|
+ filtered.权重 = nodeWeightMap[node.id];
|
|
|
+ }}
|
|
|
if (node.children) {{
|
|
|
filtered.children = node.children
|
|
|
.filter(c => c.nodeType !== "人设" && c.nodeType !== "人设扩展")
|
|
|
@@ -2128,11 +2141,21 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
${{d.target.x}},${{d.target.y}}`;
|
|
|
}};
|
|
|
|
|
|
- // 绘制树的边(帖子→维度→点→标签)- 无点击事件
|
|
|
- const treeLinks = postTreeGroup.selectAll(".tree-link")
|
|
|
- .data(postRoot.links())
|
|
|
+ // 为树边添加score字段(从目标节点的权重获取)
|
|
|
+ const treeLinkData = postRoot.links().map(d => {{
|
|
|
+ d.score = d.target.data.权重 !== undefined ? d.target.data.权重 : null;
|
|
|
+ return d;
|
|
|
+ }});
|
|
|
+
|
|
|
+ // 绘制树的边(帖子→维度→点→标签)- 用g元素包裹边和分数
|
|
|
+ const treeLinkGroups = postTreeGroup.selectAll(".tree-link-group")
|
|
|
+ .data(treeLinkData)
|
|
|
.enter()
|
|
|
- .append("path")
|
|
|
+ .append("g")
|
|
|
+ .attr("class", "tree-link-group");
|
|
|
+
|
|
|
+ // 边的路径
|
|
|
+ const treeLinks = treeLinkGroups.append("path")
|
|
|
.attr("class", "tree-link")
|
|
|
.attr("d", treeLinkPath)
|
|
|
.attr("fill", "none")
|
|
|
@@ -2140,6 +2163,20 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
.attr("stroke-width", 1.5)
|
|
|
.attr("stroke-opacity", 0.6);
|
|
|
|
|
|
+ // 在有分数的边上显示分数(统一逻辑,居中显示在边上)
|
|
|
+ treeLinkGroups.filter(d => d.score !== null)
|
|
|
+ .append("text")
|
|
|
+ .attr("class", "tree-link-score")
|
|
|
+ .attr("x", d => (d.source.x + d.target.x) / 2)
|
|
|
+ .attr("y", d => (d.source.y + d.target.y) / 2)
|
|
|
+ .attr("dy", "0.35em")
|
|
|
+ .attr("text-anchor", "middle")
|
|
|
+ .attr("fill", "#fff")
|
|
|
+ .attr("font-size", "8px")
|
|
|
+ .attr("font-weight", "bold")
|
|
|
+ .style("text-shadow", "0 0 3px #9b59b6, 0 0 3px #9b59b6")
|
|
|
+ .text(d => d.score.toFixed(2));
|
|
|
+
|
|
|
// 绘制树节点(非根节点)
|
|
|
const treeNodes = postTreeGroup.selectAll(".tree-node")
|
|
|
.data(postRoot.descendants().filter(d => !d.data.isRoot))
|
|
|
@@ -2324,6 +2361,11 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
${{d.target.x}},${{d.target.y}}`;
|
|
|
}});
|
|
|
|
|
|
+ // 更新分数文本位置(因为标签节点位置可能变了)
|
|
|
+ postTreeGroup.selectAll(".tree-link-score")
|
|
|
+ .attr("x", d => (d.source.x + d.target.x) / 2)
|
|
|
+ .attr("y", d => (d.source.y + d.target.y) / 2);
|
|
|
+
|
|
|
// ===== 下半部分:三分图(标签 ↔ 人设_1层 ↔ 人设_2层)=====
|
|
|
// 复用圆形图的节点数据,但只取人设节点(去重)
|
|
|
const personaLayer1Nodes = nodes.filter(n => getNodeLayer(n) === 1);
|
|
|
@@ -2651,8 +2693,8 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
}}
|
|
|
}});
|
|
|
|
|
|
- // 在匹配边上显示相似度
|
|
|
- graphLinks.filter(d => d.type && d.type.startsWith("匹配_") && d.similarity > 0)
|
|
|
+ // 在有分数的边上显示分数(统一逻辑)
|
|
|
+ graphLinks.filter(d => d.score !== null && d.score !== undefined)
|
|
|
.append("text")
|
|
|
.attr("x", d => (d.srcNode.x + d.tgtNode.x) / 2)
|
|
|
.attr("y", d => (d.srcNode.y + d.tgtNode.y) / 2)
|
|
|
@@ -2662,7 +2704,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
.attr("font-size", "8px")
|
|
|
.attr("font-weight", "bold")
|
|
|
.style("text-shadow", "0 0 3px #e94560, 0 0 3px #e94560")
|
|
|
- .text(d => d.similarity ? d.similarity.toFixed(2) : "");
|
|
|
+ .text(d => d.score.toFixed(2));
|
|
|
|
|
|
// 绘制人设节点(去重后的)
|
|
|
const allPersonaNodes = [...personaLayer1Nodes, ...personaLayer2Nodes];
|
|
|
@@ -3033,8 +3075,8 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
}}
|
|
|
}});
|
|
|
|
|
|
- // 为匹配边添加分数标签
|
|
|
- const edgeLabels = linkG.filter(d => d.type.startsWith("匹配_") && d.边详情 && d.边详情.相似度)
|
|
|
+ // 为有分数的边添加分数标签(统一逻辑)
|
|
|
+ const edgeLabels = linkG.filter(d => d.score !== null && d.score !== undefined)
|
|
|
.append("g")
|
|
|
.attr("class", "edge-label-group");
|
|
|
|
|
|
@@ -3045,10 +3087,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
|
|
|
edgeLabels.append("text")
|
|
|
.attr("class", "edge-label")
|
|
|
- .text(d => {{
|
|
|
- const score = d.边详情.相似度;
|
|
|
- return typeof score === "number" ? score.toFixed(2) : score;
|
|
|
- }});
|
|
|
+ .text(d => d.score.toFixed(2));
|
|
|
|
|
|
// 边的点击事件
|
|
|
linkHitarea.on("click", (event, d, i) => {{
|
|
|
@@ -4163,7 +4202,8 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
source: e.源节点ID,
|
|
|
target: e.目标节点ID,
|
|
|
type: e.边类型,
|
|
|
- curvature: curvature
|
|
|
+ curvature: curvature,
|
|
|
+ score: getEdgeScore(e)
|
|
|
}});
|
|
|
}});
|
|
|
}});
|
|
|
@@ -4312,6 +4352,18 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
.attr("stroke-opacity", 0.7)
|
|
|
.attr("fill", "none");
|
|
|
|
|
|
+ // 在有分数的边上显示分数(统一逻辑)
|
|
|
+ const edgeScoreText = egoGroup.selectAll(".ego-edge-score")
|
|
|
+ .data(links.filter(d => d.score !== null && d.score !== undefined))
|
|
|
+ .join("text")
|
|
|
+ .attr("class", "ego-edge-score")
|
|
|
+ .attr("text-anchor", "middle")
|
|
|
+ .attr("fill", "#fff")
|
|
|
+ .attr("font-size", "8px")
|
|
|
+ .attr("font-weight", "bold")
|
|
|
+ .style("text-shadow", "0 0 3px #333, 0 0 3px #333")
|
|
|
+ .text(d => d.score.toFixed(2));
|
|
|
+
|
|
|
// 绘制节点(分类用方形,标签用圆形)
|
|
|
const node = egoGroup.selectAll(".ego-node")
|
|
|
.data(nodes)
|
|
|
@@ -4407,6 +4459,10 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
simulation.on("tick", () => {{
|
|
|
link.attr("d", linkPath);
|
|
|
node.attr("transform", d => `translate(${{d.x}},${{d.y}})`);
|
|
|
+ // 更新分数位置(边的中点)
|
|
|
+ edgeScoreText
|
|
|
+ .attr("x", d => (d.source.x + d.target.x) / 2)
|
|
|
+ .attr("y", d => (d.source.y + d.target.y) / 2);
|
|
|
}});
|
|
|
}}
|
|
|
|
|
|
@@ -4499,6 +4555,15 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
return pathEdgeKeys.has(key) ? "visible" : "hidden";
|
|
|
}});
|
|
|
|
|
|
+ // 隐藏不在路径上的边的分数
|
|
|
+ egoGroup.selectAll(".ego-edge-score")
|
|
|
+ .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) {{
|
|
|
@@ -4518,6 +4583,10 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
egoGroup.selectAll(".ego-edge")
|
|
|
.style("visibility", "visible");
|
|
|
|
|
|
+ // 恢复边分数的可见性
|
|
|
+ egoGroup.selectAll(".ego-edge-score")
|
|
|
+ .style("visibility", "visible");
|
|
|
+
|
|
|
// 恢复节点的可见性
|
|
|
egoGroup.selectAll(".ego-node")
|
|
|
.style("visibility", "visible");
|
|
|
@@ -4538,8 +4607,8 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
return pathNodes.has(nodeId) ? 1 : 0.2;
|
|
|
}});
|
|
|
|
|
|
- // 高亮帖子树边
|
|
|
- postTreeGroup.selectAll(".tree-link")
|
|
|
+ // 高亮帖子树边(包含边和分数的组)
|
|
|
+ postTreeGroup.selectAll(".tree-link-group")
|
|
|
.style("opacity", function(d) {{
|
|
|
const srcId = d.source.data.id || d.source.data.节点ID;
|
|
|
const tgtId = d.target.data.id || d.target.data.节点ID;
|
|
|
@@ -4570,10 +4639,10 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
function resetRightTreeHighlight() {{
|
|
|
const postTreeGroup = d3.select(".post-tree");
|
|
|
|
|
|
- // 恢复帖子树节点和边
|
|
|
+ // 恢复帖子树节点和边(包含边和分数的组)
|
|
|
if (!postTreeGroup.empty()) {{
|
|
|
postTreeGroup.selectAll(".tree-node").style("opacity", 1);
|
|
|
- postTreeGroup.selectAll(".tree-link").style("opacity", 1);
|
|
|
+ postTreeGroup.selectAll(".tree-link-group").style("opacity", 1);
|
|
|
}}
|
|
|
|
|
|
// 恢复人设节点(在g下面)
|