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.
502 lines
18 KiB
502 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;
|
|
padding: 0;
|
|
background: linear-gradient(135deg, #0a1a3a, #1a3a6e);
|
|
color: #fff;
|
|
font-family: 'Arial', sans-serif;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#title {
|
|
position: absolute;
|
|
top: 20px;
|
|
left: 0;
|
|
right: 0;
|
|
text-align: center;
|
|
font-size: 36px;
|
|
font-weight: bold;
|
|
background: linear-gradient(90deg, #ff8a00, #e52e71);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
text-shadow: 0 0 10px rgba(255, 138, 0, 0.3);
|
|
z-index: 100;
|
|
}
|
|
|
|
#graph-container {
|
|
position: absolute;
|
|
top: 80px;
|
|
left: 0;
|
|
right: 300px;
|
|
bottom: 0;
|
|
}
|
|
|
|
#info-panel {
|
|
position: absolute;
|
|
top: 80px;
|
|
right: 0;
|
|
width: 300px;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
padding: 15px;
|
|
box-sizing: border-box;
|
|
overflow-y: auto;
|
|
border-left: 1px solid #444;
|
|
}
|
|
|
|
#legend {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 20px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
margin: 5px 0;
|
|
}
|
|
|
|
.legend-color {
|
|
width: 20px;
|
|
height: 20px;
|
|
margin-right: 10px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.node {
|
|
stroke: #fff;
|
|
stroke-width: 1.5px;
|
|
filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.3));
|
|
}
|
|
|
|
.link {
|
|
fill: none;
|
|
}
|
|
|
|
.node text {
|
|
dominant-baseline: central;
|
|
text-anchor: middle;
|
|
fill: #fff;
|
|
font-size: 12px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.tooltip {
|
|
position: absolute;
|
|
padding: 10px;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
color: #fff;
|
|
border-radius: 5px;
|
|
pointer-events: none;
|
|
max-width: 300px;
|
|
font-size: 14px;
|
|
z-index: 100;
|
|
}
|
|
|
|
#layout-controls {
|
|
position: absolute;
|
|
top: 20px;
|
|
left: 20px;
|
|
z-index: 100;
|
|
}
|
|
|
|
.layout-btn {
|
|
background: rgba(0, 0, 0, 0.5);
|
|
color: #fff;
|
|
border: 1px solid #444;
|
|
padding: 5px 10px;
|
|
margin-right: 5px;
|
|
cursor: pointer;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.layout-btn:hover {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1 id="title">秦汉时期人物关系图谱</h1>
|
|
|
|
<div id="layout-controls">
|
|
<button class="layout-btn" id="force-layout">力导向</button>
|
|
<button class="layout-btn" id="radial-layout">辐射状</button>
|
|
<button class="layout-btn" id="circular-layout">环形</button>
|
|
<button class="layout-btn" id="grid-layout">网格</button>
|
|
</div>
|
|
|
|
<div id="graph-container"></div>
|
|
<div id="info-panel">
|
|
<h3 id="selected-node">点击节点查看详细信息</h3>
|
|
<div id="node-info"></div>
|
|
<div id="relations-list"></div>
|
|
</div>
|
|
|
|
<div id="legend">
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background-color: #4CAF50;"></div>
|
|
<span>政治关系</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background-color: #2196F3;"></div>
|
|
<span>军事关系</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background-color: #FF9800;"></div>
|
|
<span>敌对关系</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background-color: #9C27B0;"></div>
|
|
<span>传承关系</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tooltip" id="tooltip"></div>
|
|
|
|
<script>
|
|
const data = {
|
|
"nodes": [
|
|
{
|
|
"id": "刘邦",
|
|
"type": "person",
|
|
"desc": "汉高祖刘邦,汉朝开国皇帝,从沛县起义反秦,最终击败项羽建立汉朝",
|
|
"rank": 10,
|
|
"image": "https://example.com/liubang.jpg"
|
|
},
|
|
{
|
|
"id": "秦始皇",
|
|
"type": "person",
|
|
"desc": "嬴政,中国历史上第一个皇帝,统一六国建立秦朝,实行中央集权制度",
|
|
"rank": 10,
|
|
"image": "https://example.com/qinshihuang.jpg"
|
|
},
|
|
{
|
|
"id": "项羽",
|
|
"type": "person",
|
|
"desc": "西楚霸王,秦末反秦起义领袖,与刘邦争夺天下失败",
|
|
"rank": 9,
|
|
"image": "https://example.com/xiangyu.jpg"
|
|
},
|
|
{
|
|
"id": "张良",
|
|
"type": "person",
|
|
"desc": "汉初三杰之一,刘邦的重要谋士,擅长运筹帷幄",
|
|
"rank": 8,
|
|
"image": "https://example.com/zhangliang.jpg"
|
|
},
|
|
{
|
|
"id": "萧何",
|
|
"type": "person",
|
|
"desc": "汉初三杰之一,刘邦的丞相,负责后勤和内政",
|
|
"rank": 8,
|
|
"image": "https://example.com/xiaohe.jpg"
|
|
},
|
|
{
|
|
"id": "韩信",
|
|
"type": "person",
|
|
"desc": "汉初三杰之一,杰出军事家,后因谋反被吕后处死",
|
|
"rank": 8,
|
|
"image": "https://example.com/hanxin.jpg"
|
|
},
|
|
{
|
|
"id": "李斯",
|
|
"type": "person",
|
|
"desc": "秦朝丞相,协助秦始皇统一六国,后与赵高合谋篡改遗诏",
|
|
"rank": 7,
|
|
"image": "https://example.com/lisi.jpg"
|
|
},
|
|
{
|
|
"id": "吕不韦",
|
|
"type": "person",
|
|
"desc": "秦国丞相,据传为秦始皇生父,后被罢免",
|
|
"rank": 6,
|
|
"image": "https://example.com/lvbuwei.jpg"
|
|
},
|
|
{
|
|
"id": "胡亥",
|
|
"type": "person",
|
|
"desc": "秦二世,秦始皇次子,昏庸无能导致秦朝灭亡",
|
|
"rank": 5,
|
|
"image": "https://example.com/huhai.jpg"
|
|
},
|
|
{
|
|
"id": "扶苏",
|
|
"type": "person",
|
|
"desc": "秦始皇长子,因谏言被贬边疆,后被胡亥赐死",
|
|
"rank": 5,
|
|
"image": "https://example.com/fusu.jpg"
|
|
}
|
|
],
|
|
"links": [
|
|
{
|
|
"source": "刘邦",
|
|
"target": "秦始皇",
|
|
"type": "传承关系",
|
|
"desc": "刘邦的命运与秦始皇关于天子气的预言相关联",
|
|
"strength": 0.8
|
|
},
|
|
{
|
|
"source": "刘邦",
|
|
"target": "项羽",
|
|
"type": "敌对关系",
|
|
"desc": "楚汉争霸,最终刘邦击败项羽建立汉朝",
|
|
"strength": 1.0
|
|
},
|
|
{
|
|
"source": "刘邦",
|
|
"target": "张良",
|
|
"type": "政治关系",
|
|
"desc": "张良是刘邦的重要谋士,多次献计献策",
|
|
"strength": 0.9
|
|
},
|
|
{
|
|
"source": "刘邦",
|
|
"target": "萧何",
|
|
"type": "政治关系",
|
|
"desc": "萧何是刘邦的丞相,负责后勤和内政",
|
|
"strength": 0.9
|
|
},
|
|
{
|
|
"source": "刘邦",
|
|
"target": "韩信",
|
|
"type": "军事关系",
|
|
"desc": "韩信为刘邦立下赫赫战功,后因谋反被处死",
|
|
"strength": 0.8
|
|
},
|
|
{
|
|
"source": "秦始皇",
|
|
"target": "李斯",
|
|
"type": "政治关系",
|
|
"desc": "李斯是秦始皇的丞相,协助统一六国",
|
|
"strength": 0.9
|
|
},
|
|
{
|
|
"source": "秦始皇",
|
|
"target": "吕不韦",
|
|
"type": "政治关系",
|
|
"desc": "吕不韦据传为秦始皇生父,后被罢免",
|
|
"strength": 0.7
|
|
},
|
|
{
|
|
"source": "秦始皇",
|
|
"target": "胡亥",
|
|
"type": "传承关系",
|
|
"desc": "胡亥是秦始皇次子,篡位成为秦二世",
|
|
"strength": 0.7
|
|
},
|
|
{
|
|
"source": "秦始皇",
|
|
"target": "扶苏",
|
|
"type": "传承关系",
|
|
"desc": "扶苏是秦始皇长子,因谏言被贬边疆",
|
|
"strength": 0.6
|
|
},
|
|
{
|
|
"source": "张良",
|
|
"target": "秦始皇",
|
|
"type": "敌对关系",
|
|
"desc": "张良曾试图刺杀秦始皇为韩国复仇",
|
|
"strength": 0.5
|
|
},
|
|
{
|
|
"source": "刘邦",
|
|
"target": "胡亥",
|
|
"type": "敌对关系",
|
|
"desc": "刘邦推翻秦二世胡亥的统治",
|
|
"strength": 0.6
|
|
},
|
|
{
|
|
"source": "项羽",
|
|
"target": "秦始皇",
|
|
"type": "传承关系",
|
|
"desc": "项羽目睹秦始皇出巡后提出取代他的言论",
|
|
"strength": 0.5
|
|
}
|
|
]
|
|
};
|
|
|
|
const width = document.getElementById('graph-container').offsetWidth;
|
|
const height = document.getElementById('graph-container').offsetHeight;
|
|
|
|
const svg = d3.select('#graph-container')
|
|
.append('svg')
|
|
.attr('width', width)
|
|
.attr('height', height);
|
|
|
|
const color = d3.scaleOrdinal()
|
|
.domain(["政治关系", "军事关系", "敌对关系", "传承关系"])
|
|
.range(["#4CAF50", "#2196F3", "#FF9800", "#9C27B0"]);
|
|
|
|
const simulation = d3.forceSimulation(data.nodes)
|
|
.force("link", d3.forceLink(data.links).id(d => d.id).distance(100).strength(d => d.strength))
|
|
.force("charge", d3.forceManyBody().strength(-500))
|
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
.force("collision", d3.forceCollide().radius(d => Math.sqrt(d.rank) * 10 + 30));
|
|
|
|
const link = svg.append("g")
|
|
.attr("class", "links")
|
|
.selectAll("line")
|
|
.data(data.links)
|
|
.enter()
|
|
.append("line")
|
|
.attr("class", "link")
|
|
.attr("stroke", d => color(d.type))
|
|
.attr("stroke-width", d => d.strength * 3);
|
|
|
|
const node = 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));
|
|
|
|
node.append("circle")
|
|
.attr("class", "node")
|
|
.attr("r", d => Math.sqrt(d.rank) * 10 + 30)
|
|
.attr("fill", d => {
|
|
if (d.id === "刘邦" || d.id === "秦始皇") return "#E53935";
|
|
if (d.id === "项羽" || d.id === "李斯") return "#8E24AA";
|
|
return "#039BE5";
|
|
})
|
|
.on("mouseover", function(event, d) {
|
|
d3.select('#tooltip')
|
|
.style("left", (event.pageX + 10) + "px")
|
|
.style("top", (event.pageY - 10) + "px")
|
|
.style("opacity", 1)
|
|
.html(`<strong>${d.id}</strong><br>${d.desc}`);
|
|
|
|
d3.select(this).attr("stroke", "#fff").attr("stroke-width", 3);
|
|
})
|
|
.on("mouseout", function() {
|
|
d3.select('#tooltip').style("opacity", 0);
|
|
d3.select(this).attr("stroke", "#fff").attr("stroke-width", 1.5);
|
|
})
|
|
.on("click", function(event, d) {
|
|
updateInfoPanel(d);
|
|
});
|
|
|
|
node.append("text")
|
|
.attr("dy", 4)
|
|
.text(d => d.id)
|
|
.attr("fill", "#fff")
|
|
.attr("font-size", d => Math.min(16, Math.sqrt(d.rank) * 3 + 10));
|
|
|
|
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("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;
|
|
}
|
|
|
|
function updateInfoPanel(d) {
|
|
document.getElementById('selected-node').textContent = d.id;
|
|
|
|
let infoHtml = `<p><strong>类型:</strong> ${d.type}</p>
|
|
<p><strong>描述:</strong> ${d.desc}</p>
|
|
<p><strong>重要性:</strong> ${d.rank}/10</p>`;
|
|
|
|
if (d.image) {
|
|
infoHtml += `<img src="${d.image}" style="max-width:100%; margin-top:10px;">`;
|
|
}
|
|
|
|
document.getElementById('node-info').innerHTML = infoHtml;
|
|
|
|
// 更新关系列表
|
|
let relations = data.links.filter(link => link.source.id === d.id || link.target.id === d.id);
|
|
|
|
let relationsHtml = '<h4>主要关系:</h4><ul>';
|
|
relations.forEach(rel => {
|
|
const otherNode = rel.source.id === d.id ? rel.target.id : rel.source.id;
|
|
relationsHtml += `<li><span style="color:${color(rel.type)}">${rel.type}</span> 与 ${otherNode}: ${rel.desc}</li>`;
|
|
});
|
|
relationsHtml += '</ul>';
|
|
|
|
document.getElementById('relations-list').innerHTML = relationsHtml;
|
|
}
|
|
|
|
// 布局控制
|
|
document.getElementById('force-layout').addEventListener('click', function() {
|
|
simulation.force("charge", d3.forceManyBody().strength(-500));
|
|
simulation.alpha(1).restart();
|
|
});
|
|
|
|
document.getElementById('radial-layout').addEventListener('click', function() {
|
|
simulation.force("charge", null);
|
|
simulation.force("radial", d3.forceRadial(width/3, width/2, height/2).strength(0.1));
|
|
simulation.alpha(1).restart();
|
|
});
|
|
|
|
document.getElementById('circular-layout').addEventListener('click', function() {
|
|
simulation.force("charge", null);
|
|
const radius = Math.min(width, height) / 2 - 50;
|
|
data.nodes.forEach((node, i) => {
|
|
node.fx = width / 2 + radius * Math.cos(i * 2 * Math.PI / data.nodes.length);
|
|
node.fy = height / 2 + radius * Math.sin(i * 2 * Math.PI / data.nodes.length);
|
|
});
|
|
simulation.alpha(1).restart();
|
|
});
|
|
|
|
document.getElementById('grid-layout').addEventListener('click', function() {
|
|
simulation.force("charge", null);
|
|
const cols = Math.ceil(Math.sqrt(data.nodes.length));
|
|
const spacing = Math.min(width, height) / (cols + 1);
|
|
data.nodes.forEach((node, i) => {
|
|
const row = Math.floor(i / cols);
|
|
const col = i % cols;
|
|
node.fx = spacing * (col + 1);
|
|
node.fy = spacing * (row + 1);
|
|
});
|
|
simulation.alpha(1).restart();
|
|
});
|
|
|
|
// 响应式调整
|
|
window.addEventListener('resize', function() {
|
|
const newWidth = document.getElementById('graph-container').offsetWidth;
|
|
const newHeight = document.getElementById('graph-container').offsetHeight;
|
|
|
|
svg.attr('width', newWidth)
|
|
.attr('height', newHeight);
|
|
|
|
simulation.force("center", d3.forceCenter(newWidth / 2, newHeight / 2));
|
|
simulation.alpha(1).restart();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|