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.
383 lines
16 KiB
383 lines
16 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;
|
|
background: linear-gradient(135deg, #0a1a3a, #1a3a7a);
|
|
font-family: 'Arial', sans-serif;
|
|
overflow: hidden;
|
|
}
|
|
#title {
|
|
position: absolute;
|
|
top: 20px;
|
|
width: 100%;
|
|
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,255,255,0.3);
|
|
z-index: 100;
|
|
}
|
|
#graph-container {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 70%;
|
|
height: 100%;
|
|
}
|
|
#info-panel {
|
|
position: absolute;
|
|
top: 80px;
|
|
right: 20px;
|
|
width: 25%;
|
|
height: calc(100% - 100px);
|
|
background: rgba(10, 26, 58, 0.8);
|
|
border-radius: 10px;
|
|
padding: 15px;
|
|
color: white;
|
|
overflow-y: auto;
|
|
box-shadow: 0 0 20px rgba(0,0,0,0.5);
|
|
}
|
|
.node {
|
|
stroke: #fff;
|
|
stroke-width: 2px;
|
|
filter: url(#glow);
|
|
}
|
|
.link {
|
|
stroke-opacity: 0.6;
|
|
}
|
|
.legend {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
background: rgba(10, 26, 58, 0.8);
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
}
|
|
.legend-item {
|
|
margin: 5px 0;
|
|
}
|
|
.legend-color {
|
|
display: inline-block;
|
|
width: 15px;
|
|
height: 15px;
|
|
margin-right: 5px;
|
|
}
|
|
#layout-controls {
|
|
position: absolute;
|
|
top: 100px;
|
|
left: 20px;
|
|
z-index: 100;
|
|
}
|
|
button {
|
|
background: rgba(255,255,255,0.2);
|
|
color: white;
|
|
border: none;
|
|
padding: 5px 10px;
|
|
margin: 5px;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
}
|
|
button:hover {
|
|
background: rgba(255,255,255,0.3);
|
|
}
|
|
.tooltip {
|
|
position: absolute;
|
|
padding: 10px;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
color: white;
|
|
border-radius: 5px;
|
|
pointer-events: none;
|
|
max-width: 300px;
|
|
font-size: 14px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="title">秦汉人物关系网络图</div>
|
|
<div id="graph-container"></div>
|
|
<div id="info-panel">
|
|
<h3 id="selected-node-title">请点击节点查看详情</h3>
|
|
<div id="selected-node-info"></div>
|
|
<div id="node-relations"></div>
|
|
</div>
|
|
<div class="legend">
|
|
<h4>关系类型</h4>
|
|
<div class="legend-item"><span class="legend-color" style="background:#ff7675;"></span>敌对关系</div>
|
|
<div class="legend-item"><span class="legend-color" style="background:#74b9ff;"></span>合作关系</div>
|
|
<div class="legend-item"><span class="legend-color" style="background:#55efc4;"></span>继承关系</div>
|
|
<div class="legend-item"><span class="legend-color" style="background:#a29bfe;"></span>其他关系</div>
|
|
</div>
|
|
<div id="layout-controls">
|
|
<button id="force-layout">力导向布局</button>
|
|
<button id="radial-layout">辐射状布局</button>
|
|
<button id="circular-layout">环形布局</button>
|
|
<button id="grid-layout">网格布局</button>
|
|
</div>
|
|
|
|
<svg width="0" height="0">
|
|
<defs>
|
|
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
|
<feGaussianBlur stdDeviation="3" result="blur" />
|
|
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
|
</filter>
|
|
</defs>
|
|
</svg>
|
|
|
|
<script>
|
|
const data = {
|
|
nodes: [
|
|
{ id: "1", name: "刘邦", type: "emperor", desc: "汉高祖,西汉开国皇帝", importance: 10 },
|
|
{ id: "2", name: "秦始皇", type: "emperor", desc: "中国第一位皇帝,秦朝建立者", importance: 10 },
|
|
{ id: "3", name: "项羽", type: "warlord", desc: "西楚霸王,刘邦的主要对手", importance: 9 },
|
|
{ id: "4", name: "张良", type: "strategist", desc: "汉初三杰之一,刘邦的重要谋士", importance: 8 },
|
|
{ id: "5", name: "萧何", type: "minister", desc: "汉初三杰之一,刘邦的丞相", importance: 8 },
|
|
{ id: "6", name: "韩信", type: "general", desc: "汉初三杰之一,著名军事家", importance: 8 },
|
|
{ id: "7", name: "吕不韦", type: "minister", desc: "秦国丞相,嬴政的重要辅佐者", importance: 7 },
|
|
{ id: "8", name: "李斯", type: "minister", desc: "秦国丞相,协助秦始皇统一六国", importance: 7 },
|
|
{ id: "9", name: "蒙恬", type: "general", desc: "秦国名将,修筑长城", importance: 6 },
|
|
{ id: "10", name: "扶苏", type: "prince", desc: "秦始皇长子,被胡亥害死", importance: 5 },
|
|
{ id: "11", name: "胡亥", type: "emperor", desc: "秦二世,秦始皇次子", importance: 5 },
|
|
{ id: "12", name: "项梁", type: "warlord", desc: "项羽叔父,反秦起义领袖", importance: 6 },
|
|
{ id: "13", name: "陈胜", type: "rebel", desc: "大泽乡起义领袖", importance: 5 },
|
|
{ id: "14", name: "吕后", type: "empress", desc: "刘邦皇后,西汉实际统治者", importance: 7 },
|
|
{ id: "15", name: "樊哙", type: "general", desc: "刘邦将领,吕后妹夫", importance: 5 }
|
|
],
|
|
links: [
|
|
{ source: "1", target: "2", type: "other", desc: "刘邦的命运与秦始皇关于天子气的预言相关联" },
|
|
{ source: "1", target: "3", type: "enemy", desc: "楚汉争霸,最终刘邦击败项羽" },
|
|
{ source: "1", target: "4", type: "collaboration", desc: "张良是刘邦的重要谋士" },
|
|
{ source: "1", target: "5", type: "collaboration", desc: "萧何是刘邦的丞相" },
|
|
{ source: "1", target: "6", type: "collaboration", desc: "韩信是刘邦的大将" },
|
|
{ source: "1", target: "14", type: "other", desc: "吕后是刘邦的皇后" },
|
|
{ source: "2", target: "7", type: "other", desc: "吕不韦据传是秦始皇生父" },
|
|
{ source: "2", target: "8", type: "collaboration", desc: "李斯是秦始皇的丞相" },
|
|
{ source: "2", target: "9", type: "collaboration", desc: "蒙恬是秦始皇的大将" },
|
|
{ source: "2", target: "10", type: "other", desc: "扶苏是秦始皇长子" },
|
|
{ source: "2", target: "11", type: "other", desc: "胡亥是秦始皇次子" },
|
|
{ source: "3", target: "12", type: "other", desc: "项梁是项羽叔父" },
|
|
{ source: "3", target: "13", type: "other", desc: "项羽与陈胜同为反秦力量" },
|
|
{ source: "4", target: "2", type: "enemy", desc: "张良曾试图刺杀秦始皇" },
|
|
{ source: "6", target: "1", type: "other", desc: "韩信后被刘邦贬为淮阴侯" },
|
|
{ source: "6", target: "3", type: "enemy", desc: "韩信在垓下之战击败项羽" },
|
|
{ source: "10", target: "11", type: "enemy", desc: "胡亥害死扶苏" },
|
|
{ source: "1", target: "15", type: "collaboration", desc: "樊哙是刘邦将领" },
|
|
{ source: "14", target: "6", type: "enemy", desc: "吕后害死韩信" }
|
|
]
|
|
};
|
|
|
|
const colorMap = {
|
|
"emperor": "#e74c3c",
|
|
"warlord": "#3498db",
|
|
"strategist": "#2ecc71",
|
|
"minister": "#f39c12",
|
|
"general": "#9b59b6",
|
|
"prince": "#1abc9c",
|
|
"empress": "#e84393",
|
|
"rebel": "#d63031"
|
|
};
|
|
|
|
const linkColorMap = {
|
|
"enemy": "#ff7675",
|
|
"collaboration": "#74b9ff",
|
|
"other": "#a29bfe"
|
|
};
|
|
|
|
const width = window.innerWidth * 0.7;
|
|
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(-500))
|
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
.force("collision", d3.forceCollide().radius(d => d.importance * 5 + 10));
|
|
|
|
const link = svg.append("g")
|
|
.selectAll("line")
|
|
.data(data.links)
|
|
.enter().append("line")
|
|
.attr("class", "link")
|
|
.attr("stroke", d => linkColorMap[d.type])
|
|
.attr("stroke-width", 2);
|
|
|
|
const node = svg.append("g")
|
|
.selectAll("circle")
|
|
.data(data.nodes)
|
|
.enter().append("circle")
|
|
.attr("class", "node")
|
|
.attr("r", d => d.importance * 5 + 10)
|
|
.attr("fill", d => colorMap[d.type])
|
|
.call(d3.drag()
|
|
.on("start", dragstarted)
|
|
.on("drag", dragged)
|
|
.on("end", dragended))
|
|
.on("mouseover", showTooltip)
|
|
.on("mouseout", hideTooltip)
|
|
.on("click", showNodeInfo);
|
|
|
|
const label = svg.append("g")
|
|
.selectAll("text")
|
|
.data(data.nodes)
|
|
.enter().append("text")
|
|
.attr("dy", 4)
|
|
.attr("text-anchor", "middle")
|
|
.attr("dominant-baseline", "central")
|
|
.style("fill", "white")
|
|
.style("font-size", "12px")
|
|
.style("pointer-events", "none")
|
|
.text(d => d.name);
|
|
|
|
const tooltip = d3.select("body").append("div")
|
|
.attr("class", "tooltip")
|
|
.style("opacity", 0);
|
|
|
|
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 showTooltip(event, d) {
|
|
tooltip.transition()
|
|
.duration(200)
|
|
.style("opacity", .9);
|
|
tooltip.html(`<strong>${d.name}</strong><br/>${d.desc}`)
|
|
.style("left", (event.pageX + 10) + "px")
|
|
.style("top", (event.pageY - 28) + "px");
|
|
}
|
|
|
|
function hideTooltip() {
|
|
tooltip.transition()
|
|
.duration(500)
|
|
.style("opacity", 0);
|
|
}
|
|
|
|
function showNodeInfo(event, d) {
|
|
d3.select("#selected-node-title").text(d.name);
|
|
|
|
let infoHtml = `<p><strong>类型:</strong> ${getTypeName(d.type)}</p>
|
|
<p><strong>描述:</strong> ${d.desc}</p>
|
|
<p><strong>重要性:</strong> ${d.importance}/10</p>`;
|
|
|
|
d3.select("#selected-node-info").html(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 ?
|
|
data.nodes.find(n => n.id === rel.target.id) :
|
|
data.nodes.find(n => n.id === rel.source.id);
|
|
|
|
relationsHtml += `<li><span style="color:${linkColorMap[rel.type]}">■</span>
|
|
<strong>${otherNode.name}</strong>: ${rel.desc}</li>`;
|
|
});
|
|
|
|
relationsHtml += "</ul>";
|
|
d3.select("#node-relations").html(relationsHtml);
|
|
}
|
|
|
|
function getTypeName(type) {
|
|
const typeNames = {
|
|
"emperor": "皇帝",
|
|
"warlord": "军阀",
|
|
"strategist": "谋士",
|
|
"minister": "大臣",
|
|
"general": "将军",
|
|
"prince": "王子",
|
|
"empress": "皇后",
|
|
"rebel": "起义领袖"
|
|
};
|
|
return typeNames[type] || type;
|
|
}
|
|
|
|
// Layout controls
|
|
d3.select("#force-layout").on("click", function() {
|
|
simulation.force("charge", d3.forceManyBody().strength(-500))
|
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
.alphaTarget(0.3).restart();
|
|
});
|
|
|
|
d3.select("#radial-layout").on("click", function() {
|
|
simulation.force("center", null)
|
|
.force("radial", d3.forceRadial(height / 3, width / 2, height / 2).strength(0.1))
|
|
.alphaTarget(0.3).restart();
|
|
});
|
|
|
|
d3.select("#circular-layout").on("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.alphaTarget(0.3).restart();
|
|
});
|
|
|
|
d3.select("#grid-layout").on("click", function() {
|
|
const cols = Math.ceil(Math.sqrt(data.nodes.length));
|
|
const spacing = Math.min(width, height) / (cols + 1);
|
|
|
|
data.nodes.forEach((d, i) => {
|
|
d.fx = (i % cols) * spacing + spacing;
|
|
d.fy = Math.floor(i / cols) * spacing + spacing;
|
|
});
|
|
|
|
simulation.alphaTarget(0.3).restart();
|
|
});
|
|
|
|
// Responsive resize
|
|
window.addEventListener('resize', function() {
|
|
const newWidth = window.innerWidth * 0.7;
|
|
const newHeight = window.innerHeight;
|
|
|
|
svg.attr("width", newWidth).attr("height", newHeight);
|
|
|
|
simulation.force("center", d3.forceCenter(newWidth / 2, newHeight / 2))
|
|
.alphaTarget(0.3).restart();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|