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.
708 lines
24 KiB
708 lines
24 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;
|
|
padding: 0;
|
|
background: linear-gradient(135deg, #0a1f44, #1e3d7a);
|
|
font-family: 'Arial', sans-serif;
|
|
color: white;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#header {
|
|
text-align: center;
|
|
padding: 20px 0;
|
|
background: linear-gradient(90deg, rgba(0,0,0,0), rgba(66, 165, 245, 0.5), rgba(0,0,0,0));
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
#title {
|
|
font-size: 36px;
|
|
font-weight: bold;
|
|
background: linear-gradient(90deg, #ff8a00, #e52e71);
|
|
-webkit-background-clip: text;
|
|
background-clip: text;
|
|
color: transparent;
|
|
text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
|
|
}
|
|
|
|
#container {
|
|
display: flex;
|
|
height: calc(100vh - 100px);
|
|
}
|
|
|
|
#graph {
|
|
flex: 3;
|
|
height: 100%;
|
|
}
|
|
|
|
#panel {
|
|
flex: 1;
|
|
padding: 20px;
|
|
background: rgba(15, 30, 60, 0.8);
|
|
border-left: 1px solid #2a4a7a;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.node {
|
|
stroke: #fff;
|
|
stroke-width: 1.5px;
|
|
filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.5));
|
|
}
|
|
|
|
.link {
|
|
stroke-opacity: 0.6;
|
|
}
|
|
|
|
.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: 50%;
|
|
}
|
|
|
|
.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;
|
|
z-index: 10;
|
|
}
|
|
|
|
.panel-title {
|
|
font-size: 24px;
|
|
margin-bottom: 20px;
|
|
color: #4fc3f7;
|
|
border-bottom: 1px solid #2a4a7a;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.panel-content {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.panel-section {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.panel-section-title {
|
|
font-size: 18px;
|
|
color: #81d4fa;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.layout-controls {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.layout-btn {
|
|
background: #2a4a7a;
|
|
color: white;
|
|
border: none;
|
|
padding: 8px 12px;
|
|
margin-right: 5px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.layout-btn:hover {
|
|
background: #3a5a8a;
|
|
}
|
|
|
|
.layout-btn.active {
|
|
background: #4fc3f7;
|
|
color: #0a1f44;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="header">
|
|
<div id="title">刘邦集团人物关系网络图</div>
|
|
</div>
|
|
|
|
<div id="container">
|
|
<div id="graph"></div>
|
|
<div id="panel">
|
|
<div class="panel-title">节点信息</div>
|
|
<div id="node-info" class="panel-content">
|
|
<div class="panel-section">
|
|
<div class="panel-section-title">请点击节点查看详情</div>
|
|
</div>
|
|
</div>
|
|
<div class="panel-section">
|
|
<div class="panel-section-title">关系类型</div>
|
|
<div class="legend">
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background-color: #ff5252;"></div>
|
|
<div>敌对关系</div>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background-color: #4caf50;"></div>
|
|
<div>合作关系</div>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background-color: #ffeb3b;"></div>
|
|
<div>亲属关系</div>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background-color: #9c27b0;"></div>
|
|
<div>上下级关系</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="layout-controls">
|
|
<div class="panel-section-title">布局切换</div>
|
|
<button class="layout-btn active" data-layout="force">力导向</button>
|
|
<button class="layout-btn" data-layout="radial">辐射状</button>
|
|
<button class="layout-btn" data-layout="circular">环形</button>
|
|
<button class="layout-btn" data-layout="grid">网格</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tooltip"></div>
|
|
|
|
<script>
|
|
// Data definition
|
|
const data = {
|
|
"nodes": [
|
|
{
|
|
"id": "1",
|
|
"name": "刘邦",
|
|
"type": "帝王",
|
|
"desc": "汉朝开国皇帝,从平民起义到建立汉朝",
|
|
"importance": 10,
|
|
"group": 1
|
|
},
|
|
{
|
|
"id": "2",
|
|
"name": "吕后",
|
|
"type": "皇后",
|
|
"desc": "刘邦的妻子,汉朝第一位皇后,后期掌握大权",
|
|
"importance": 9,
|
|
"group": 1
|
|
},
|
|
{
|
|
"id": "3",
|
|
"name": "项羽",
|
|
"type": "诸侯",
|
|
"desc": "楚汉相争中刘邦的主要对手",
|
|
"importance": 9,
|
|
"group": 2
|
|
},
|
|
{
|
|
"id": "4",
|
|
"name": "韩信",
|
|
"type": "将军",
|
|
"desc": "汉初三大名将之一,为刘邦立下赫赫战功",
|
|
"importance": 8,
|
|
"group": 1
|
|
},
|
|
{
|
|
"id": "5",
|
|
"name": "张良",
|
|
"type": "谋士",
|
|
"desc": "刘邦的重要谋臣,汉初三杰之一",
|
|
"importance": 8,
|
|
"group": 1
|
|
},
|
|
{
|
|
"id": "6",
|
|
"name": "萧何",
|
|
"type": "丞相",
|
|
"desc": "刘邦的重要助手,汉初三杰之一",
|
|
"importance": 8,
|
|
"group": 1
|
|
},
|
|
{
|
|
"id": "7",
|
|
"name": "曹参",
|
|
"type": "将军",
|
|
"desc": "刘邦的重要将领,后接替萧何为相",
|
|
"importance": 7,
|
|
"group": 1
|
|
},
|
|
{
|
|
"id": "8",
|
|
"name": "英布",
|
|
"type": "诸侯",
|
|
"desc": "初为项羽部将,后归刘邦,最终反叛",
|
|
"importance": 7,
|
|
"group": 1
|
|
},
|
|
{
|
|
"id": "9",
|
|
"name": "彭越",
|
|
"type": "将军",
|
|
"desc": "刘邦的重要将领,后因谋反罪名被杀",
|
|
"importance": 7,
|
|
"group": 1
|
|
},
|
|
{
|
|
"id": "10",
|
|
"name": "项伯",
|
|
"type": "谋士",
|
|
"desc": "项羽的叔父,多次帮助刘邦",
|
|
"importance": 6,
|
|
"group": 2
|
|
},
|
|
{
|
|
"id": "11",
|
|
"name": "范增",
|
|
"type": "谋士",
|
|
"desc": "项羽的主要谋臣,被离间计陷害",
|
|
"importance": 6,
|
|
"group": 2
|
|
},
|
|
{
|
|
"id": "12",
|
|
"name": "戚夫人",
|
|
"type": "妃子",
|
|
"desc": "刘邦宠妃,与吕后争权失败被做成人彘",
|
|
"importance": 5,
|
|
"group": 1
|
|
},
|
|
{
|
|
"id": "13",
|
|
"name": "刘盈",
|
|
"type": "太子",
|
|
"desc": "刘邦与吕后之子,汉惠帝",
|
|
"importance": 5,
|
|
"group": 1
|
|
},
|
|
{
|
|
"id": "14",
|
|
"name": "刘如意",
|
|
"type": "诸侯",
|
|
"desc": "刘邦与戚夫人之子,赵王",
|
|
"importance": 5,
|
|
"group": 1
|
|
},
|
|
{
|
|
"id": "15",
|
|
"name": "审食其",
|
|
"type": "大臣",
|
|
"desc": "刘邦同乡,吕后信任的人",
|
|
"importance": 4,
|
|
"group": 1
|
|
},
|
|
{
|
|
"id": "16",
|
|
"name": "周勃",
|
|
"type": "将军",
|
|
"desc": "刘邦同乡,开国功臣",
|
|
"importance": 4,
|
|
"group": 1
|
|
}
|
|
],
|
|
"links": [
|
|
{
|
|
"source": "1",
|
|
"target": "2",
|
|
"type": "亲属关系",
|
|
"desc": "夫妻关系,后期关系紧张"
|
|
},
|
|
{
|
|
"source": "1",
|
|
"target": "3",
|
|
"type": "敌对关系",
|
|
"desc": "楚汉相争的主要对手"
|
|
},
|
|
{
|
|
"source": "1",
|
|
"target": "4",
|
|
"type": "上下级关系",
|
|
"desc": "刘邦重用韩信又猜忌他"
|
|
},
|
|
{
|
|
"source": "1",
|
|
"target": "5",
|
|
"type": "合作关系",
|
|
"desc": "张良是刘邦最重要的谋士"
|
|
},
|
|
{
|
|
"source": "1",
|
|
"target": "6",
|
|
"type": "合作关系",
|
|
"desc": "萧何是刘邦最重要的助手"
|
|
},
|
|
{
|
|
"source": "1",
|
|
"target": "7",
|
|
"type": "上下级关系",
|
|
"desc": "曹参是刘邦的重要将领"
|
|
},
|
|
{
|
|
"source": "1",
|
|
"target": "8",
|
|
"type": "上下级关系",
|
|
"desc": "英布先归顺后反叛刘邦"
|
|
},
|
|
{
|
|
"source": "1",
|
|
"target": "9",
|
|
"type": "上下级关系",
|
|
"desc": "彭越为刘邦立下战功后被处死"
|
|
},
|
|
{
|
|
"source": "1",
|
|
"target": "10",
|
|
"type": "合作关系",
|
|
"desc": "项伯多次在关键时刻帮助刘邦"
|
|
},
|
|
{
|
|
"source": "1",
|
|
"target": "12",
|
|
"type": "亲属关系",
|
|
"desc": "刘邦宠妃,引发与吕后的矛盾"
|
|
},
|
|
{
|
|
"source": "1",
|
|
"target": "13",
|
|
"type": "亲属关系",
|
|
"desc": "刘邦与吕后之子"
|
|
},
|
|
{
|
|
"source": "1",
|
|
"target": "14",
|
|
"type": "亲属关系",
|
|
"desc": "刘邦与戚夫人之子"
|
|
},
|
|
{
|
|
"source": "1",
|
|
"target": "15",
|
|
"type": "上下级关系",
|
|
"desc": "审食其是刘邦同乡,受吕后信任"
|
|
},
|
|
{
|
|
"source": "1",
|
|
"target": "16",
|
|
"type": "上下级关系",
|
|
"desc": "周勃是刘邦同乡,开国功臣"
|
|
},
|
|
{
|
|
"source": "2",
|
|
"target": "12",
|
|
"type": "敌对关系",
|
|
"desc": "吕后与戚夫人的权力斗争"
|
|
},
|
|
{
|
|
"source": "2",
|
|
"target": "14",
|
|
"type": "敌对关系",
|
|
"desc": "吕后毒杀刘如意"
|
|
},
|
|
{
|
|
"source": "2",
|
|
"target": "15",
|
|
"type": "合作关系",
|
|
"desc": "审食其是吕后最信任的人"
|
|
},
|
|
{
|
|
"source": "3",
|
|
"target": "10",
|
|
"type": "亲属关系",
|
|
"desc": "项羽与项伯是叔侄关系"
|
|
},
|
|
{
|
|
"source": "3",
|
|
"target": "11",
|
|
"type": "上下级关系",
|
|
"desc": "范增是项羽的主要谋士"
|
|
},
|
|
{
|
|
"source": "4",
|
|
"target": "6",
|
|
"type": "合作关系",
|
|
"desc": "萧何月下追韩信"
|
|
},
|
|
{
|
|
"source": "5",
|
|
"target": "10",
|
|
"type": "合作关系",
|
|
"desc": "张良与项伯有交情"
|
|
},
|
|
{
|
|
"source": "6",
|
|
"target": "7",
|
|
"type": "合作关系",
|
|
"desc": "萧何与曹参关系复杂"
|
|
},
|
|
{
|
|
"source": "8",
|
|
"target": "3",
|
|
"type": "上下级关系",
|
|
"desc": "英布曾是项羽部将"
|
|
}
|
|
]
|
|
};
|
|
|
|
// Color scale for different types
|
|
const color = d3.scaleOrdinal()
|
|
.domain(["帝王", "皇后", "诸侯", "将军", "谋士", "丞相", "妃子", "太子", "大臣"])
|
|
.range(["#e53935", "#8e24aa", "#3949ab", "#039be5", "#00897b", "#43a047", "#f4511e", "#fb8c00", "#6d4c41"]);
|
|
|
|
// Relationship type colors
|
|
const linkColor = d3.scaleOrdinal()
|
|
.domain(["敌对关系", "合作关系", "亲属关系", "上下级关系"])
|
|
.range(["#ff5252", "#4caf50", "#ffeb3b", "#9c27b0"]);
|
|
|
|
// Graph dimensions
|
|
const width = document.getElementById('graph').clientWidth;
|
|
const height = document.getElementById('graph').clientHeight;
|
|
|
|
// Create SVG
|
|
const svg = d3.select("#graph")
|
|
.append("svg")
|
|
.attr("width", width)
|
|
.attr("height", height)
|
|
.attr("viewBox", [0, 0, width, height])
|
|
.call(d3.zoom().on("zoom", function(event) {
|
|
svg.attr("transform", event.transform);
|
|
}))
|
|
.append("g");
|
|
|
|
// Tooltip
|
|
const tooltip = d3.select(".tooltip");
|
|
|
|
// Simulation
|
|
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));
|
|
|
|
// Drag functions
|
|
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;
|
|
}
|
|
|
|
// Create links
|
|
const link = svg.append("g")
|
|
.selectAll("line")
|
|
.data(data.links)
|
|
.enter().append("line")
|
|
.attr("class", "link")
|
|
.attr("stroke", d => linkColor(d.type))
|
|
.attr("stroke-width", 2);
|
|
|
|
// Create nodes
|
|
const node = svg.append("g")
|
|
.selectAll("circle")
|
|
.data(data.nodes)
|
|
.enter().append("circle")
|
|
.attr("class", "node")
|
|
.attr("r", d => 10 + d.importance * 2)
|
|
.attr("fill", d => color(d.type))
|
|
.call(d3.drag()
|
|
.on("start", dragstarted)
|
|
.on("drag", dragged)
|
|
.on("end", dragended))
|
|
.on("mouseover", function(event, d) {
|
|
tooltip.transition()
|
|
.duration(200)
|
|
.style("opacity", .9);
|
|
tooltip.html(`<strong>${d.name}</strong><br/>${d.type}<br/><br/>${d.desc}`)
|
|
.style("left", (event.pageX + 10) + "px")
|
|
.style("top", (event.pageY - 28) + "px");
|
|
})
|
|
.on("mouseout", function() {
|
|
tooltip.transition()
|
|
.duration(500)
|
|
.style("opacity", 0);
|
|
})
|
|
.on("click", function(event, d) {
|
|
updatePanel(d);
|
|
});
|
|
|
|
// Add labels
|
|
const labels = svg.append("g")
|
|
.selectAll("text")
|
|
.data(data.nodes)
|
|
.enter().append("text")
|
|
.attr("dy", -15)
|
|
.attr("text-anchor", "middle")
|
|
.attr("dominant-baseline", "central")
|
|
.style("fill", "white")
|
|
.style("font-size", "12px")
|
|
.text(d => d.name);
|
|
|
|
// Update panel info
|
|
function updatePanel(d) {
|
|
const panel = d3.select("#node-info");
|
|
panel.html(`
|
|
<div class="panel-section">
|
|
<div class="panel-section-title">${d.name}</div>
|
|
<div>类型: ${d.type}</div>
|
|
<div>重要性: ${d.importance}/10</div>
|
|
</div>
|
|
<div class="panel-section">
|
|
<div class="panel-section-title">描述</div>
|
|
<div>${d.desc}</div>
|
|
</div>
|
|
<div class="panel-section">
|
|
<div class="panel-section-title">关系</div>
|
|
<div>${getRelationships(d.id)}</div>
|
|
</div>
|
|
`);
|
|
}
|
|
|
|
// Get relationships for a node
|
|
function getRelationships(nodeId) {
|
|
let relationships = [];
|
|
|
|
// Find links where this node is source or target
|
|
data.links.forEach(link => {
|
|
if (link.source.id === nodeId || link.source === nodeId) {
|
|
const targetNode = data.nodes.find(n => n.id === link.target || n.id === link.target.id);
|
|
relationships.push({
|
|
name: targetNode.name,
|
|
type: link.type,
|
|
desc: link.desc,
|
|
direction: "out"
|
|
});
|
|
}
|
|
|
|
if (link.target.id === nodeId || link.target === nodeId) {
|
|
const sourceNode = data.nodes.find(n => n.id === link.source || n.id === link.source.id);
|
|
relationships.push({
|
|
name: sourceNode.name,
|
|
type: link.type,
|
|
desc: link.desc,
|
|
direction: "in"
|
|
});
|
|
}
|
|
});
|
|
|
|
// Format the relationships for display
|
|
let html = '<ul style="padding-left: 20px;">';
|
|
relationships.forEach(r => {
|
|
const arrow = r.direction === "out" ? "→" : "←";
|
|
html += `<li><strong>${r.name}</strong> ${arrow} <span style="color: ${linkColor(r.type)}">${r.type}</span><br/><small>${r.desc}</small></li>`;
|
|
});
|
|
html += '</ul>';
|
|
|
|
return html;
|
|
}
|
|
|
|
// Update positions
|
|
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);
|
|
|
|
labels
|
|
.attr("x", d => d.x)
|
|
.attr("y", d => d.y);
|
|
});
|
|
|
|
// Layout controls
|
|
d3.selectAll(".layout-btn").on("click", function() {
|
|
d3.selectAll(".layout-btn").classed("active", false);
|
|
d3.select(this).classed("active", true);
|
|
|
|
const layout = d3.select(this).attr("data-layout");
|
|
changeLayout(layout);
|
|
});
|
|
|
|
// Change layout
|
|
function changeLayout(layout) {
|
|
switch(layout) {
|
|
case "force":
|
|
simulation
|
|
.force("charge", d3.forceManyBody().strength(-300))
|
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
.alphaTarget(0.3)
|
|
.restart();
|
|
break;
|
|
|
|
case "radial":
|
|
simulation
|
|
.force("radial", d3.forceRadial(width / 3, width / 2, height / 2).strength(0.1))
|
|
.force("center", null)
|
|
.alphaTarget(0.3)
|
|
.restart();
|
|
break;
|
|
|
|
case "circular":
|
|
const radius = Math.min(width, height) / 2 - 100;
|
|
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
|
|
.force("radial", null)
|
|
.force("center", null)
|
|
.alphaTarget(0)
|
|
.restart();
|
|
break;
|
|
|
|
case "grid":
|
|
const cols = Math.ceil(Math.sqrt(data.nodes.length));
|
|
const cellSize = Math.min(width, height) / cols;
|
|
|
|
data.nodes.forEach((d, i) => {
|
|
const row = Math.floor(i / cols);
|
|
const col = i % cols;
|
|
d.fx = col * cellSize + cellSize / 2;
|
|
d.fy = row * cellSize + cellSize / 2;
|
|
});
|
|
|
|
simulation
|
|
.force("radial", null)
|
|
.force("center", null)
|
|
.alphaTarget(0)
|
|
.restart();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Initial layout
|
|
changeLayout("force");
|
|
</script>
|
|
</body>
|
|
</html>
|