|
@@ -2011,7 +2011,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
return "match";
|
|
return "match";
|
|
|
}}
|
|
}}
|
|
|
|
|
|
|
|
- // ===== 在上方绘制帖子树(只是展示用)=====
|
|
|
|
|
|
|
+ // ===== 帖子树放在圆的右侧,与圆层对齐 =====
|
|
|
const postDetail = data.postDetail || {{}};
|
|
const postDetail = data.postDetail || {{}};
|
|
|
window.currentPostDetail = postDetail;
|
|
window.currentPostDetail = postDetail;
|
|
|
|
|
|
|
@@ -2021,12 +2021,15 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
// 帖子树D3布局(只在有帖子树数据时绘制)
|
|
// 帖子树D3布局(只在有帖子树数据时绘制)
|
|
|
const postTreeGroup = g.append("g").attr("class", "post-tree");
|
|
const postTreeGroup = g.append("g").attr("class", "post-tree");
|
|
|
let postTreeActualHeight = 0;
|
|
let postTreeActualHeight = 0;
|
|
|
- let postTreeOffsetY = 60;
|
|
|
|
|
let postRoot = null;
|
|
let postRoot = null;
|
|
|
|
|
|
|
|
|
|
+ // 卡片尺寸(提前定义)
|
|
|
|
|
+ const cardWidth = 240;
|
|
|
|
|
+ const cardHeight = 90;
|
|
|
|
|
+
|
|
|
if (postTree && postTree.root) {{
|
|
if (postTree && postTree.root) {{
|
|
|
const postTreeLayout = d3.tree()
|
|
const postTreeLayout = d3.tree()
|
|
|
- .nodeSize([70, 80])
|
|
|
|
|
|
|
+ .nodeSize([50, 80])
|
|
|
.separation((a, b) => a.parent === b.parent ? 1.2 : 1.5);
|
|
.separation((a, b) => a.parent === b.parent ? 1.2 : 1.5);
|
|
|
|
|
|
|
|
postRoot = d3.hierarchy(postTree.root);
|
|
postRoot = d3.hierarchy(postTree.root);
|
|
@@ -2041,13 +2044,69 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
treeMaxY = Math.max(treeMaxY, d.y);
|
|
treeMaxY = Math.max(treeMaxY, d.y);
|
|
|
}});
|
|
}});
|
|
|
|
|
|
|
|
- // 帖子树居中,放在圆的上方
|
|
|
|
|
- const postTreeOffsetX = circleAreaCenterX - (treeMinX + treeMaxX) / 2;
|
|
|
|
|
- postTreeGroup.attr("transform", `translate(${{postTreeOffsetX}}, ${{postTreeOffsetY}})`);
|
|
|
|
|
|
|
+ // 找出各层的Y坐标
|
|
|
|
|
+ let rootLayerY = 0, dimLayerY = 0, pointLayerY = 0;
|
|
|
|
|
+ let tagLayerY = 0, personaLayerY = 0, expandedLayerY = 0;
|
|
|
|
|
+ postRoot.descendants().forEach(d => {{
|
|
|
|
|
+ if (d.data.isRoot) rootLayerY = d.y;
|
|
|
|
|
+ if (d.data.isDimension) dimLayerY = d.y;
|
|
|
|
|
+ if (d.data.nodeType === "点") pointLayerY = d.y;
|
|
|
|
|
+ if (d.data.nodeType === "标签") tagLayerY = d.y;
|
|
|
|
|
+ if (d.data.nodeType === "人设") personaLayerY = d.y;
|
|
|
|
|
+ if (d.data.nodeType === "人设扩展") expandedLayerY = d.y;
|
|
|
|
|
+ }});
|
|
|
|
|
|
|
|
- // 卡片尺寸(提前定义,边绘制时需要用)
|
|
|
|
|
- const cardWidth = 240;
|
|
|
|
|
- const cardHeight = 90;
|
|
|
|
|
|
|
+ // 计算目标圆心Y坐标(加上circleYOffset后的实际值)
|
|
|
|
|
+ const circleYOffsetPreview = 40;
|
|
|
|
|
+ const targetTagY = layerCenterY[0] + circleYOffsetPreview; // 帖子标签圆心
|
|
|
|
|
+ const targetPersonaY = layerCenterY[1] + circleYOffsetPreview; // 人设匹配_1层圆心
|
|
|
|
|
+ const targetExpandedY = layerCenterY[2] + circleYOffsetPreview; // 人设匹配_2层圆心
|
|
|
|
|
+
|
|
|
|
|
+ // 计算层间距
|
|
|
|
|
+ const circleGap = targetPersonaY - targetTagY; // 两个圆心之间的距离
|
|
|
|
|
+ const hasExpandedLayer = expandedLayerY > 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 重新分配Y坐标
|
|
|
|
|
+ // 前3层(帖子、维度、点)层高是后面层高的一半
|
|
|
|
|
+ // 后3层(标签、人设、人设扩展)对齐三个圆心
|
|
|
|
|
+ const halfGap = circleGap / 2;
|
|
|
|
|
+
|
|
|
|
|
+ postRoot.descendants().forEach(d => {{
|
|
|
|
|
+ if (d.data.isRoot) {{
|
|
|
|
|
+ // 帖子层:标签层上方 2.5 个半层高
|
|
|
|
|
+ d.y = targetTagY - halfGap * 3;
|
|
|
|
|
+ }} else if (d.data.isDimension) {{
|
|
|
|
|
+ // 维度层:标签层上方 2 个半层高
|
|
|
|
|
+ d.y = targetTagY - halfGap * 2;
|
|
|
|
|
+ }} else if (d.data.nodeType === "点") {{
|
|
|
|
|
+ // 点层:标签层上方 1 个半层高
|
|
|
|
|
+ d.y = targetTagY - halfGap;
|
|
|
|
|
+ }} else if (d.data.nodeType === "标签") {{
|
|
|
|
|
+ // 标签层对齐帖子标签圆心
|
|
|
|
|
+ d.y = targetTagY;
|
|
|
|
|
+ }} else if (d.data.nodeType === "人设") {{
|
|
|
|
|
+ // 人设层对齐人设匹配_1层圆心
|
|
|
|
|
+ d.y = targetPersonaY;
|
|
|
|
|
+ }} else if (d.data.nodeType === "人设扩展") {{
|
|
|
|
|
+ // 扩展层对齐人设匹配_2层圆心
|
|
|
|
|
+ d.y = targetExpandedY;
|
|
|
|
|
+ }}
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 更新边界(因为Y坐标变了)
|
|
|
|
|
+ treeMinY = Infinity; treeMaxY = -Infinity;
|
|
|
|
|
+ postRoot.descendants().forEach(d => {{
|
|
|
|
|
+ treeMinY = Math.min(treeMinY, d.y);
|
|
|
|
|
+ treeMaxY = Math.max(treeMaxY, d.y);
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 树放在圆的右侧,确保树的最左边与圆的最右边有间隔
|
|
|
|
|
+ const circleRightEdge = circleAreaCenterX + unifiedRadius;
|
|
|
|
|
+ const treeGap = 120; // 圆右边与树左边的间隔
|
|
|
|
|
+ const postTreeOffsetX = circleRightEdge + treeGap - treeMinX;
|
|
|
|
|
+
|
|
|
|
|
+ // Y方向不需要额外偏移,因为已经直接设置了目标Y坐标
|
|
|
|
|
+ const postTreeOffsetY = 0;
|
|
|
|
|
|
|
|
// 绘制帖子树边(属于边,紫色,粗细根据权重)
|
|
// 绘制帖子树边(属于边,紫色,粗细根据权重)
|
|
|
const postTreeLinks = postTreeGroup.selectAll(".post-tree-link")
|
|
const postTreeLinks = postTreeGroup.selectAll(".post-tree-link")
|
|
@@ -2067,21 +2126,64 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
${{d.target.x}},${{d.target.y}}`;
|
|
${{d.target.x}},${{d.target.y}}`;
|
|
|
}})
|
|
}})
|
|
|
.attr("fill", "none")
|
|
.attr("fill", "none")
|
|
|
- .attr("stroke", "#9b59b6")
|
|
|
|
|
|
|
+ .attr("stroke", d => {{
|
|
|
|
|
+ // 边颜色与圆形图统一
|
|
|
|
|
+ if (d.target.data.nodeType === "人设") {{
|
|
|
|
|
+ // 匹配边用红色
|
|
|
|
|
+ return "#e94560";
|
|
|
|
|
+ }}
|
|
|
|
|
+ if (d.target.data.nodeType === "人设扩展") {{
|
|
|
|
|
+ // 扩展边根据边类型决定颜色(与圆形图一致)
|
|
|
|
|
+ const edgeType = d.target.data.edgeType || "";
|
|
|
|
|
+ const edgeColors = {{
|
|
|
|
|
+ "属于": "#9b59b6", // 紫色
|
|
|
|
|
+ "包含": "#ffb6c1", // 淡粉
|
|
|
|
|
+ "分类共现(跨点)": "#2ecc71", // 绿色
|
|
|
|
|
+ "分类共现(点内)": "#3498db", // 蓝色
|
|
|
|
|
+ "标签共现": "#f39c12" // 橙色
|
|
|
|
|
+ }};
|
|
|
|
|
+ return edgeColors[edgeType] || "#9b59b6";
|
|
|
|
|
+ }}
|
|
|
|
|
+ return "#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 => {{
|
|
|
|
|
+ // 相似匹配用虚线
|
|
|
|
|
+ if (d.target.data.nodeType === "人设" && d.target.data.matchType === "相似") {{
|
|
|
|
|
+ return "4,2";
|
|
|
|
|
+ }}
|
|
|
|
|
+ return "none";
|
|
|
|
|
+ }});
|
|
|
|
|
+
|
|
|
|
|
+ // 只显示权重和相似度分数,直接在边的中间显示
|
|
|
|
|
|
|
|
- // 在点->标签的边上显示权重(浮点数)
|
|
|
|
|
|
|
+ // 在点->标签的边上显示权重(居中)
|
|
|
postTreeLinks.filter(d => d.target.data.nodeType === "标签" && d.target.data.weight > 0)
|
|
postTreeLinks.filter(d => d.target.data.nodeType === "标签" && d.target.data.weight > 0)
|
|
|
.append("text")
|
|
.append("text")
|
|
|
.attr("x", d => (d.source.x + d.target.x) / 2)
|
|
.attr("x", d => (d.source.x + d.target.x) / 2)
|
|
|
.attr("y", d => (d.source.y + d.target.y) / 2)
|
|
.attr("y", d => (d.source.y + d.target.y) / 2)
|
|
|
|
|
+ .attr("dy", "0.35em")
|
|
|
.attr("text-anchor", "middle")
|
|
.attr("text-anchor", "middle")
|
|
|
- .attr("fill", "#9b59b6")
|
|
|
|
|
- .attr("font-size", "9px")
|
|
|
|
|
|
|
+ .attr("fill", "#fff")
|
|
|
|
|
+ .attr("font-size", "8px")
|
|
|
.attr("font-weight", "bold")
|
|
.attr("font-weight", "bold")
|
|
|
|
|
+ .style("text-shadow", "0 0 3px #9b59b6, 0 0 3px #9b59b6")
|
|
|
.text(d => d.target.data.weight.toFixed(1));
|
|
.text(d => d.target.data.weight.toFixed(1));
|
|
|
|
|
|
|
|
|
|
+ // 在标签->人设的边上显示相似度分数(居中)
|
|
|
|
|
+ postTreeLinks.filter(d => d.target.data.nodeType === "人设" && d.target.data.similarity > 0)
|
|
|
|
|
+ .append("text")
|
|
|
|
|
+ .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 #e94560, 0 0 3px #e94560")
|
|
|
|
|
+ .text(d => d.target.data.similarity.toFixed(2))
|
|
|
|
|
+
|
|
|
// 六边形路径生成函数
|
|
// 六边形路径生成函数
|
|
|
function hexagonPath(r) {{
|
|
function hexagonPath(r) {{
|
|
|
const a = Math.PI / 3;
|
|
const a = Math.PI / 3;
|
|
@@ -2103,36 +2205,78 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
.attr("class", "post-tree-node")
|
|
.attr("class", "post-tree-node")
|
|
|
.attr("transform", d => `translate(${{d.x}},${{d.y}})`);
|
|
.attr("transform", d => `translate(${{d.x}},${{d.y}})`);
|
|
|
|
|
|
|
|
- // 节点形状(维度=大圆,点=六边形,标签=小圆)
|
|
|
|
|
|
|
+ // 节点形状和大小(与圆形图统一)
|
|
|
|
|
+ // 点节点:六边形 size=16,标签:圆形 size=12,人设:圆形 size=10
|
|
|
postTreeNodes.each(function(d) {{
|
|
postTreeNodes.each(function(d) {{
|
|
|
const node = d3.select(this);
|
|
const node = d3.select(this);
|
|
|
|
|
+ const fill = d.data.dimColor || "#888";
|
|
|
|
|
+
|
|
|
if (d.data.nodeType === "点") {{
|
|
if (d.data.nodeType === "点") {{
|
|
|
- // 点节点用六边形
|
|
|
|
|
|
|
+ // 点节点用六边形,size=16(与圆形图一致)
|
|
|
node.append("path")
|
|
node.append("path")
|
|
|
- .attr("d", hexagonPath(6))
|
|
|
|
|
- .attr("fill", d.data.dimColor || "#888")
|
|
|
|
|
|
|
+ .attr("d", hexagonPath(16))
|
|
|
|
|
+ .attr("fill", fill)
|
|
|
.attr("stroke", "#fff")
|
|
.attr("stroke", "#fff")
|
|
|
- .attr("stroke-width", 1);
|
|
|
|
|
|
|
+ .attr("stroke-width", 2);
|
|
|
|
|
+ }} else if (d.data.isDimension) {{
|
|
|
|
|
+ // 维度节点用大圆
|
|
|
|
|
+ node.append("circle")
|
|
|
|
|
+ .attr("r", 14)
|
|
|
|
|
+ .attr("fill", fill)
|
|
|
|
|
+ .attr("stroke", "#fff")
|
|
|
|
|
+ .attr("stroke-width", 2);
|
|
|
|
|
+ }} else if (d.data.nodeType === "标签") {{
|
|
|
|
|
+ // 标签节点,size=12(与圆形图帖子标签一致)
|
|
|
|
|
+ node.append("circle")
|
|
|
|
|
+ .attr("r", 12)
|
|
|
|
|
+ .attr("fill", fill)
|
|
|
|
|
+ .attr("stroke", "#fff")
|
|
|
|
|
+ .attr("stroke-width", 2);
|
|
|
|
|
+ }} else if (d.data.nodeType === "人设" || d.data.nodeType === "人设扩展") {{
|
|
|
|
|
+ // 人设节点和扩展节点,根据原始类型决定形状(与圆形图一致)
|
|
|
|
|
+ const size = 10;
|
|
|
|
|
+ if (d.data.originalType === "分类") {{
|
|
|
|
|
+ // 分类用方形
|
|
|
|
|
+ node.append("rect")
|
|
|
|
|
+ .attr("width", size * 2)
|
|
|
|
|
+ .attr("height", size * 2)
|
|
|
|
|
+ .attr("x", -size)
|
|
|
|
|
+ .attr("y", -size)
|
|
|
|
|
+ .attr("fill", fill)
|
|
|
|
|
+ .attr("stroke", "#fff")
|
|
|
|
|
+ .attr("stroke-width", 2)
|
|
|
|
|
+ .attr("rx", 3);
|
|
|
|
|
+ }} else {{
|
|
|
|
|
+ // 标签用圆形
|
|
|
|
|
+ node.append("circle")
|
|
|
|
|
+ .attr("r", size)
|
|
|
|
|
+ .attr("fill", fill)
|
|
|
|
|
+ .attr("stroke", "#fff")
|
|
|
|
|
+ .attr("stroke-width", 2);
|
|
|
|
|
+ }}
|
|
|
}} else {{
|
|
}} else {{
|
|
|
- // 维度和标签用圆形
|
|
|
|
|
|
|
+ // 其他节点默认
|
|
|
node.append("circle")
|
|
node.append("circle")
|
|
|
- .attr("r", d.data.isDimension ? 7 : 3)
|
|
|
|
|
- .attr("fill", d.data.dimColor || "#888")
|
|
|
|
|
|
|
+ .attr("r", 8)
|
|
|
|
|
+ .attr("fill", fill)
|
|
|
.attr("stroke", "#fff")
|
|
.attr("stroke", "#fff")
|
|
|
.attr("stroke-width", 1);
|
|
.attr("stroke-width", 1);
|
|
|
}}
|
|
}}
|
|
|
}});
|
|
}});
|
|
|
|
|
|
|
|
- // 节点标签
|
|
|
|
|
|
|
+ // 节点标签(统一放在节点上方)
|
|
|
postTreeNodes.append("text")
|
|
postTreeNodes.append("text")
|
|
|
- .attr("dy", d => d.children ? -10 : 12)
|
|
|
|
|
|
|
+ .attr("dy", d => {{
|
|
|
|
|
+ // 根据节点类型调整偏移量,统一放在节点上方
|
|
|
|
|
+ if (d.data.isDimension) return -22;
|
|
|
|
|
+ if (d.data.nodeType === "点") return -24;
|
|
|
|
|
+ if (d.data.nodeType === "标签") return -20;
|
|
|
|
|
+ if (d.data.nodeType === "人设" || d.data.nodeType === "人设扩展") return -18;
|
|
|
|
|
+ return -18;
|
|
|
|
|
+ }})
|
|
|
.attr("text-anchor", "middle")
|
|
.attr("text-anchor", "middle")
|
|
|
.attr("fill", d => d.data.dimColor || "#ccc")
|
|
.attr("fill", d => d.data.dimColor || "#ccc")
|
|
|
- .attr("font-size", d => {{
|
|
|
|
|
- if (d.data.isDimension) return "11px";
|
|
|
|
|
- if (d.data.nodeType === "点") return "10px";
|
|
|
|
|
- return "8px";
|
|
|
|
|
- }})
|
|
|
|
|
|
|
+ .attr("font-size", "11px")
|
|
|
.attr("font-weight", d => d.data.isDimension ? "bold" : "normal")
|
|
.attr("font-weight", d => d.data.isDimension ? "bold" : "normal")
|
|
|
.text(d => d.data.name || "");
|
|
.text(d => d.data.name || "");
|
|
|
|
|
|
|
@@ -2178,19 +2322,25 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|
|
rootCard.append("xhtml:div").html(cardHtml);
|
|
rootCard.append("xhtml:div").html(cardHtml);
|
|
|
|
|
|
|
|
postTreeActualHeight = treeMaxY - treeMinY;
|
|
postTreeActualHeight = treeMaxY - treeMinY;
|
|
|
|
|
+
|
|
|
|
|
+ // 应用帖子树的位置变换(放在圆的右侧)
|
|
|
|
|
+ postTreeGroup.attr("transform", `translate(${{postTreeOffsetX}}, ${{postTreeOffsetY}})`);
|
|
|
}} // end if (postTree && postTree.root)
|
|
}} // end if (postTree && postTree.root)
|
|
|
|
|
|
|
|
- // 根据帖子树高度,调整圆的Y位置
|
|
|
|
|
- const postTreeTotalHeight = postTreeOffsetY + postTreeActualHeight + 80;
|
|
|
|
|
- const circleYOffset = postTreeTotalHeight;
|
|
|
|
|
|
|
+ // 圆的Y位置不再受帖子树高度影响,使用固定偏移
|
|
|
|
|
+ const circleYOffset = 40;
|
|
|
|
|
|
|
|
// 更新圆心Y坐标(三层)
|
|
// 更新圆心Y坐标(三层)
|
|
|
layerCenterY[0] += circleYOffset;
|
|
layerCenterY[0] += circleYOffset;
|
|
|
layerCenterY[1] += circleYOffset;
|
|
layerCenterY[1] += circleYOffset;
|
|
|
layerCenterY[2] += circleYOffset;
|
|
layerCenterY[2] += circleYOffset;
|
|
|
|
|
|
|
|
- // 更新SVG高度
|
|
|
|
|
- svg.attr("height", height + circleYOffset);
|
|
|
|
|
|
|
+ // 更新SVG高度(取圆和帖子树的最大高度)
|
|
|
|
|
+ const minHeight = Math.max(
|
|
|
|
|
+ layerCenterY[2] + layerRadius[2] + 50,
|
|
|
|
|
+ postTreeActualHeight + 150
|
|
|
|
|
+ );
|
|
|
|
|
+ svg.attr("height", Math.max(height, minHeight));
|
|
|
|
|
|
|
|
// 绘制层背景(圆形,使用动态大小)
|
|
// 绘制层背景(圆形,使用动态大小)
|
|
|
const layerBg = g.append("g").attr("class", "layer-backgrounds");
|
|
const layerBg = g.append("g").attr("class", "layer-backgrounds");
|