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.
330 lines
13 KiB
330 lines
13 KiB
```html
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>刘邦集团人物关系网络</title>
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
background: linear-gradient(135deg, #0a192f, #172a45);
|
|
font-family: "SimSun", serif;
|
|
color: #e6f1ff;
|
|
overflow: hidden;
|
|
}
|
|
#container {
|
|
display: flex;
|
|
height: 100vh;
|
|
}
|
|
#graph {
|
|
flex: 1;
|
|
position: relative;
|
|
}
|
|
#panel {
|
|
width: 300px;
|
|
background: rgba(10, 25, 47, 0.8);
|
|
border-left: 1px solid #64ffda;
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
}
|
|
.node {
|
|
stroke: #fff;
|
|
stroke-width: 1.5px;
|
|
filter: drop-shadow(0 0 5px rgba(100, 255, 218, 0.5));
|
|
}
|
|
.link {
|
|
stroke: #495670;
|
|
stroke-opacity: 0.6;
|
|
}
|
|
.legend {
|
|
margin-bottom: 15px;
|
|
padding: 10px;
|
|
background: rgba(23, 42, 69, 0.5);
|
|
border-radius: 5px;
|
|
}
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
margin: 5px 0;
|
|
}
|
|
.legend-color {
|
|
width: 15px;
|
|
height: 15px;
|
|
margin-right: 10px;
|
|
border-radius: 3px;
|
|
}
|
|
.panel-title {
|
|
color: #64ffda;
|
|
border-bottom: 1px solid #64ffda;
|
|
padding-bottom: 10px;
|
|
margin-bottom: 15px;
|
|
}
|
|
.toggle-btn {
|
|
position: absolute;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: rgba(100, 255, 218, 0.2);
|
|
color: #64ffda;
|
|
border: 1px solid #64ffda;
|
|
padding: 5px 10px;
|
|
cursor: pointer;
|
|
z-index: 100;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="container">
|
|
<div id="graph"></div>
|
|
<div id="panel">
|
|
<h2 class="panel-title">人物详情</h2>
|
|
<div id="info-content">
|
|
<p>点击节点查看人物详情</p>
|
|
</div>
|
|
<div class="legend">
|
|
<h3>关系类型图例</h3>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background-color: #ff5555;"></div>
|
|
<span>敌对关系</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background-color: #55ff55;"></div>
|
|
<span>联盟关系</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background-color: #5555ff;"></div>
|
|
<span>君臣关系</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background-color: #ffff55;"></div>
|
|
<span>家族关系</span>
|
|
</div>
|
|
</div>
|
|
<div class="legend">
|
|
<h3>布局切换</h3>
|
|
<button onclick="changeLayout('force')">力导向布局</button>
|
|
<button onclick="changeLayout('radial')">辐射布局</button>
|
|
<button onclick="changeLayout('hierarchical')">层级布局</button>
|
|
<button onclick="changeLayout('circular')">环形布局</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const width = document.getElementById('graph').clientWidth;
|
|
const height = document.getElementById('graph').clientHeight;
|
|
|
|
const data = {
|
|
nodes: [
|
|
{ id: "刘邦", group: 1, size: 30, desc: "汉高祖刘邦,汉朝开国皇帝" },
|
|
{ id: "吕后", group: 1, size: 25, desc: "刘邦妻子,汉朝第一位皇后" },
|
|
{ id: "项羽", group: 2, size: 28, desc: "西楚霸王,刘邦主要对手" },
|
|
{ id: "韩信", group: 3, size: 22, desc: "汉初三杰之一,著名军事家" },
|
|
{ id: "萧何", group: 3, size: 22, desc: "汉初三杰之一,政治家" },
|
|
{ id: "张良", group: 3, size: 22, desc: "汉初三杰之一,谋略家" },
|
|
{ id: "项伯", group: 2, size: 18, desc: "项羽叔父,曾帮助刘邦" },
|
|
{ id: "曹无伤", group: 2, size: 15, desc: "刘邦部下,背叛刘邦" },
|
|
{ id: "项庄", group: 2, size: 16, desc: "项羽堂弟,鸿门宴舞剑" },
|
|
{ id: "叔孙通", group: 3, size: 18, desc: "制定汉朝礼仪制度" },
|
|
{ id: "刘姓诸侯", group: 4, size: 20, desc: "刘邦分封的同姓诸侯" },
|
|
{ id: "吕氏家族", group: 4, size: 20, desc: "吕后家族成员" }
|
|
],
|
|
links: [
|
|
{ source: "刘邦", target: "吕后", value: 3, type: "family" },
|
|
{ source: "刘邦", target: "项羽", value: 5, type: "enemy" },
|
|
{ source: "刘邦", target: "韩信", value: 4, type: "loyalty" },
|
|
{ source: "刘邦", target: "萧何", value: 4, type: "loyalty" },
|
|
{ source: "刘邦", target: "张良", value: 4, type: "loyalty" },
|
|
{ source: "刘邦", target: "项伯", value: 2, type: "alliance" },
|
|
{ source: "刘邦", target: "曹无伤", value: 1, type: "enemy" },
|
|
{ source: "刘邦", target: "叔孙通", value: 2, type: "loyalty" },
|
|
{ source: "刘邦", target: "刘姓诸侯", value: 3, type: "family" },
|
|
{ source: "吕后", target: "吕氏家族", value: 4, type: "family" },
|
|
{ source: "项羽", target: "项伯", value: 3, type: "family" },
|
|
{ source: "项羽", target: "项庄", value: 3, type: "family" },
|
|
{ source: "项羽", target: "曹无伤", value: 2, type: "alliance" },
|
|
{ source: "吕后", target: "韩信", value: 1, type: "enemy" },
|
|
{ source: "刘姓诸侯", target: "吕氏家族", value: 2, type: "enemy" }
|
|
]
|
|
};
|
|
|
|
const color = d3.scaleOrdinal()
|
|
.domain(["enemy", "alliance", "loyalty", "family"])
|
|
.range(["#ff5555", "#55ff55", "#5555ff", "#ffff55"]);
|
|
|
|
const svg = d3.select("#graph")
|
|
.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 => d.value * 30))
|
|
.force("charge", d3.forceManyBody().strength(-500))
|
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
.force("collision", d3.forceCollide().radius(d => d.size));
|
|
|
|
const link = svg.append("g")
|
|
.selectAll("line")
|
|
.data(data.links)
|
|
.enter().append("line")
|
|
.attr("class", "link")
|
|
.attr("stroke", d => color(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 => d.size)
|
|
.attr("fill", d => {
|
|
if (d.id === "刘邦") return "#ff6600";
|
|
if (d.group === 1) return "#33cc33";
|
|
if (d.group === 2) return "#cc3333";
|
|
if (d.group === 3) return "#3366cc";
|
|
return "#cccc33";
|
|
})
|
|
.call(d3.drag()
|
|
.on("start", dragstarted)
|
|
.on("drag", dragged)
|
|
.on("end", dragended))
|
|
.on("mouseover", function(event, d) {
|
|
d3.select(this).attr("stroke", "#fff").attr("stroke-width", 2);
|
|
showInfoPanel(d);
|
|
})
|
|
.on("mouseout", function() {
|
|
d3.select(this).attr("stroke", null);
|
|
})
|
|
.on("click", function(event, d) {
|
|
showInfoPanel(d);
|
|
});
|
|
|
|
const label = svg.append("g")
|
|
.selectAll("text")
|
|
.data(data.nodes)
|
|
.enter().append("text")
|
|
.attr("dy", -15)
|
|
.attr("text-anchor", "middle")
|
|
.text(d => d.id)
|
|
.attr("fill", "#e6f1ff");
|
|
|
|
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 showInfoPanel(d) {
|
|
const panel = document.getElementById('info-content');
|
|
panel.innerHTML = `
|
|
<h3>${d.id}</h3>
|
|
<p>${d.desc}</p>
|
|
<p><strong>重要性:</strong> ${d.size/5}星</p>
|
|
<p><strong>所属集团:</strong> ${getGroupName(d.group)}</p>
|
|
<h4>主要关系:</h4>
|
|
<ul>
|
|
${getRelationships(d.id)}
|
|
</ul>
|
|
`;
|
|
}
|
|
|
|
function getGroupName(group) {
|
|
switch(group) {
|
|
case 1: return "刘邦核心集团";
|
|
case 2: return "项羽集团";
|
|
case 3: return "刘邦功臣集团";
|
|
case 4: return "家族势力";
|
|
default: return "其他";
|
|
}
|
|
}
|
|
|
|
function getRelationships(id) {
|
|
let relationships = [];
|
|
data.links.forEach(link => {
|
|
if (link.source.id === id) {
|
|
relationships.push(`<li>与 <strong>${link.target.id}</strong> 的 ${getRelationshipType(link.type)}关系</li>`);
|
|
} else if (link.target.id === id) {
|
|
relationships.push(`<li>与 <strong>${link.source.id}</strong> 的 ${getRelationshipType(link.type)}关系</li>`);
|
|
}
|
|
});
|
|
return relationships.join('');
|
|
}
|
|
|
|
function getRelationshipType(type) {
|
|
switch(type) {
|
|
case "enemy": return "敌对";
|
|
case "alliance": return "联盟";
|
|
case "loyalty": return "君臣";
|
|
case "family": return "家族";
|
|
default: return "其他";
|
|
}
|
|
}
|
|
|
|
function changeLayout(type) {
|
|
simulation.force("link", d3.forceLink(data.links).id(d => d.id).distance(d => d.value * 30));
|
|
|
|
if (type === 'force') {
|
|
simulation.force("charge", d3.forceManyBody().strength(-500));
|
|
simulation.force("center", d3.forceCenter(width / 2, height / 2));
|
|
} else if (type === 'radial') {
|
|
simulation.force("radial", d3.forceRadial(height / 3, width / 2, height / 2).strength(0.1));
|
|
} else if (type === 'hierarchical') {
|
|
simulation.force("x", d3.forceX(width / 2).strength(0.1));
|
|
simulation.force("y", d3.forceY().y(d => {
|
|
if (d.group === 1) return height / 4;
|
|
if (d.group === 2) return height / 4 * 3;
|
|
return height / 2;
|
|
}).strength(0.2));
|
|
} else if (type === 'circular') {
|
|
simulation.force("x", null);
|
|
simulation.force("y", null);
|
|
simulation.force("radial", null);
|
|
data.nodes.forEach((d, i) => {
|
|
const angle = (i / data.nodes.length) * 2 * Math.PI;
|
|
d.x = width / 2 + Math.cos(angle) * width / 3;
|
|
d.y = height / 2 + Math.sin(angle) * height / 3;
|
|
});
|
|
}
|
|
|
|
simulation.alpha(0.3).restart();
|
|
}
|
|
|
|
window.addEventListener('resize', function() {
|
|
const newWidth = document.getElementById('graph').clientWidth;
|
|
const newHeight = document.getElementById('graph').clientHeight;
|
|
svg.attr("width", newWidth).attr("height", newHeight);
|
|
simulation.force("center", d3.forceCenter(newWidth / 2, newHeight / 2));
|
|
simulation.alpha(0.3).restart();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
``` |