|
@@ -2119,19 +2119,22 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
return path + "Z";
|
|
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")
|
|
const treeLinks = postTreeGroup.selectAll(".tree-link")
|
|
|
.data(postRoot.links())
|
|
.data(postRoot.links())
|
|
|
.enter()
|
|
.enter()
|
|
|
.append("path")
|
|
.append("path")
|
|
|
.attr("class", "tree-link")
|
|
.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("fill", "none")
|
|
|
.attr("stroke", "#9b59b6")
|
|
.attr("stroke", "#9b59b6")
|
|
|
.attr("stroke-width", 1.5)
|
|
.attr("stroke-width", 1.5)
|
|
@@ -2562,33 +2565,91 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
.append("g")
|
|
.append("g")
|
|
|
.attr("class", "graph-link-group");
|
|
.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("fill", "none")
|
|
|
.attr("stroke", d => graphEdgeColors[d.type] || "#9b59b6")
|
|
.attr("stroke", d => graphEdgeColors[d.type] || "#9b59b6")
|
|
|
.attr("stroke-width", 1.5)
|
|
.attr("stroke-width", 1.5)
|
|
|
.attr("stroke-opacity", 0.6)
|
|
.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)
|
|
graphLinks.filter(d => d.type && d.type.startsWith("匹配_") && d.similarity > 0)
|