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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!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>