|
|
@@ -958,12 +958,12 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
</div>
|
|
|
<div class="cascade-edges">
|
|
|
<div class="cascade-edge-group active" data-level="1">
|
|
|
- <div class="cascade-edge-group-header">L1 <span class="header-actions"><label class="select-all"><input type="checkbox" data-level="1" data-select-all>全选</label><span class="invert-btn" data-level="1" onclick="invertSelection(1)">反选</span><span class="reset-btn" onclick="resetSelection()">重置</span></span></div>
|
|
|
- <label class="cascade-edge-item"><input type="checkbox" data-level="1" data-type="属于"><span class="edge-color" style="background:#9b59b6"></span>属于</label>
|
|
|
- <label class="cascade-edge-item"><input type="checkbox" data-level="1" data-type="包含"><span class="edge-color" style="background:#ffb6c1"></span>包含</label>
|
|
|
- <label class="cascade-edge-item"><input type="checkbox" data-level="1" data-type="分类共现(跨点)"><span class="edge-color" style="background:#2ecc71"></span>跨点共现</label>
|
|
|
- <label class="cascade-edge-item"><input type="checkbox" data-level="1" data-type="分类共现(点内)"><span class="edge-color" style="background:#3498db"></span>点内共现</label>
|
|
|
- <label class="cascade-edge-item"><input type="checkbox" data-level="1" data-type="标签共现"><span class="edge-color" style="background:#f39c12"></span>标签共现</label>
|
|
|
+ <div class="cascade-edge-group-header">L1 <span class="header-actions"><label class="select-all"><input type="checkbox" data-level="1" data-select-all checked>全选</label><span class="invert-btn" data-level="1" onclick="invertSelection(1)">反选</span><span class="reset-btn" onclick="resetSelection()">重置</span></span></div>
|
|
|
+ <label class="cascade-edge-item"><input type="checkbox" data-level="1" data-type="属于" checked><span class="edge-color" style="background:#9b59b6"></span>属于</label>
|
|
|
+ <label class="cascade-edge-item"><input type="checkbox" data-level="1" data-type="包含" checked><span class="edge-color" style="background:#ffb6c1"></span>包含</label>
|
|
|
+ <label class="cascade-edge-item"><input type="checkbox" data-level="1" data-type="分类共现(跨点)" checked><span class="edge-color" style="background:#2ecc71"></span>跨点共现</label>
|
|
|
+ <label class="cascade-edge-item"><input type="checkbox" data-level="1" data-type="分类共现(点内)" checked><span class="edge-color" style="background:#3498db"></span>点内共现</label>
|
|
|
+ <label class="cascade-edge-item"><input type="checkbox" data-level="1" data-type="标签共现" checked><span class="edge-color" style="background:#f39c12"></span>标签共现</label>
|
|
|
</div>
|
|
|
<div class="cascade-edge-group" data-level="2">
|
|
|
<div class="cascade-edge-group-header">L2 <span class="header-actions"><label class="select-all"><input type="checkbox" data-level="2" data-select-all>全选</label><span class="invert-btn" data-level="2" onclick="invertSelection(2)">反选</span></span></div>
|
|
|
@@ -2143,7 +2143,13 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
.enter()
|
|
|
.append("g")
|
|
|
.attr("class", "tree-node")
|
|
|
- .attr("transform", d => `translate(${{d.x}},${{d.y}})`);
|
|
|
+ .attr("transform", d => `translate(${{d.x}},${{d.y}})`)
|
|
|
+ .style("cursor", "pointer")
|
|
|
+ .on("click", (event, d) => {{
|
|
|
+ event.stopPropagation();
|
|
|
+ // 复用共享的节点点击处理函数
|
|
|
+ handleNodeClick(d.data.id || d.data.节点ID, d.data.name || d.data.节点名称);
|
|
|
+ }});
|
|
|
|
|
|
// 节点形状
|
|
|
treeNodes.each(function(d) {{
|
|
|
@@ -2644,11 +2650,10 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
.attr("font-size", "11px")
|
|
|
.text(d => d.节点名称 || d.name || "");
|
|
|
|
|
|
- // 点击联动
|
|
|
+ // 点击联动 - 复用共享的节点点击处理函数
|
|
|
graphNodes.on("click", function(event, d) {{
|
|
|
event.stopPropagation();
|
|
|
- nodeElements.classed("highlighted", n => n.id === d.id);
|
|
|
- nodeElements.classed("dimmed", n => n.id !== d.id);
|
|
|
+ handleNodeClick(d.id || d.节点ID, d.节点名称 || d.name);
|
|
|
}});
|
|
|
|
|
|
// 存储引用
|
|
|
@@ -2926,6 +2931,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
if (event.target === this) {{
|
|
|
resetTreeHighlight();
|
|
|
resetGraphHighlight();
|
|
|
+ resetRightTreeHighlight();
|
|
|
clearEgoGraph();
|
|
|
closeDetailPanel();
|
|
|
}}
|
|
|
@@ -3017,6 +3023,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
|
|
|
// 3. 同步人设树高亮和关系图展示
|
|
|
syncTreeAndRelationGraph(personaPathNodes, edgeType);
|
|
|
+ // 注:highlightEdge 内部已调用 highlightRightTree 联动右边树
|
|
|
}})
|
|
|
.on("mouseover", function(event, d) {{
|
|
|
d3.select(this.parentNode).select(".link")
|
|
|
@@ -3123,9 +3130,25 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
event.stopPropagation();
|
|
|
highlightNode(d);
|
|
|
showNodeInfo(d);
|
|
|
- // 人设节点联动:高亮人设树 + 显示关系图
|
|
|
+ // 联动:高亮人设树 + 显示关系图 + 右边树
|
|
|
if (d.source === "人设") {{
|
|
|
handleNodeClick(d.节点ID, d.节点名称);
|
|
|
+ }} else {{
|
|
|
+ // 帖子标签节点:直接联动右边树高亮
|
|
|
+ const highlightedNodeIds = new Set([d.id]);
|
|
|
+ const highlightedEdgeKeys = new Set();
|
|
|
+ // 找相连的边和节点
|
|
|
+ g.selectAll(".link-group").each(function(link) {{
|
|
|
+ const srcId = typeof link.source === "object" ? link.source.id : link.source;
|
|
|
+ const tgtId = typeof link.target === "object" ? link.target.id : link.target;
|
|
|
+ if (srcId === d.id || tgtId === d.id) {{
|
|
|
+ highlightedNodeIds.add(srcId);
|
|
|
+ highlightedNodeIds.add(tgtId);
|
|
|
+ highlightedEdgeKeys.add(`${{srcId}}|${{tgtId}}`);
|
|
|
+ highlightedEdgeKeys.add(`${{tgtId}}|${{srcId}}`);
|
|
|
+ }}
|
|
|
+ }});
|
|
|
+ highlightRightTree(highlightedNodeIds, highlightedEdgeKeys);
|
|
|
}}
|
|
|
}});
|
|
|
|
|
|
@@ -3334,6 +3357,17 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
d3.select(this).style("display", "block");
|
|
|
}}
|
|
|
}});
|
|
|
+
|
|
|
+ // 联动右边树:收集高亮边的key
|
|
|
+ const highlightEdgeKeys = new Set();
|
|
|
+ highlightLinkIndices.forEach(i => {{
|
|
|
+ const link = links[i];
|
|
|
+ const srcId = typeof link.source === "object" ? link.source.id : link.source;
|
|
|
+ const tgtId = typeof link.target === "object" ? link.target.id : link.target;
|
|
|
+ highlightEdgeKeys.add(`${{srcId}}|${{tgtId}}`);
|
|
|
+ highlightEdgeKeys.add(`${{tgtId}}|${{srcId}}`);
|
|
|
+ }});
|
|
|
+ highlightRightTree(highlightNodeIds, highlightEdgeKeys);
|
|
|
}}
|
|
|
|
|
|
// 点击空白处清除高亮(合并所有空白点击逻辑)
|
|
|
@@ -3348,6 +3382,8 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
// 清除人设树高亮
|
|
|
resetTreeHighlight();
|
|
|
resetGraphHighlight();
|
|
|
+ // 恢复右边帖子树
|
|
|
+ resetRightTreeHighlight();
|
|
|
// 关闭详情面板
|
|
|
closeDetailPanel();
|
|
|
// 清除关系图
|
|
|
@@ -3731,28 +3767,32 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
}}, 50);
|
|
|
}}
|
|
|
|
|
|
- // 高亮右侧图中对应节点及其相关节点和边
|
|
|
+ // 高亮左边圆中对应节点及边,同时收集用于右边树联动的数据
|
|
|
+ // 收集高亮的节点ID和边key(左边圆高亮什么,右边树就高亮什么)
|
|
|
+ const highlightedNodeIds = new Set([clickedNodeId]);
|
|
|
+ const highlightedEdgeKeys = new Set();
|
|
|
+
|
|
|
if (g) {{
|
|
|
- // 收集相关节点ID(点击的节点及其直接连接的节点)
|
|
|
- const relatedNodeIds = new Set([clickedNodeId]);
|
|
|
- // 找所有与该节点相连的边
|
|
|
+ // 找所有与该节点相连的边,收集相关节点和边
|
|
|
g.selectAll(".link-group").each(function(d) {{
|
|
|
const srcId = typeof d.source === "object" ? d.source.id : d.source;
|
|
|
const tgtId = typeof d.target === "object" ? d.target.id : d.target;
|
|
|
if (srcId === clickedNodeId || tgtId === clickedNodeId) {{
|
|
|
- relatedNodeIds.add(srcId);
|
|
|
- relatedNodeIds.add(tgtId);
|
|
|
+ highlightedNodeIds.add(srcId);
|
|
|
+ highlightedNodeIds.add(tgtId);
|
|
|
+ highlightedEdgeKeys.add(`${{srcId}}|${{tgtId}}`);
|
|
|
+ highlightedEdgeKeys.add(`${{tgtId}}|${{srcId}}`);
|
|
|
}}
|
|
|
}});
|
|
|
|
|
|
// 检查是否有相关节点在图中
|
|
|
- const hasRelatedNodes = g.selectAll(".node").filter(n => relatedNodeIds.has(n.id)).size() > 0;
|
|
|
+ const hasRelatedNodes = g.selectAll(".node").filter(n => highlightedNodeIds.has(n.id)).size() > 0;
|
|
|
|
|
|
if (hasRelatedNodes) {{
|
|
|
// 高亮相关节点,变灰其他
|
|
|
g.selectAll(".node")
|
|
|
- .classed("dimmed", n => !relatedNodeIds.has(n.id))
|
|
|
- .classed("highlighted", n => relatedNodeIds.has(n.id));
|
|
|
+ .classed("dimmed", n => !highlightedNodeIds.has(n.id))
|
|
|
+ .classed("highlighted", n => highlightedNodeIds.has(n.id));
|
|
|
// 高亮相关边
|
|
|
g.selectAll(".link-group").each(function(d) {{
|
|
|
const srcId = typeof d.source === "object" ? d.source.id : d.source;
|
|
|
@@ -3772,6 +3812,9 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
g.selectAll(".link-group").classed("dimmed", true).classed("highlighted", false);
|
|
|
}}
|
|
|
}}
|
|
|
+
|
|
|
+ // 联动高亮右边帖子树(直接用左边圆高亮的节点和边)
|
|
|
+ highlightRightTree(highlightedNodeIds, highlightedEdgeKeys);
|
|
|
}}
|
|
|
|
|
|
// 同步人设树高亮和关系图展示(职责单一:不处理力导向图)
|
|
|
@@ -4400,6 +4443,9 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
.style("visibility", function(d) {{
|
|
|
return pathNodes.has(d.id) ? "visible" : "hidden";
|
|
|
}});
|
|
|
+
|
|
|
+ // 联动:高亮右边树上对应的节点和边
|
|
|
+ highlightRightTree(pathNodes, pathEdgeKeys);
|
|
|
}}
|
|
|
|
|
|
// 恢复关系图的高亮状态
|
|
|
@@ -4414,6 +4460,66 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
// 恢复节点的可见性
|
|
|
egoGroup.selectAll(".ego-node")
|
|
|
.style("visibility", "visible");
|
|
|
+
|
|
|
+ // 联动:恢复右边树
|
|
|
+ resetRightTreeHighlight();
|
|
|
+ }}
|
|
|
+
|
|
|
+ // 高亮右边树上对应的节点和边
|
|
|
+ function highlightRightTree(pathNodes, pathEdgeKeys) {{
|
|
|
+ const postTreeGroup = d3.select(".post-tree");
|
|
|
+
|
|
|
+ // 高亮帖子树节点
|
|
|
+ if (!postTreeGroup.empty()) {{
|
|
|
+ postTreeGroup.selectAll(".tree-node")
|
|
|
+ .style("opacity", function(d) {{
|
|
|
+ const nodeId = d.data.id || d.data.节点ID;
|
|
|
+ return pathNodes.has(nodeId) ? 1 : 0.2;
|
|
|
+ }});
|
|
|
+
|
|
|
+ // 高亮帖子树边
|
|
|
+ postTreeGroup.selectAll(".tree-link")
|
|
|
+ .style("opacity", function(d) {{
|
|
|
+ const srcId = d.source.data.id || d.source.data.节点ID;
|
|
|
+ const tgtId = d.target.data.id || d.target.data.节点ID;
|
|
|
+ // 如果两端节点都在路径中,边高亮
|
|
|
+ return (pathNodes.has(srcId) && pathNodes.has(tgtId)) ? 1 : 0.2;
|
|
|
+ }});
|
|
|
+ }}
|
|
|
+
|
|
|
+ // 高亮人设节点(右边下方的图节点,在g下面不是postTreeGroup下)
|
|
|
+ d3.selectAll(".graph-node")
|
|
|
+ .style("opacity", function(d) {{
|
|
|
+ return pathNodes.has(d.id) ? 1 : 0.2;
|
|
|
+ }});
|
|
|
+
|
|
|
+ // 高亮跨层边(graph-link-group 包含 graph-link)
|
|
|
+ d3.selectAll(".graph-link-group")
|
|
|
+ .style("opacity", function(d) {{
|
|
|
+ const srcId = d.source ? (d.source.id || d.source) : null;
|
|
|
+ const tgtId = d.target ? (d.target.id || d.target) : null;
|
|
|
+ if (!srcId || !tgtId) return 0.2;
|
|
|
+ const key1 = `${{srcId}}|${{tgtId}}`;
|
|
|
+ const key2 = `${{tgtId}}|${{srcId}}`;
|
|
|
+ return (pathEdgeKeys.has(key1) || pathEdgeKeys.has(key2)) ? 1 : 0.2;
|
|
|
+ }});
|
|
|
+ }}
|
|
|
+
|
|
|
+ // 恢复右边树的高亮状态
|
|
|
+ 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);
|
|
|
+ }}
|
|
|
+
|
|
|
+ // 恢复人设节点(在g下面)
|
|
|
+ d3.selectAll(".graph-node").style("opacity", 1);
|
|
|
+
|
|
|
+ // 恢复跨层边
|
|
|
+ d3.selectAll(".graph-link-group").style("opacity", 1);
|
|
|
}}
|
|
|
|
|
|
// 渲染路径上的所有节点和边(点击镜像边/二阶边时调用)
|