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.
428 lines
17 KiB
428 lines
17 KiB
|
|
<!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, #0a1a2e, #1a3a5a);
|
|
font-family: 'Microsoft YaHei', sans-serif;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#container {
|
|
display: flex;
|
|
height: 100vh;
|
|
}
|
|
|
|
#graph {
|
|
flex: 3;
|
|
position: relative;
|
|
}
|
|
|
|
#panel {
|
|
flex: 1;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
backdrop-filter: blur(10px);
|
|
padding: 20px;
|
|
color: white;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.node {
|
|
stroke: #fff;
|
|
stroke-width: 1.5px;
|
|
cursor: pointer;
|
|
filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.3));
|
|
}
|
|
|
|
.node text {
|
|
pointer-events: none;
|
|
text-anchor: middle;
|
|
dominant-baseline: central;
|
|
fill: white;
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.link {
|
|
stroke-opacity: 0.6;
|
|
}
|
|
|
|
.link.military {
|
|
stroke: #ff6b6b;
|
|
}
|
|
|
|
.link.political {
|
|
stroke: #4ecdc4;
|
|
}
|
|
|
|
.link.family {
|
|
stroke: #ffd166;
|
|
}
|
|
|
|
.info-title {
|
|
font-size: 24px;
|
|
margin-bottom: 15px;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.info-description {
|
|
margin-bottom: 20px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.relations-list {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.relation-item {
|
|
margin-bottom: 10px;
|
|
padding: 10px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.relation-type {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
font-size: 12px;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.military-badge {
|
|
background: #ff6b6b;
|
|
}
|
|
|
|
.political-badge {
|
|
background: #4ecdc4;
|
|
}
|
|
|
|
.family-badge {
|
|
background: #ffd166;
|
|
}
|
|
|
|
.tooltip {
|
|
position: absolute;
|
|
padding: 10px;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
color: white;
|
|
border-radius: 5px;
|
|
pointer-events: none;
|
|
max-width: 200px;
|
|
font-size: 14px;
|
|
line-height: 1.4;
|
|
z-index: 100;
|
|
}
|
|
|
|
.controls {
|
|
position: absolute;
|
|
top: 20px;
|
|
right: 20px;
|
|
z-index: 10;
|
|
}
|
|
|
|
.control-btn {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
color: white;
|
|
border: none;
|
|
padding: 8px 12px;
|
|
margin-left: 5px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.control-btn:hover {
|
|
background: rgba(255, 255, 255, 0.4);
|
|
}
|
|
|
|
.selected {
|
|
filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.7));
|
|
stroke: white;
|
|
stroke-width: 2px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="container">
|
|
<div id="graph"></div>
|
|
<div id="panel">
|
|
<div class="info-title">请点击节点查看详情</div>
|
|
<div class="info-description"></div>
|
|
<div class="relations-list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<button class="control-btn" id="force">力导向布局</button>
|
|
<button class="control-btn" id="radial">辐射状布局</button>
|
|
<button class="control-btn" id="circular">环形布局</button>
|
|
<button class="control-btn" id="grid">网格布局</button>
|
|
</div>
|
|
|
|
<div class="tooltip"></div>
|
|
|
|
<script>
|
|
const width = document.getElementById('graph').clientWidth;
|
|
const height = document.getElementById('graph').clientHeight;
|
|
|
|
const svg = d3.select("#graph")
|
|
.append("svg")
|
|
.attr("width", width)
|
|
.attr("height", height);
|
|
|
|
const simulation = d3.forceSimulation()
|
|
.force("link", d3.forceLink().id(d => d.id).distance(100))
|
|
.force("charge", d3.forceManyBody().strength(-300))
|
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
.force("collision", d3.forceCollide().radius(d => Math.sqrt(d.size) * 5 + 30));
|
|
|
|
const tooltip = d3.select(".tooltip");
|
|
|
|
// 数据
|
|
const data = {
|
|
nodes: [
|
|
{ id: "1", name: "刘邦", type: "person", size: 50, description: "汉朝开国皇帝,出身平民,善于用人,击败项羽统一天下,建立汉朝。" },
|
|
{ id: "2", name: "吕后", type: "person", size: 40, description: "刘邦的皇后,政治手段强硬,刘邦死后掌握大权,诛杀功臣和刘姓诸侯。" },
|
|
{ id: "3", name: "韩信", type: "person", size: 35, description: "汉初三大名将之一,军事天才,为刘邦立下汗马功劳,后被吕后设计杀害。" },
|
|
{ id: "4", name: "萧何", type: "person", size: 35, description: "刘邦的重要谋臣,擅长治国理政,为汉朝制定法律制度,推荐韩信等人才。" },
|
|
{ id: "5", name: "张良", type: "person", size: 35, description: "刘邦的首席谋士,战略家,在鸿门宴等关键时刻帮助刘邦脱险。" },
|
|
{ id: "6", name: "项羽", type: "person", size: 40, description: "刘邦的主要对手,西楚霸王,军事天才但缺乏政治远见,最终自刎乌江。" },
|
|
{ id: "7", name: "英布", type: "person", size: 30, description: "汉初名将,后反叛刘邦,被平定。" },
|
|
{ id: "8", name: "彭越", type: "person", size: 30, description: "汉初名将,后因谋反罪名被吕后诛杀。" },
|
|
{ id: "9", name: "项伯", type: "person", size: 25, description: "项羽的叔父,与张良交好,多次帮助刘邦脱险。" },
|
|
{ id: "10", name: "戚姬", type: "person", size: 20, description: "刘邦宠妃,刘如意之母,与吕后争宠,后被吕后残忍杀害。" },
|
|
{ id: "11", name: "刘如意", type: "person", size: 20, description: "刘邦与戚姬之子,曾被考虑为太子,后被吕后毒杀。" },
|
|
{ id: "12", name: "刘盈", type: "person", size: 20, description: "刘邦与吕后之子,汉惠帝,性格懦弱,早逝。" },
|
|
{ id: "13", name: "刘姓诸侯", type: "organization", size: 25, description: "刘邦分封的同姓诸侯王,部分后来被吕后诛杀。" },
|
|
{ id: "14", name: "吕氏家族", type: "organization", size: 25, description: "吕后掌权时大力提拔的家族势力,吕后死后被清算。" }
|
|
],
|
|
links: [
|
|
{ source: "1", target: "2", type: "family", description: "夫妻关系,后期关系恶化" },
|
|
{ source: "1", target: "3", type: "military", description: "君臣关系,韩信为刘邦立下汗马功劳,后被猜忌杀害" },
|
|
{ source: "1", target: "4", type: "political", description: "君臣关系,萧何为刘邦制定法律制度" },
|
|
{ source: "1", target: "5", type: "political", description: "君臣关系,张良为刘邦重要谋士" },
|
|
{ source: "1", target: "6", type: "military", description: "敌对关系,楚汉相争的主要对手" },
|
|
{ source: "1", target: "7", type: "military", description: "君臣关系,后英布反叛" },
|
|
{ source: "1", target: "8", type: "military", description: "君臣关系,后彭越被诛杀" },
|
|
{ source: "1", target: "9", type: "political", description: "政治盟友,项伯多次帮助刘邦" },
|
|
{ source: "1", target: "10", type: "family", description: "宠妃关系,引发与吕后的矛盾" },
|
|
{ source: "1", target: "11", type: "family", description: "父子关系,曾考虑立为太子" },
|
|
{ source: "1", target: "12", type: "family", description: "父子关系,汉惠帝" },
|
|
{ source: "1", target: "13", type: "political", description: "分封同姓诸侯王" },
|
|
{ source: "2", target: "10", type: "political", description: "敌对关系,吕后残酷杀害戚姬" },
|
|
{ source: "2", target: "11", type: "political", description: "敌对关系,吕后毒杀刘如意" },
|
|
{ source: "2", target: "13", type: "political", description: "敌对关系,吕后诛杀部分刘姓诸侯" },
|
|
{ source: "2", target: "14", type: "family", description: "吕后提拔吕氏家族" },
|
|
{ source: "4", target: "3", type: "political", description: "萧何推荐韩信" },
|
|
{ source: "6", target: "9", type: "family", description: "叔侄关系" },
|
|
{ source: "5", target: "9", type: "political", description: "好友关系" }
|
|
]
|
|
};
|
|
|
|
// 创建链接
|
|
const link = svg.append("g")
|
|
.attr("class", "links")
|
|
.selectAll("line")
|
|
.data(data.links)
|
|
.enter()
|
|
.append("line")
|
|
.attr("class", d => `link ${d.type}`)
|
|
.attr("stroke-width", 2);
|
|
|
|
// 创建节点组
|
|
const nodeGroups = svg.append("g")
|
|
.attr("class", "nodes")
|
|
.selectAll("g")
|
|
.data(data.nodes)
|
|
.enter()
|
|
.append("g")
|
|
.call(d3.drag()
|
|
.on("start", dragstarted)
|
|
.on("drag", dragged)
|
|
.on("end", dragended));
|
|
|
|
// 创建3D效果节点
|
|
nodeGroups.each(function(d) {
|
|
const node = d3.select(this);
|
|
const size = Math.sqrt(d.size) * 5;
|
|
|
|
// 创建3D效果
|
|
node.append("circle")
|
|
.attr("class", "node")
|
|
.attr("r", size)
|
|
.attr("fill", d => {
|
|
if (d.type === "person") return "#4a89dc";
|
|
if (d.type === "organization") return "#8cc152";
|
|
});
|
|
|
|
// 添加内圆创建深度效果
|
|
node.append("circle")
|
|
.attr("r", size * 0.7)
|
|
.attr("fill", d => {
|
|
if (d.type === "person") return "#5d9cec";
|
|
if (d.type === "organization") return "#a0d468";
|
|
})
|
|
.attr("opacity", 0.8);
|
|
|
|
// 添加文字
|
|
node.append("text")
|
|
.text(d.name)
|
|
.attr("dy", 0);
|
|
});
|
|
|
|
// 添加交互
|
|
nodeGroups.on("mouseover", function(event, d) {
|
|
tooltip.transition()
|
|
.duration(200)
|
|
.style("opacity", .9);
|
|
tooltip.html(`<strong>${d.name}</strong><br/>${d.description}`)
|
|
.style("left", (event.pageX + 10) + "px")
|
|
.style("top", (event.pageY - 28) + "px");
|
|
|
|
d3.select(this).select(".node").classed("selected", true);
|
|
})
|
|
.on("mouseout", function() {
|
|
tooltip.transition()
|
|
.duration(500)
|
|
.style("opacity", 0);
|
|
|
|
d3.select(this).select(".node").classed("selected", false);
|
|
})
|
|
.on("click", function(event, d) {
|
|
updatePanel(d);
|
|
});
|
|
|
|
// 更新右侧面板
|
|
function updatePanel(node) {
|
|
d3.select(".info-title").text(node.name);
|
|
d3.select(".info-description").html(`<strong>类型:</strong> ${node.type === "person" ? "人物" : "组织"}<br/><br/>${node.description}`);
|
|
|
|
const relatedLinks = data.links.filter(link => link.source === node.id || link.target === node.id);
|
|
const relationsList = d3.select(".relations-list").html("");
|
|
|
|
relatedLinks.forEach(link => {
|
|
const otherNodeId = link.source === node.id ? link.target : link.source;
|
|
const otherNode = data.nodes.find(n => n.id === otherNodeId);
|
|
const relationType = link.type === "military" ? "军事" :
|
|
link.type === "political" ? "政治" : "家族";
|
|
|
|
relationsList.append("div")
|
|
.attr("class", "relation-item")
|
|
.html(`
|
|
<span class="relation-type ${link.type}-badge">${relationType}</span>
|
|
<strong>${otherNode.name}</strong>: ${link.description}
|
|
`);
|
|
});
|
|
}
|
|
|
|
// 更新力导向模拟
|
|
simulation
|
|
.nodes(data.nodes)
|
|
.on("tick", ticked);
|
|
|
|
simulation.force("link")
|
|
.links(data.links);
|
|
|
|
function ticked() {
|
|
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);
|
|
|
|
nodeGroups
|
|
.attr("transform", d => `translate(${d.x},${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;
|
|
}
|
|
|
|
// 布局控制
|
|
d3.select("#force").on("click", () => {
|
|
simulation.force("charge", d3.forceManyBody().strength(-300))
|
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
.alphaTarget(0.3)
|
|
.restart();
|
|
});
|
|
|
|
d3.select("#radial").on("click", () => {
|
|
const center = {x: width / 2, y: height / 2};
|
|
simulation.force("radial", d3.forceRadial(d => {
|
|
const distance = Math.sqrt(d.size) * 10;
|
|
return distance;
|
|
}, center.x, center.y))
|
|
.force("charge", null)
|
|
.alphaTarget(0.3)
|
|
.restart();
|
|
});
|
|
|
|
d3.select("#circular").on("click", () => {
|
|
const center = {x: width / 2, y: height / 2};
|
|
const radius = Math.min(width, height) * 0.4;
|
|
const angle = (2 * Math.PI) / data.nodes.length;
|
|
|
|
data.nodes.forEach((d, i) => {
|
|
d.fx = center.x + radius * Math.cos(i * angle);
|
|
d.fy = center.y + radius * Math.sin(i * angle);
|
|
});
|
|
|
|
simulation.force("charge", null)
|
|
.alphaTarget(0)
|
|
.restart();
|
|
});
|
|
|
|
d3.select("#grid").on("click", () => {
|
|
const cols = Math.ceil(Math.sqrt(data.nodes.length));
|
|
const spacing = Math.min(width, height) * 0.8 / cols;
|
|
const offsetX = (width - (cols - 1) * spacing) / 2;
|
|
const offsetY = (height - (Math.ceil(data.nodes.length / cols) - 1) * spacing) / 2;
|
|
|
|
data.nodes.forEach((d, i) => {
|
|
const row = Math.floor(i / cols);
|
|
const col = i % cols;
|
|
d.fx = offsetX + col * spacing;
|
|
d.fy = offsetY + row * spacing;
|
|
});
|
|
|
|
simulation.force("charge", null)
|
|
.alphaTarget(0)
|
|
.restart();
|
|
});
|
|
|
|
// 响应窗口大小变化
|
|
window.addEventListener('resize', () => {
|
|
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))
|
|
.restart();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|