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.

729 lines
26 KiB

2 weeks ago
<!DOCTYPE html>
<html lang="zh">
<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, #0a1a3a, #1a3a6a);
color: white;
font-family: "Microsoft YaHei", sans-serif;
overflow: hidden;
}
#header {
text-align: center;
padding: 20px 0;
background: rgba(0,0,0,0.3);
margin-bottom: 20px;
}
#title {
font-size: 36px;
font-weight: bold;
background: linear-gradient(to right, #ff8a00, #e52e71);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 10px rgba(255,138,0,0.3);
}
#container {
display: flex;
height: calc(100vh - 120px);
}
#graph {
flex: 3;
border-right: 1px solid rgba(255,255,255,0.1);
}
#panel {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.node {
stroke: #fff;
stroke-width: 1.5px;
filter: drop-shadow(0 0 5px rgba(255,255,255,0.5));
}
.node:hover {
filter: drop-shadow(0 0 10px gold);
}
.link {
stroke-opacity: 0.6;
}
.link.military {
stroke: #ff4d4d;
}
.link.political {
stroke: #4da6ff;
}
.link.family {
stroke: #66ff66;
}
.legend {
margin-top: 20px;
}
.legend-item {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.legend-color {
width: 20px;
height: 20px;
margin-right: 10px;
border-radius: 3px;
}
#tooltip {
position: absolute;
padding: 10px;
background: rgba(0,0,0,0.8);
border-radius: 5px;
pointer-events: none;
max-width: 200px;
font-size: 14px;
z-index: 10;
}
.layout-btn {
padding: 8px 15px;
margin-right: 10px;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
color: white;
cursor: pointer;
border-radius: 4px;
}
.layout-btn:hover {
background: rgba(255,255,255,0.2);
}
#controls {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.panel-title {
font-size: 20px;
margin-bottom: 15px;
border-bottom: 1px solid rgba(255,255,255,0.2);
padding-bottom: 10px;
}
.panel-content {
font-size: 14px;
line-height: 1.6;
}
.relation-item {
margin-bottom: 10px;
padding: 8px;
background: rgba(255,255,255,0.1);
border-radius: 4px;
}
</style>
</head>
<body>
<div id="header">
<div id="title">三国人物关系图谱</div>
</div>
<div id="controls">
<button class="layout-btn" onclick="changeLayout('force')">力导向布局</button>
<button class="layout-btn" onclick="changeLayout('radial')">辐射状布局</button>
<button class="layout-btn" onclick="changeLayout('circular')">环形布局</button>
<button class="layout-btn" onclick="changeLayout('grid')">网格布局</button>
</div>
<div id="container">
<div id="graph"></div>
<div id="panel">
<div class="panel-title">人物信息</div>
<div id="person-info" class="panel-content">
点击节点查看详细信息
</div>
<div class="panel-title" style="margin-top: 20px;">关系</div>
<div id="relations-list"></div>
<div class="panel-title legend">图例</div>
<div class="legend-item">
<div class="legend-color" style="background: #ff4d4d;"></div>
<div>军事关系</div>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #4da6ff;"></div>
<div>政治关系</div>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #66ff66;"></div>
<div>家族关系</div>
</div>
</div>
</div>
<div id="tooltip"></div>
<script>
// 数据定义
const data = {
"nodes": [
{
"id": "曹操",
"group": 1,
"type": "君主",
"desc": "字孟德,魏国奠基人,政治家、军事家、文学家",
"rank": 10
},
{
"id": "刘备",
"group": 2,
"type": "君主",
"desc": "字玄德,蜀汉开国皇帝,以仁德著称",
"rank": 10
},
{
"id": "孙权",
"group": 3,
"type": "君主",
"desc": "字仲谋吴国建立者统治江东50余年",
"rank": 10
},
{
"id": "诸葛亮",
"group": 2,
"type": "谋士",
"desc": "字孔明,蜀汉丞相,杰出的政治家、军事家",
"rank": 9
},
{
"id": "司马懿",
"group": 1,
"type": "谋士",
"desc": "字仲达,魏国重臣,晋朝奠基人",
"rank": 9
},
{
"id": "周瑜",
"group": 3,
"type": "武将",
"desc": "字公瑾,东吴名将,赤壁之战主帅",
"rank": 8
},
{
"id": "关羽",
"group": 2,
"type": "武将",
"desc": "字云长,蜀汉五虎上将之首,忠义化身",
"rank": 8
},
{
"id": "张飞",
"group": 2,
"type": "武将",
"desc": "字益德,蜀汉五虎上将,勇猛善战",
"rank": 7
},
{
"id": "赵云",
"group": 2,
"type": "武将",
"desc": "字子龙,蜀汉五虎上将,常胜将军",
"rank": 7
},
{
"id": "吕布",
"group": 4,
"type": "武将",
"desc": "字奉先,猛将,有\"人中吕布,马中赤兔\"之称",
"rank": 7
},
{
"id": "荀彧",
"group": 1,
"type": "谋士",
"desc": "字文若,曹操重要谋士,战略家",
"rank": 7
},
{
"id": "郭嘉",
"group": 1,
"type": "谋士",
"desc": "字奉孝,曹操重要谋士,英年早逝",
"rank": 7
},
{
"id": "鲁肃",
"group": 3,
"type": "谋士",
"desc": "字子敬,东吴战略家,促成孙刘联盟",
"rank": 7
},
{
"id": "陆逊",
"group": 3,
"type": "武将",
"desc": "字伯言,东吴名将,夷陵之战击败刘备",
"rank": 7
},
{
"id": "黄盖",
"group": 3,
"type": "武将",
"desc": "字公覆,东吴老将,赤壁之战献苦肉计",
"rank": 6
},
{
"id": "马超",
"group": 2,
"type": "武将",
"desc": "字孟起,蜀汉五虎上将,西凉名将",
"rank": 6
},
{
"id": "黄忠",
"group": 2,
"type": "武将",
"desc": "字汉升,蜀汉五虎上将,老当益壮",
"rank": 6
},
{
"id": "夏侯惇",
"group": 1,
"type": "武将",
"desc": "字元让,曹操宗族大将,独眼将军",
"rank": 6
},
{
"id": "张辽",
"group": 1,
"type": "武将",
"desc": "字文远,魏国名将,逍遥津之战威震江东",
"rank": 6
},
{
"id": "许褚",
"group": 1,
"type": "武将",
"desc": "字仲康,曹操虎卫,号称\"虎痴\"",
"rank": 5
}
],
"links": [
{
"source": "曹操",
"target": "刘备",
"value": 5,
"type": "political",
"desc": "早期合作对抗董卓,后期成为主要对手"
},
{
"source": "曹操",
"target": "孙权",
"value": 5,
"type": "political",
"desc": "赤壁之战对手,长期对峙"
},
{
"source": "刘备",
"target": "孙权",
"value": 5,
"type": "political",
"desc": "曾结盟抗曹,后因荆州问题反目"
},
{
"source": "刘备",
"target": "诸葛亮",
"value": 8,
"type": "political",
"desc": "三顾茅庐请出,君臣相得"
},
{
"source": "曹操",
"target": "司马懿",
"value": 6,
"type": "political",
"desc": "重要谋士,后期掌握大权"
},
{
"source": "孙权",
"target": "周瑜",
"value": 7,
"type": "political",
"desc": "君臣关系,周瑜为东吴重要将领"
},
{
"source": "刘备",
"target": "关羽",
"value": 8,
"type": "military",
"desc": "结义兄弟,重要将领"
},
{
"source": "刘备",
"target": "张飞",
"value": 8,
"type": "military",
"desc": "结义兄弟,重要将领"
},
{
"source": "刘备",
"target": "赵云",
"value": 7,
"type": "military",
"desc": "重要将领,曾救阿斗"
},
{
"source": "曹操",
"target": "荀彧",
"value": 7,
"type": "political",
"desc": "重要谋士,战略规划者"
},
{
"source": "曹操",
"target": "郭嘉",
"value": 7,
"type": "political",
"desc": "重要谋士,英年早逝"
},
{
"source": "孙权",
"target": "鲁肃",
"value": 6,
"type": "political",
"desc": "重要谋士,主张联刘抗曹"
},
{
"source": "孙权",
"target": "陆逊",
"value": 6,
"type": "military",
"desc": "重要将领,夷陵之战主帅"
},
{
"source": "周瑜",
"target": "诸葛亮",
"value": 5,
"type": "political",
"desc": "赤壁之战合作,后因荆州问题对立"
},
{
"source": "关羽",
"target": "曹操",
"value": 5,
"type": "political",
"desc": "曾短暂投靠曹操,后回归刘备"
},
{
"source": "吕布",
"target": "曹操",
"value": 5,
"type": "military",
"desc": "敌对关系,被曹操擒杀"
},
{
"source": "吕布",
"target": "刘备",
"value": 5,
"type": "political",
"desc": "曾合作又反目,夺取徐州"
},
{
"source": "曹操",
"target": "夏侯惇",
"value": 6,
"type": "family",
"desc": "宗族关系,重要将领"
},
{
"source": "曹操",
"target": "张辽",
"value": 6,
"type": "military",
"desc": "重要降将,五子良将之首"
},
{
"source": "曹操",
"target": "许褚",
"value": 5,
"type": "military",
"desc": "贴身护卫,忠勇将领"
},
{
"source": "刘备",
"target": "马超",
"value": 5,
"type": "military",
"desc": "重要降将,五虎上将"
},
{
"source": "刘备",
"target": "黄忠",
"value": 5,
"type": "military",
"desc": "重要将领,五虎上将"
},
{
"source": "孙权",
"target": "黄盖",
"value": 5,
"type": "military",
"desc": "重要将领,献苦肉计"
}
]
};
// 可视化实现
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)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");
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("x", d3.forceX(width / 2).strength(0.05))
.force("y", d3.forceY(height / 2).strength(0.05))
.force("collision", d3.forceCollide().radius(d => Math.sqrt(d.rank) * 10));
// 定义渐变
const defs = svg.append("defs");
const gradient1 = defs.append("radialGradient")
.attr("id", "nodeGradient1")
.attr("cx", "50%")
.attr("cy", "50%")
.attr("r", "70%")
.attr("fx", "40%")
.attr("fy", "40%");
gradient1.append("stop").attr("offset", "0%").attr("stop-color", "#4da6ff");
gradient1.append("stop").attr("offset", "100%").attr("stop-color", "#1a3a6a");
const gradient2 = defs.append("radialGradient")
.attr("id", "nodeGradient2")
.attr("cx", "50%")
.attr("cy", "50%")
.attr("r", "70%")
.attr("fx", "40%")
.attr("fy", "40%");
gradient2.append("stop").attr("offset", "0%").attr("stop-color", "#ff6666");
gradient2.append("stop").attr("offset", "100%").attr("stop-color", "#8b0000");
const gradient3 = defs.append("radialGradient")
.attr("id", "nodeGradient3")
.attr("cx", "50%")
.attr("cy", "50%")
.attr("r", "70%")
.attr("fx", "40%")
.attr("fy", "40%");
gradient3.append("stop").attr("offset", "0%").attr("stop-color", "#66ff66");
gradient3.append("stop").attr("offset", "100%").attr("stop-color", "#006400");
const gradient4 = defs.append("radialGradient")
.attr("id", "nodeGradient4")
.attr("cx", "50%")
.attr("cy", "50%")
.attr("r", "70%")
.attr("fx", "40%")
.attr("fy", "40%");
gradient4.append("stop").attr("offset", "0%").attr("stop-color", "#ffcc00");
gradient4.append("stop").attr("offset", "100%").attr("stop-color", "#996600");
// 绘制连线
const link = svg.append("g")
.selectAll("line")
.data(data.links)
.join("line")
.attr("class", d => `link ${d.type}`)
.attr("stroke-width", d => Math.sqrt(d.value));
// 绘制节点
const node = svg.append("g")
.selectAll("circle")
.data(data.nodes)
.join("circle")
.attr("class", "node")
.attr("r", d => Math.sqrt(d.rank) * 5)
.attr("fill", d => {
switch(d.group) {
case 1: return "url(#nodeGradient1)";
case 2: return "url(#nodeGradient2)";
case 3: return "url(#nodeGradient3)";
default: return "url(#nodeGradient4)";
}
})
.call(drag(simulation))
.on("mouseover", showTooltip)
.on("mouseout", hideTooltip)
.on("click", updatePanel);
// 添加节点文字
const text = svg.append("g")
.selectAll("text")
.data(data.nodes)
.join("text")
.attr("dy", 4)
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.text(d => d.id)
.style("font-size", d => Math.max(10, Math.sqrt(d.rank) * 3))
.style("fill", "white")
.style("pointer-events", "none");
// 更新力导向布局
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);
text
.attr("x", d => d.x)
.attr("y", d => d.y);
});
// 拖拽功能
function drag(simulation) {
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;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
// 工具提示
const tooltip = d3.select("#tooltip");
function showTooltip(event, d) {
tooltip
.style("opacity", 1)
.html(`<strong>${d.id}</strong><br>${d.type}<br>${d.desc}`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 10) + "px");
}
function hideTooltip() {
tooltip.style("opacity", 0);
}
// 更新右侧面板
function updatePanel(event, d) {
// 更新人物信息
document.getElementById("person-info").innerHTML = `
<h3>${d.id}</h3>
<p><strong>类型:</strong> ${d.type}</p>
<p><strong>描述:</strong> ${d.desc}</p>
<p><strong>重要性:</strong> ${d.rank}/10</p>
`;
// 更新关系列表
const relations = data.links.filter(
link => link.source.id === d.id || link.target.id === d.id
);
let relationsHtml = "";
relations.forEach(rel => {
const otherNode = rel.source.id === d.id ? rel.target : rel.source;
const relationType =
rel.type === "military" ? "军事关系" :
rel.type === "political" ? "政治关系" : "家族关系";
relationsHtml += `
<div class="relation-item">
<strong>${otherNode}</strong>
<p>关系类型: ${relationType}</p>
<p>${rel.desc}</p>
</div>
`;
});
document.getElementById("relations-list").innerHTML = relationsHtml || "<p>无记录的关系</p>";
}
// 布局切换
function changeLayout(type) {
simulation.stop();
switch(type) {
case "force":
simulation
.force("x", d3.forceX(width / 2).strength(0.05))
.force("y", d3.forceY(height / 2).strength(0.05))
.force("charge", d3.forceManyBody().strength(-300))
.alpha(1).restart();
break;
case "radial":
simulation
.force("x", null)
.force("y", null)
.force("radial", d3.forceRadial(height / 3, width / 2, height / 2).strength(0.1))
.force("charge", d3.forceManyBody().strength(-500))
.alpha(1).restart();
break;
case "circular":
simulation
.force("x", null)
.force("y", null)
.force("charge", null)
.force("circle", forceCircle())
.alpha(1).restart();
break;
case "grid":
simulation
.force("x", d3.forceX(d => (data.nodes.indexOf(d) % 5) * 150 + 100).strength(1))
.force("y", d3.forceY(d => Math.floor(data.nodes.indexOf(d) / 5) * 150 + 100).strength(1))
.force("charge", d3.forceManyBody().strength(-1000))
.alpha(1).restart();
break;
}
}
// 圆形布局力
function forceCircle() {
const radius = Math.min(width, height) / 3;
let nodes;
function force(alpha) {
const centerX = width / 2;
const centerY = height / 2;
nodes.forEach((d, i) => {
const angle = (i * 2 * Math.PI) / nodes.length;
d.x = centerX + radius * Math.cos(angle);
d.y = centerY + radius * Math.sin(angle);
});
}
force.initialize = _ => nodes = _;
return force;
}
</script>
</body>
</html>