You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
430 lines
18 KiB
430 lines
18 KiB
|
|
<!DOCTYPE html>
|
|
<html lang="zh">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>刘邦集团人物关系网络</title>
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
background: linear-gradient(135deg, #0f2027, #203a43, #2c5364);
|
|
font-family: "Microsoft YaHei", sans-serif;
|
|
color: white;
|
|
}
|
|
|
|
#container {
|
|
display: flex;
|
|
height: 100vh;
|
|
}
|
|
|
|
#graph-container {
|
|
flex: 1;
|
|
position: relative;
|
|
}
|
|
|
|
#info-panel {
|
|
width: 300px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
border-left: 1px solid #444;
|
|
}
|
|
|
|
.node {
|
|
stroke: #fff;
|
|
stroke-width: 1.5px;
|
|
filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.3));
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.node:hover {
|
|
stroke-width: 3px;
|
|
filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.6));
|
|
}
|
|
|
|
.link {
|
|
stroke-opacity: 0.6;
|
|
stroke-width: 2px;
|
|
}
|
|
|
|
.legend {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 20px;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.control-panel {
|
|
position: absolute;
|
|
top: 20px;
|
|
right: 330px;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
z-index: 100;
|
|
}
|
|
|
|
button {
|
|
background: #2c5364;
|
|
color: white;
|
|
border: none;
|
|
padding: 5px 10px;
|
|
margin: 5px;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
button:hover {
|
|
background: #3a7bd5;
|
|
}
|
|
|
|
.info-title {
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
margin-bottom: 10px;
|
|
color: #3a7bd5;
|
|
}
|
|
|
|
.info-content {
|
|
margin-bottom: 15px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="container">
|
|
<div id="graph-container">
|
|
<div class="control-panel">
|
|
<button onclick="changeLayout('force')">力导向布局</button>
|
|
<button onclick="changeLayout('circular')">环形布局</button>
|
|
<button onclick="changeLayout('radial')">辐射布局</button>
|
|
<button onclick="changeLayout('hierarchical')">层级布局</button>
|
|
</div>
|
|
<div class="legend">
|
|
<div><svg width="20" height="10"><line x1="0" y1="5" x2="20" y2="5" stroke="#ff7f0e" stroke-width="2"/></svg> 政治联盟</div>
|
|
<div><svg width="20" height="10"><line x1="0" y1="5" x2="20" y2="5" stroke="#1f77b4" stroke-width="2"/></svg> 军事合作</div>
|
|
<div><svg width="20" height="10"><line x1="0" y1="5" x2="20" y2="5" stroke="#2ca02c" stroke-width="2"/></svg> 亲属关系</div>
|
|
<div><svg width="20" height="10"><line x1="0" y1="5" x2="20" y2="5" stroke="#d62728" stroke-width="2"/></svg> 敌对关系</div>
|
|
</div>
|
|
</div>
|
|
<div id="info-panel">
|
|
<div class="info-title">刘邦集团人物关系图</div>
|
|
<div class="info-content">点击节点查看详细信息</div>
|
|
<div id="node-info"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const width = window.innerWidth - 300;
|
|
const height = window.innerHeight;
|
|
|
|
const data = {
|
|
nodes: [
|
|
{ id: "刘邦", name: "刘邦", type: "emperor", desc: "汉朝开国皇帝", importance: 10 },
|
|
{ id: "吕后", name: "吕后", type: "empress", desc: "刘邦的皇后,汉朝实际掌权者", importance: 9 },
|
|
{ id: "项羽", name: "项羽", type: "rival", desc: "刘邦的主要对手", importance: 8 },
|
|
{ id: "韩信", name: "韩信", type: "general", desc: "汉朝开国功臣,军事家", importance: 9 },
|
|
{ id: "张良", name: "张良", type: "advisor", desc: "刘邦的重要谋士", importance: 8 },
|
|
{ id: "萧何", name: "萧何", type: "minister", desc: "汉朝丞相,治国能臣", importance: 8 },
|
|
{ id: "项伯", name: "项伯", type: "relative", desc: "项羽的叔父,暗中帮助刘邦", importance: 6 },
|
|
{ id: "曹无伤", name: "曹无伤", type: "traitor", desc: "刘邦部下,背叛刘邦", importance: 4 },
|
|
{ id: "项庄", name: "项庄", type: "general", desc: "项羽部下,鸿门宴舞剑", importance: 5 },
|
|
{ id: "英布", name: "英布", type: "general", desc: "汉初名将,后反叛", importance: 7 },
|
|
{ id: "彭越", name: "彭越", type: "general", desc: "汉初名将,后被杀", importance: 6 },
|
|
{ id: "刘濞", name: "刘濞", type: "prince", desc: "刘邦侄子,吴王", importance: 5 },
|
|
{ id: "叔孙通", name: "叔孙通", type: "advisor", desc: "汉朝礼制制定者", importance: 5 },
|
|
{ id: "周勃", name: "周勃", type: "general", desc: "汉朝开国功臣", importance: 6 },
|
|
{ id: "贯高", name: "贯高", type: "official", desc: "赵王张敖的臣子", importance: 3 }
|
|
],
|
|
links: [
|
|
{ source: "刘邦", target: "吕后", type: "spouse", value: 5, desc: "夫妻关系,后期矛盾激化" },
|
|
{ source: "刘邦", target: "项羽", type: "enemy", value: 8, desc: "楚汉相争的主要对手" },
|
|
{ source: "刘邦", target: "韩信", type: "military", value: 7, desc: "军事重用与猜忌" },
|
|
{ source: "刘邦", target: "张良", type: "political", value: 8, desc: "重要谋士与信任" },
|
|
{ source: "刘邦", target: "萧何", type: "political", value: 8, desc: "治国能臣与猜疑" },
|
|
{ source: "刘邦", target: "项伯", type: "political", value: 5, desc: "鸿门宴中的帮助" },
|
|
{ source: "刘邦", target: "曹无伤", type: "enemy", value: 3, desc: "背叛后被处死" },
|
|
{ source: "刘邦", target: "英布", type: "military", value: 6, desc: "军事重用与后期反叛" },
|
|
{ source: "刘邦", target: "彭越", type: "military", value: 5, desc: "军事合作后被诛杀" },
|
|
{ source: "刘邦", target: "刘濞", type: "family", value: 4, desc: "分封诸侯与矛盾" },
|
|
{ source: "刘邦", target: "叔孙通", type: "political", value: 5, desc: "礼制改革" },
|
|
{ source: "刘邦", target: "周勃", type: "military", value: 6, desc: "开国功臣" },
|
|
{ source: "刘邦", target: "贯高", type: "enemy", value: 2, desc: "刺杀未遂" },
|
|
{ source: "项羽", target: "项伯", type: "family", value: 5, desc: "叔侄关系" },
|
|
{ source: "项羽", target: "项庄", type: "military", value: 5, desc: "部下将领" },
|
|
{ source: "吕后", target: "韩信", type: "enemy", value: 6, desc: "设计杀害" },
|
|
{ source: "吕后", target: "彭越", type: "enemy", value: 5, desc: "设计杀害" },
|
|
{ source: "吕后", target: "刘濞", type: "family", value: 3, desc: "宗室关系" }
|
|
]
|
|
};
|
|
|
|
const colorScale = d3.scaleOrdinal()
|
|
.domain(["emperor", "empress", "rival", "general", "advisor", "minister", "relative", "traitor", "prince", "official"])
|
|
.range(["#ffd700", "#ff69b4", "#ff4500", "#1e90ff", "#32cd32", "#9370db", "#ffa500", "#a9a9a9", "#4169e1", "#2e8b57"]);
|
|
|
|
const linkColorScale = d3.scaleOrdinal()
|
|
.domain(["political", "military", "family", "spouse", "enemy"])
|
|
.range(["#ff7f0e", "#1f77b4", "#2ca02c", "#9467bd", "#d62728"]);
|
|
|
|
const svg = d3.select("#graph-container")
|
|
.append("svg")
|
|
.attr("width", width)
|
|
.attr("height", height);
|
|
|
|
const simulation = d3.forceSimulation(data.nodes)
|
|
.force("link", d3.forceLink(data.links).id(d => d.id).distance(d => 150 - d.value * 10))
|
|
.force("charge", d3.forceManyBody().strength(-500))
|
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
.force("collision", d3.forceCollide().radius(d => Math.sqrt(d.importance) * 8));
|
|
|
|
const link = svg.append("g")
|
|
.selectAll("line")
|
|
.data(data.links)
|
|
.enter().append("line")
|
|
.attr("class", "link")
|
|
.attr("stroke", d => linkColorScale(d.type))
|
|
.attr("stroke-width", d => Math.sqrt(d.value));
|
|
|
|
const node = svg.append("g")
|
|
.selectAll("circle")
|
|
.data(data.nodes)
|
|
.enter().append("circle")
|
|
.attr("class", "node")
|
|
.attr("r", d => Math.sqrt(d.importance) * 6)
|
|
.attr("fill", d => colorScale(d.type))
|
|
.call(d3.drag()
|
|
.on("start", dragstarted)
|
|
.on("drag", dragged)
|
|
.on("end", dragended))
|
|
.on("mouseover", function(event, d) {
|
|
d3.select(this).attr("stroke-width", 3).attr("stroke", "#fff");
|
|
showNodeInfo(d);
|
|
})
|
|
.on("mouseout", function(event, d) {
|
|
d3.select(this).attr("stroke-width", 1.5);
|
|
})
|
|
.on("click", function(event, d) {
|
|
showNodeInfo(d);
|
|
});
|
|
|
|
const label = svg.append("g")
|
|
.selectAll("text")
|
|
.data(data.nodes)
|
|
.enter().append("text")
|
|
.attr("dy", -10)
|
|
.attr("text-anchor", "middle")
|
|
.text(d => d.name)
|
|
.attr("fill", "white")
|
|
.attr("font-size", d => Math.sqrt(d.importance) * 3 + "px");
|
|
|
|
simulation.on("tick", () => {
|
|
link
|
|
.attr("x1", d => d.source.x)
|
|
.attr("y1", d => d.source.y)
|
|
.attr("x2", d => d.target.x)
|
|
.attr("y2", d => d.target.y);
|
|
|
|
node
|
|
.attr("cx", d => d.x)
|
|
.attr("cy", d => d.y);
|
|
|
|
label
|
|
.attr("x", d => d.x)
|
|
.attr("y", d => d.y);
|
|
});
|
|
|
|
function dragstarted(event, d) {
|
|
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
d.fx = d.x;
|
|
d.fy = d.y;
|
|
}
|
|
|
|
function dragged(event, d) {
|
|
d.fx = event.x;
|
|
d.fy = event.y;
|
|
}
|
|
|
|
function dragended(event, d) {
|
|
if (!event.active) simulation.alphaTarget(0);
|
|
d.fx = null;
|
|
d.fy = null;
|
|
}
|
|
|
|
function showNodeInfo(d) {
|
|
const connections = data.links.filter(link => link.source.id === d.id || link.target.id === d.id);
|
|
const connectedNodes = connections.map(link => {
|
|
return link.source.id === d.id ? data.nodes.find(n => n.id === link.target.id) :
|
|
data.nodes.find(n => n.id === link.source.id);
|
|
});
|
|
|
|
let infoHTML = `
|
|
<div class="info-title">${d.name}</div>
|
|
<div class="info-content"><strong>类型:</strong> ${getTypeName(d.type)}</div>
|
|
<div class="info-content"><strong>重要性:</strong> ${d.importance}/10</div>
|
|
<div class="info-content"><strong>描述:</strong> ${d.desc}</div>
|
|
<div class="info-content"><strong>关联人物 (${connections.length}):</strong><br>`;
|
|
|
|
connections.forEach(link => {
|
|
const otherNode = link.source.id === d.id ? data.nodes.find(n => n.id === link.target.id) :
|
|
data.nodes.find(n => n.id === link.source.id);
|
|
const relationType = getRelationTypeName(link.type);
|
|
infoHTML += `<div style="margin-top:5px;">
|
|
<svg width="15" height="10"><line x1="0" y1="5" x2="15" y2="5" stroke="${linkColorScale(link.type)}" stroke-width="2"/></svg>
|
|
${otherNode.name} (${relationType})</div>`;
|
|
});
|
|
|
|
infoHTML += `</div>`;
|
|
|
|
document.getElementById("node-info").innerHTML = infoHTML;
|
|
}
|
|
|
|
function getTypeName(type) {
|
|
const typeNames = {
|
|
"emperor": "皇帝",
|
|
"empress": "皇后",
|
|
"rival": "对手",
|
|
"general": "将领",
|
|
"advisor": "谋士",
|
|
"minister": "大臣",
|
|
"relative": "亲属",
|
|
"traitor": "叛徒",
|
|
"prince": "诸侯王",
|
|
"official": "官员"
|
|
};
|
|
return typeNames[type] || type;
|
|
}
|
|
|
|
function getRelationTypeName(type) {
|
|
const typeNames = {
|
|
"political": "政治联盟",
|
|
"military": "军事合作",
|
|
"family": "亲属关系",
|
|
"spouse": "夫妻关系",
|
|
"enemy": "敌对关系"
|
|
};
|
|
return typeNames[type] || type;
|
|
}
|
|
|
|
function changeLayout(layoutType) {
|
|
simulation.force("link", d3.forceLink(data.links).id(d => d.id).distance(d => 150 - d.value * 10));
|
|
|
|
if (layoutType === 'circular') {
|
|
const radius = Math.min(width, height) / 3;
|
|
const angle = (2 * Math.PI) / data.nodes.length;
|
|
|
|
data.nodes.forEach((node, i) => {
|
|
node.x = width / 2 + radius * Math.cos(i * angle);
|
|
node.y = height / 2 + radius * Math.sin(i * angle);
|
|
node.fx = node.x;
|
|
node.fy = node.y;
|
|
});
|
|
|
|
simulation.alpha(1).restart();
|
|
setTimeout(() => {
|
|
data.nodes.forEach(node => {
|
|
node.fx = null;
|
|
node.fy = null;
|
|
});
|
|
}, 1000);
|
|
} else if (layoutType === 'radial') {
|
|
const centerNode = data.nodes.find(n => n.id === "刘邦");
|
|
centerNode.fx = width / 2;
|
|
centerNode.fy = height / 2;
|
|
|
|
const radius = Math.min(width, height) / 3;
|
|
const angle = (2 * Math.PI) / (data.nodes.length - 1);
|
|
|
|
let i = 0;
|
|
data.nodes.forEach(node => {
|
|
if (node.id !== "刘邦") {
|
|
node.x = width / 2 + radius * Math.cos(i * angle);
|
|
node.y = height / 2 + radius * Math.sin(i * angle);
|
|
node.fx = node.x;
|
|
node.fy = node.y;
|
|
i++;
|
|
}
|
|
});
|
|
|
|
simulation.alpha(1).restart();
|
|
setTimeout(() => {
|
|
data.nodes.forEach(node => {
|
|
if (node.id !== "刘邦") {
|
|
node.fx = null;
|
|
node.fy = null;
|
|
}
|
|
});
|
|
}, 1000);
|
|
} else if (layoutType === 'hierarchical') {
|
|
const levels = {
|
|
"emperor": 0,
|
|
"empress": 1,
|
|
"rival": 1,
|
|
"general": 2,
|
|
"advisor": 2,
|
|
"minister": 2,
|
|
"relative": 3,
|
|
"traitor": 3,
|
|
"prince": 3,
|
|
"official": 3
|
|
};
|
|
|
|
const levelCounts = {};
|
|
data.nodes.forEach(node => {
|
|
const level = levels[node.type] || 0;
|
|
levelCounts[level] = (levelCounts[level] || 0) + 1;
|
|
});
|
|
|
|
const levelPositions = {};
|
|
Object.keys(levelCounts).forEach(level => {
|
|
levelPositions[level] = {
|
|
x: width / (Object.keys(levelCounts).length + 1) * (parseInt(level) + 1),
|
|
count: 0
|
|
};
|
|
});
|
|
|
|
data.nodes.forEach(node => {
|
|
const level = levels[node.type] || 0;
|
|
const levelInfo = levelPositions[level];
|
|
const spacing = height / (levelCounts[level] + 1);
|
|
|
|
node.x = levelInfo.x;
|
|
node.y = spacing * (levelInfo.count + 1);
|
|
node.fx = node.x;
|
|
node.fy = node.y;
|
|
|
|
levelInfo.count++;
|
|
});
|
|
|
|
simulation.alpha(1).restart();
|
|
setTimeout(() => {
|
|
data.nodes.forEach(node => {
|
|
node.fx = null;
|
|
node.fy = null;
|
|
});
|
|
}, 1000);
|
|
} else {
|
|
// Force layout
|
|
data.nodes.forEach(node => {
|
|
node.fx = null;
|
|
node.fy = null;
|
|
});
|
|
simulation.alpha(1).restart();
|
|
}
|
|
}
|
|
|
|
window.addEventListener("resize", function() {
|
|
const newWidth = window.innerWidth - 300;
|
|
const newHeight = window.innerHeight;
|
|
|
|
svg.attr("width", newWidth).attr("height", newHeight);
|
|
simulation.force("center", d3.forceCenter(newWidth / 2, newHeight / 2));
|
|
simulation.alpha(1).restart();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|