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.
508 lines
17 KiB
508 lines
17 KiB
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<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, #0a1a3a, #1a3a7a);
|
|
font-family: 'Arial', sans-serif;
|
|
color: #fff;
|
|
}
|
|
|
|
#title {
|
|
position: absolute;
|
|
top: 20px;
|
|
width: 100%;
|
|
text-align: center;
|
|
font-size: 32px;
|
|
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,255,255,0.3);
|
|
z-index: 100;
|
|
}
|
|
|
|
#graph-container {
|
|
position: absolute;
|
|
width: 70%;
|
|
height: 100%;
|
|
left: 0;
|
|
top: 0;
|
|
}
|
|
|
|
#info-panel {
|
|
position: absolute;
|
|
width: 30%;
|
|
height: 100%;
|
|
right: 0;
|
|
top: 0;
|
|
background: rgba(10, 25, 50, 0.8);
|
|
padding: 20px;
|
|
box-sizing: border-box;
|
|
overflow-y: auto;
|
|
border-left: 1px solid #2a4a8a;
|
|
}
|
|
|
|
.node {
|
|
stroke: #fff;
|
|
stroke-width: 1.5px;
|
|
filter: drop-shadow(0 0 5px rgba(255,255,255,0.3));
|
|
}
|
|
|
|
.link {
|
|
stroke-opacity: 0.6;
|
|
}
|
|
|
|
.node text {
|
|
dominant-baseline: central;
|
|
text-anchor: middle;
|
|
fill: #fff;
|
|
font-size: 10px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.legend {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 20px;
|
|
background: rgba(10, 25, 50, 0.8);
|
|
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: 3px;
|
|
}
|
|
|
|
#layout-controls {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
right: calc(30% + 20px);
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
.layout-btn {
|
|
padding: 5px 10px;
|
|
background: #2a4a8a;
|
|
border: none;
|
|
color: white;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.layout-btn:hover {
|
|
background: #3a5a9a;
|
|
}
|
|
|
|
.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: 12px;
|
|
z-index: 10;
|
|
display: none;
|
|
}
|
|
|
|
.node:hover {
|
|
stroke: #ff8a00;
|
|
stroke-width: 3px;
|
|
filter: drop-shadow(0 0 10px rgba(255,138,0,0.7));
|
|
}
|
|
|
|
.info-title {
|
|
font-size: 24px;
|
|
margin-bottom: 15px;
|
|
color: #ff8a00;
|
|
border-bottom: 1px solid #2a4a8a;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.info-description {
|
|
margin-bottom: 20px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.info-relations {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.relation-item {
|
|
margin-bottom: 10px;
|
|
padding: 10px;
|
|
background: rgba(42, 74, 138, 0.5);
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.relation-type {
|
|
font-weight: bold;
|
|
color: #e52e71;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1 id="title">秦汉时期核心人物关系图谱</h1>
|
|
<div id="graph-container"></div>
|
|
<div id="info-panel">
|
|
<div class="info-title">点击节点查看详情</div>
|
|
<div class="info-description">请点击左侧图谱中的节点查看详细信息</div>
|
|
<div class="info-relations"></div>
|
|
</div>
|
|
<div class="legend">
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background-color: #ff8a00;"></div>
|
|
<span>政治关系</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background-color: #e52e71;"></div>
|
|
<span>军事关系</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background-color: #1cb5e0;"></div>
|
|
<span>个人关系</span>
|
|
</div>
|
|
</div>
|
|
<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 class="tooltip"></div>
|
|
|
|
<script>
|
|
const data = {
|
|
"nodes": [
|
|
{
|
|
"id": "刘邦",
|
|
"type": "person",
|
|
"desc": "汉高祖,西汉开国皇帝,击败项羽建立汉朝",
|
|
"rank": 9,
|
|
"group": 1
|
|
},
|
|
{
|
|
"id": "秦始皇",
|
|
"type": "person",
|
|
"desc": "中国第一位皇帝,统一六国建立秦朝",
|
|
"rank": 10,
|
|
"group": 1
|
|
},
|
|
{
|
|
"id": "项羽",
|
|
"type": "person",
|
|
"desc": "西楚霸王,刘邦的主要对手",
|
|
"rank": 8,
|
|
"group": 1
|
|
},
|
|
{
|
|
"id": "张良",
|
|
"type": "person",
|
|
"desc": "汉初三杰之一,刘邦的重要谋士",
|
|
"rank": 7,
|
|
"group": 2
|
|
},
|
|
{
|
|
"id": "萧何",
|
|
"type": "person",
|
|
"desc": "汉初三杰之一,刘邦的丞相",
|
|
"rank": 7,
|
|
"group": 2
|
|
},
|
|
{
|
|
"id": "韩信",
|
|
"type": "person",
|
|
"desc": "汉初三杰之一,著名军事家",
|
|
"rank": 7,
|
|
"group": 2
|
|
},
|
|
{
|
|
"id": "李斯",
|
|
"type": "person",
|
|
"desc": "秦朝丞相,协助秦始皇统一六国",
|
|
"rank": 6,
|
|
"group": 3
|
|
},
|
|
{
|
|
"id": "吕不韦",
|
|
"type": "person",
|
|
"desc": "秦国丞相,据传为秦始皇生父",
|
|
"rank": 5,
|
|
"group": 3
|
|
},
|
|
{
|
|
"id": "吕后",
|
|
"type": "person",
|
|
"desc": "刘邦的皇后,汉朝第一位皇后",
|
|
"rank": 5,
|
|
"group": 2
|
|
}
|
|
],
|
|
"links": [
|
|
{
|
|
"source": "刘邦",
|
|
"target": "秦始皇",
|
|
"type": "historical",
|
|
"desc": "刘邦的命运与秦始皇关于天子气的预言相关联"
|
|
},
|
|
{
|
|
"source": "刘邦",
|
|
"target": "项羽",
|
|
"type": "military",
|
|
"desc": "楚汉相争的主要对手"
|
|
},
|
|
{
|
|
"source": "刘邦",
|
|
"target": "张良",
|
|
"type": "political",
|
|
"desc": "刘邦的重要谋士"
|
|
},
|
|
{
|
|
"source": "刘邦",
|
|
"target": "萧何",
|
|
"type": "political",
|
|
"desc": "刘邦的丞相"
|
|
},
|
|
{
|
|
"source": "刘邦",
|
|
"target": "韩信",
|
|
"type": "military",
|
|
"desc": "刘邦的重要将领"
|
|
},
|
|
{
|
|
"source": "刘邦",
|
|
"target": "吕后",
|
|
"type": "personal",
|
|
"desc": "夫妻关系"
|
|
},
|
|
{
|
|
"source": "秦始皇",
|
|
"target": "李斯",
|
|
"type": "political",
|
|
"desc": "秦始皇的重要辅佐"
|
|
},
|
|
{
|
|
"source": "秦始皇",
|
|
"target": "吕不韦",
|
|
"type": "personal",
|
|
"desc": "据传为父子关系"
|
|
},
|
|
{
|
|
"source": "张良",
|
|
"target": "秦始皇",
|
|
"type": "historical",
|
|
"desc": "张良曾试图刺杀秦始皇"
|
|
},
|
|
{
|
|
"source": "项羽",
|
|
"target": "秦始皇",
|
|
"type": "historical",
|
|
"desc": "项羽观看秦始皇出巡时表示要取代他"
|
|
}
|
|
]
|
|
};
|
|
|
|
const width = document.getElementById('graph-container').clientWidth;
|
|
const height = window.innerHeight;
|
|
|
|
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(100))
|
|
.force('charge', d3.forceManyBody().strength(-300))
|
|
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
.force('collision', d3.forceCollide().radius(50));
|
|
|
|
const link = svg.append('g')
|
|
.attr('class', 'links')
|
|
.selectAll('line')
|
|
.data(data.links)
|
|
.enter()
|
|
.append('line')
|
|
.attr('class', 'link')
|
|
.attr('stroke', d => {
|
|
switch(d.type) {
|
|
case 'political': return '#ff8a00';
|
|
case 'military': return '#e52e71';
|
|
case 'personal': return '#1cb5e0';
|
|
default: return '#aaa';
|
|
}
|
|
})
|
|
.attr('stroke-width', 2);
|
|
|
|
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 => 10 + d.rank)
|
|
.attr('fill', d => {
|
|
switch(d.group) {
|
|
case 1: return '#e52e71';
|
|
case 2: return '#1cb5e0';
|
|
case 3: return '#ff8a00';
|
|
default: return '#aaa';
|
|
}
|
|
})
|
|
.on('mouseover', function(event, d) {
|
|
d3.select('.tooltip')
|
|
.style('left', (event.pageX + 10) + 'px')
|
|
.style('top', (event.pageY - 10) + 'px')
|
|
.style('display', 'block')
|
|
.html(`<strong>${d.id}</strong><br>${d.desc}`);
|
|
})
|
|
.on('mouseout', function() {
|
|
d3.select('.tooltip').style('display', 'none');
|
|
})
|
|
.on('click', function(event, d) {
|
|
updateInfoPanel(d);
|
|
});
|
|
|
|
node.append('text')
|
|
.attr('dy', 4)
|
|
.text(d => d.id);
|
|
|
|
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) {
|
|
const infoPanel = d3.select('#info-panel');
|
|
|
|
infoPanel.select('.info-title').text(d.id);
|
|
infoPanel.select('.info-description').text(d.desc);
|
|
|
|
const relations = data.links.filter(
|
|
link => link.source.id === d.id || link.target.id === d.id
|
|
);
|
|
|
|
const relationsList = infoPanel.select('.info-relations')
|
|
.html('<h3>重要关系</h3>')
|
|
.selectAll('.relation-item')
|
|
.data(relations)
|
|
.enter()
|
|
.append('div')
|
|
.attr('class', 'relation-item');
|
|
|
|
relationsList.append('div')
|
|
.attr('class', 'relation-type')
|
|
.text(d => {
|
|
const otherNode = d.source.id === d.id ? d.target.id : d.source.id;
|
|
return `${d.id === d.source.id ? '→ ' : '← '}${otherNode}`;
|
|
});
|
|
|
|
relationsList.append('div')
|
|
.text(d => d.desc);
|
|
}
|
|
|
|
// Layout controls
|
|
document.getElementById('force-layout').addEventListener('click', function() {
|
|
simulation.force('link', d3.forceLink(data.links).id(d => d.id).distance(100))
|
|
.force('charge', d3.forceManyBody().strength(-300))
|
|
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
.alpha(1).restart();
|
|
});
|
|
|
|
document.getElementById('radial-layout').addEventListener('click', function() {
|
|
simulation.force('link', d3.forceLink(data.links).id(d => d.id).distance(100))
|
|
.force('charge', d3.forceManyBody().strength(-300))
|
|
.force('radial', d3.forceRadial(height / 3, width / 2, height / 2).strength(0.1))
|
|
.alpha(1).restart();
|
|
});
|
|
|
|
document.getElementById('circular-layout').addEventListener('click', function() {
|
|
const radius = Math.min(width, height) / 3;
|
|
const angle = 2 * Math.PI / data.nodes.length;
|
|
|
|
data.nodes.forEach((d, i) => {
|
|
d.fx = width / 2 + radius * Math.cos(i * angle);
|
|
d.fy = height / 2 + radius * Math.sin(i * angle);
|
|
});
|
|
|
|
simulation.alpha(1).restart();
|
|
});
|
|
|
|
document.getElementById('grid-layout').addEventListener('click', function() {
|
|
const cols = Math.ceil(Math.sqrt(data.nodes.length));
|
|
const spacing = Math.min(width, height) / (cols + 1);
|
|
|
|
data.nodes.forEach((d, i) => {
|
|
const row = Math.floor(i / cols);
|
|
const col = i % cols;
|
|
d.fx = width / 2 + (col - cols / 2 + 0.5) * spacing;
|
|
d.fy = height / 2 + (row - Math.floor(data.nodes.length / cols) / 2 + 0.5) * spacing;
|
|
});
|
|
|
|
simulation.alpha(1).restart();
|
|
});
|
|
|
|
// Initial info panel update
|
|
updateInfoPanel(data.nodes[0]);
|
|
|
|
// Responsive resize
|
|
window.addEventListener('resize', function() {
|
|
const newWidth = document.getElementById('graph-container').clientWidth;
|
|
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>
|