feat(visualizer): 引入核心图按需加载和节点扩展功能
为了解决记忆图谱数据量过大导致前端加载缓慢和渲染卡顿的问题,本次更新引入了核心图按需加载和节点扩展机制。
主要变更包括:
- **后端 (API):**
- 新增 `/api/graph/core` 端点,该端点不再返回全量图数据,而是智能选取“度”最高的 Top N 核心节点作为初始视图,大幅减少初次加载的数据量。
- 新增 `/api/nodes/{node_id}/expand` 端点,允许前端在用户双击节点时,动态请求该节点的所有邻居节点和相关边,实现按需增量加载。
- 优化了数据加载逻辑,在内存中构建并缓存了节点字典和邻接表,以极高的效率支持节点扩展查询。
- **前端 (UI):**
- 初始加载逻辑从请求 `/api/graph/full` 切换到新的 `/api/graph/core` 端点。
- 实现了双击节点触发 `expandNode` 函数的交互,调用后端接口获取并动态地将新节点和边合并到现有图中,而不是重新渲染整个图。
- 使用 `vis.DataSet` 来管理图数据,支持高效地动态添加和更新节点与边。
- 节点大小现在与其“度”(连接数)相关联,使得关键节点在视觉上更加突出。
This commit is contained in:
@@ -532,12 +532,17 @@
|
||||
<script>
|
||||
let network = null;
|
||||
let availableFiles = [];
|
||||
// 现在的数据集是动态增长的,我们需要用 vis.DataSet 来管理
|
||||
let nodesDataSet = new vis.DataSet([])
|
||||
;
|
||||
let edgesDataSet = new vis.DataSet([]);
|
||||
|
||||
let graphData = {
|
||||
nodes: [],
|
||||
nodes: [], // 这将作为原始数据的备份
|
||||
edges: [],
|
||||
memories: []
|
||||
};
|
||||
let originalData = null;
|
||||
let originalData = null; // 用于过滤器
|
||||
|
||||
// 节点颜色配置
|
||||
const nodeColors = {
|
||||
@@ -618,26 +623,32 @@
|
||||
dragView: true
|
||||
}
|
||||
};
|
||||
// 初始化时使用我们可动态管理的 DataSet
|
||||
const data = {
|
||||
nodes: nodesDataSet,
|
||||
edges: edgesDataSet
|
||||
};
|
||||
|
||||
const data = {
|
||||
nodes: new vis.DataSet([]),
|
||||
edges: new vis.DataSet([])
|
||||
};
|
||||
|
||||
network = new vis.Network(container, data, options);
|
||||
network = new vis.Network(container, data, options);
|
||||
|
||||
// 添加事件监听
|
||||
network.on('click', function(params) {
|
||||
if (params.nodes.length > 0) {
|
||||
const nodeId = params.nodes[0];
|
||||
showNodeInfo(nodeId);
|
||||
highlightConnectedNodes(nodeId);
|
||||
// 单击时只高亮,不再执行复杂的BFS
|
||||
} else {
|
||||
// 点击空白处,恢复所有节点
|
||||
resetNodeHighlight();
|
||||
resetNodeHighlight(); // 点击空白处恢复
|
||||
}
|
||||
});
|
||||
|
||||
// 这才是我们的秘密武器: 双击扩展! 哼哼~
|
||||
network.on('doubleClick', async function(params) {
|
||||
if (params.nodes.length > 0) {
|
||||
const nodeId = params.nodes[0];
|
||||
await expandNode(nodeId);
|
||||
}
|
||||
});
|
||||
// 稳定化完成后停止物理引擎
|
||||
network.on('stabilizationIterationsDone', function() {
|
||||
console.log('初始稳定化完成,停止物理引擎');
|
||||
@@ -657,16 +668,20 @@
|
||||
async function loadGraph() {
|
||||
try {
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
|
||||
const response = await fetch('/visualizer/api/graph/full');
|
||||
// 请求新的核心节点接口,而不是那个又笨又重的full接口
|
||||
const response = await fetch('/visualizer/api/graph/core');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
originalData = result.data;
|
||||
updateGraph(result.data);
|
||||
originalData = result.data; // 保存原始数据用于过滤
|
||||
// 初始加载时,清空旧数据
|
||||
nodesDataSet.clear();
|
||||
edgesDataSet.clear();
|
||||
|
||||
updateGraph(result.data, true); // true表示是初始加载
|
||||
updateStats(result.data.stats);
|
||||
} else {
|
||||
alert('加载失败: ' + result.error);
|
||||
alert('加载核心节点失败: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载图形失败:', error);
|
||||
@@ -675,41 +690,62 @@
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新图形显示
|
||||
function updateGraph(data) {
|
||||
graphData = data;
|
||||
function updateGraph(data, isInitialLoad = false) {
|
||||
if (isInitialLoad) {
|
||||
// 如果是初始加载,则完全替换数据
|
||||
graphData = data;
|
||||
} else {
|
||||
// 如果是扩展,则合并数据
|
||||
// 使用一个Set来避免重复添加节点
|
||||
const existingNodeIds = new Set(graphData.nodes.map(n => n.id));
|
||||
data.nodes.forEach(newNode => {
|
||||
if (!existingNodeIds.has(newNode.id)) {
|
||||
graphData.nodes.push(newNode);
|
||||
existingNodeIds.add(newNode.id);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理节点数据
|
||||
const nodes = data.nodes.map(node => ({
|
||||
// 同样避免重复添加边
|
||||
const existingEdgeIds = new Set(graphData.edges.map(e => e.id));
|
||||
data.edges.forEach(newEdge => {
|
||||
if (!existingEdgeIds.has(newEdge.id)) {
|
||||
graphData.edges.push(newEdge);
|
||||
existingEdgeIds.add(newEdge.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理节点数据,添加或更新到DataSet
|
||||
const nodesToAdd = data.nodes.map(node => ({
|
||||
id: node.id,
|
||||
label: node.label,
|
||||
title: node.title,
|
||||
group: node.group,
|
||||
color: nodeColors[node.group] || '#999',
|
||||
// 瞧,现在节点越大,就说明它越重要,是不是很酷?
|
||||
size: 15 + Math.min((node.degree || 0) * 2, 20),
|
||||
metadata: node.metadata
|
||||
}));
|
||||
nodesDataSet.update(nodesToAdd);
|
||||
|
||||
// 处理边数据
|
||||
const edges = data.edges.map(edge => ({
|
||||
// 处理边数据,添加到DataSet
|
||||
const edgesToAdd = data.edges.map(edge => ({
|
||||
id: edge.id,
|
||||
from: edge.from,
|
||||
to: edge.to,
|
||||
label: edge.label,
|
||||
title: edge.title,
|
||||
width: edge.importance * 3 + 1
|
||||
// 根据重要性调整边的宽度
|
||||
width: (edge.importance || 0.5) * 2 + 1
|
||||
}));
|
||||
edgesDataSet.update(edgesToAdd);
|
||||
|
||||
// 更新网络
|
||||
network.setData({
|
||||
nodes: new vis.DataSet(nodes),
|
||||
edges: new vis.DataSet(edges)
|
||||
});
|
||||
|
||||
// 注意:setData 会自动触发物理引擎重新布局
|
||||
// stabilizationIterationsDone 事件监听器会自动停止物理引擎
|
||||
// 只有在添加新节点时才需要重新稳定布局
|
||||
if (nodesToAdd.length > 0) {
|
||||
network.stabilize();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
function updateStats(stats) {
|
||||
document.getElementById('statNodes').textContent = stats.total_nodes;
|
||||
@@ -1013,18 +1049,40 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 适应窗口
|
||||
function fitNetwork() {
|
||||
if (network) {
|
||||
network.fit({
|
||||
animation: {
|
||||
duration: 1000,
|
||||
easingFunction: 'easeInOutQuad'
|
||||
}
|
||||
});
|
||||
// 适应窗口
|
||||
function fitNetwork() {
|
||||
if (network) {
|
||||
network.fit({
|
||||
animation: {
|
||||
duration: 1000,
|
||||
easingFunction: 'easeInOutQuad'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 新增: 扩展节点的函数
|
||||
async function expandNode(nodeId) {
|
||||
console.log(`正在扩展节点: ${nodeId}`);
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/visualizer/api/nodes/${nodeId}/expand`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
console.log(`收到 ${result.data.nodes.length} 个新节点, ${result.data.edges.length} 条新边`);
|
||||
updateGraph(result.data);
|
||||
} else {
|
||||
alert(`扩展节点失败: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('扩展节点失败:', error);
|
||||
alert('扩展节点失败: ' + error.message);
|
||||
} finally {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 导出图形数据
|
||||
function exportGraph() {
|
||||
|
||||
Reference in New Issue
Block a user