|
|
@@ -193,6 +193,10 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
stroke: #fff;
|
|
|
stroke-dasharray: 4,2;
|
|
|
}}
|
|
|
+ .node .post-node.unmatched {{
|
|
|
+ stroke: #555;
|
|
|
+ stroke-dasharray: 2,2;
|
|
|
+ }}
|
|
|
.node .persona-node {{
|
|
|
stroke: #fff;
|
|
|
}}
|
|
|
@@ -265,11 +269,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
stroke: #8e44ad;
|
|
|
stroke-dasharray: 6,3;
|
|
|
}}
|
|
|
- /* 二阶镜像边样式(点划线) */
|
|
|
- .link.second-order {{
|
|
|
- stroke: #17a2b8;
|
|
|
- stroke-dasharray: 8,3,2,3;
|
|
|
- }}
|
|
|
+ /* 二阶边现在使用与镜像边相同的样式(基于原始边类型) */
|
|
|
/* 高亮/灰化样式 */
|
|
|
.node.dimmed circle, .node.dimmed rect {{
|
|
|
opacity: 0.15 !important;
|
|
|
@@ -760,8 +760,21 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
|
|
|
// 力导向模拟
|
|
|
simulation = d3.forceSimulation(nodes)
|
|
|
- .force("link", d3.forceLink(links).id(d => d.id).distance(80).strength(0.1))
|
|
|
- .force("charge", d3.forceManyBody().strength(-300))
|
|
|
+ .force("link", d3.forceLink(links).id(d => d.id)
|
|
|
+ .distance(d => {{
|
|
|
+ // 帖子之间的边(镜像边/二阶边)距离更大
|
|
|
+ if (d.type && (d.type.startsWith("镜像_") || d.type.startsWith("二阶_"))) {{
|
|
|
+ return 200;
|
|
|
+ }}
|
|
|
+ return 80;
|
|
|
+ }})
|
|
|
+ .strength(0.1))
|
|
|
+ .force("charge", d3.forceManyBody()
|
|
|
+ .strength(d => {{
|
|
|
+ // 帖子节点排斥力更强
|
|
|
+ if (d.source === "帖子") return -600;
|
|
|
+ return -300;
|
|
|
+ }}))
|
|
|
// X方向:拉向目标位置,但允许被推开
|
|
|
.force("x", d3.forceX(d => nodeTargetX[d.id] || width / 2).strength(0.15))
|
|
|
// Y方向力:三层布局+倾斜
|
|
|
@@ -769,7 +782,12 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
const baseY = getNodeBaseY(d);
|
|
|
return getTiltedY(baseY, d.x || width / 2);
|
|
|
}}).strength(0.5))
|
|
|
- .force("collision", d3.forceCollide().radius(40));
|
|
|
+ .force("collision", d3.forceCollide()
|
|
|
+ .radius(d => {{
|
|
|
+ // 帖子节点碰撞半径更大
|
|
|
+ if (d.source === "帖子") return 60;
|
|
|
+ return 40;
|
|
|
+ }}));
|
|
|
|
|
|
// 边类型到CSS类的映射
|
|
|
const edgeTypeClass = {{
|
|
|
@@ -787,10 +805,15 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
"镜像_包含": "mirror-contain"
|
|
|
}};
|
|
|
|
|
|
- // 获取边的CSS类(处理二阶边)
|
|
|
+ // 获取边的CSS类(处理二阶边,映射到对应的镜像边样式)
|
|
|
function getEdgeClass(edgeType) {{
|
|
|
if (edgeTypeClass[edgeType]) return edgeTypeClass[edgeType];
|
|
|
- if (edgeType.startsWith("二阶_")) return "second-order";
|
|
|
+ // 二阶边映射到对应的镜像边样式(颜色相同,都用虚线)
|
|
|
+ if (edgeType.startsWith("二阶_")) {{
|
|
|
+ const originalType = edgeType.replace("二阶_", "");
|
|
|
+ const mirrorType = "镜像_" + originalType;
|
|
|
+ if (edgeTypeClass[mirrorType]) return edgeTypeClass[mirrorType];
|
|
|
+ }}
|
|
|
return "match";
|
|
|
}}
|
|
|
|
|
|
@@ -859,14 +882,15 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
.on("end", dragended));
|
|
|
|
|
|
// 根据节点类型绘制不同形状:标签用圆形,分类用方形
|
|
|
- // 扩展节点用较低透明度表示
|
|
|
+ // 扩展节点和未匹配节点用较低透明度表示
|
|
|
node.each(function(d) {{
|
|
|
const el = d3.select(this);
|
|
|
const isExpanded = d.是否扩展 === true;
|
|
|
+ const isUnmatched = d.source === "帖子" && d.已匹配 === false;
|
|
|
const size = d.source === "帖子" ? 12 : (isExpanded ? 8 : 10);
|
|
|
const fill = levelColors[d.level] || "#666";
|
|
|
- const nodeClass = d.source === "帖子" ? "post-node" : "persona-node";
|
|
|
- const opacity = isExpanded ? 0.5 : 1;
|
|
|
+ const nodeClass = d.source === "帖子" ? (isUnmatched ? "post-node unmatched" : "post-node") : "persona-node";
|
|
|
+ const opacity = isExpanded ? 0.5 : (isUnmatched ? 0.4 : 1);
|
|
|
|
|
|
if (d.节点类型 === "分类") {{
|
|
|
// 方形
|
|
|
@@ -883,7 +907,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
// 圆形(标签)
|
|
|
el.append("circle")
|
|
|
.attr("r", size)
|
|
|
- .attr("fill", fill)
|
|
|
+ .attr("fill", isUnmatched ? "#666" : fill)
|
|
|
.attr("class", nodeClass)
|
|
|
.attr("opacity", opacity);
|
|
|
}}
|