215 lines
6.8 KiB
JavaScript
Raw Normal View History

// js/graph.js
function openGraphModal(label) {
const modal = document.getElementById("graph-modal");
const graphTitle = document.getElementById("graph-title");
if (!modal || !graphTitle) {
console.error("Key element not found");
return;
}
graphTitle.textContent = `Knowledge Graph - ${label}`;
modal.style.display = "flex";
renderGraph(label);
}
function closeGraphModal() {
const modal = document.getElementById("graph-modal");
modal.style.display = "none";
clearGraph();
}
function clearGraph() {
const svg = document.getElementById("graph-svg");
svg.innerHTML = "";
}
async function getGraph(label) {
try {
const response = await fetch(`/graphs?label=${label}`);
const rawData = await response.json();
console.log({data: JSON.parse(JSON.stringify(rawData))});
const nodes = rawData.nodes
nodes.forEach(node => {
node.id = Date.now().toString(36) + Math.random().toString(36).substring(2); // 使用 crypto.randomUUID() 生成唯一 UUID
});
// 严格验证边数据 Strictly verify edge data
const edges = (rawData.edges || []).map(edge => {
const sourceNode = nodes.find(n => n.labels.includes(edge.source));
const targetNode = nodes.find(n => n.labels.includes(edge.target)
)
;
if (!sourceNode || !targetNode) {
console.warn("NOT VALID EDGE:", edge);
return null;
}
return {
source: sourceNode,
target: targetNode,
type: edge.type || ""
};
}).filter(edge => edge !== null);
return {nodes, edges};
} catch (error) {
console.error("Loading graph failed:", error);
return {nodes: [], edges: []};
}
}
async function renderGraph(label) {
const data = await getGraph(label);
if (!data.nodes || data.nodes.length === 0) {
d3.select("#graph-svg")
.html(`<text x="50%" y="50%" text-anchor="middle">No valid nodes</text>`);
return;
}
//
const svg = d3.select("#graph-svg");
const width = svg.node().clientWidth;
const height = svg.node().clientHeight;
//
svg.selectAll("*").remove();
// 创建力导向图布局 Create a force oriented diagram layout
const simulation = d3.forceSimulation(data.nodes)
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width / 2, height / 2));
// 添加连线(如果存在有效边) Add a connection (if there are valid edges)
if (data.edges.length > 0) {
simulation.force("link",
d3.forceLink(data.edges)
.id(d => d.id)
.distance(100)
);
}
// 绘制节点 Draw nodes
const nodes = svg.selectAll(".node")
.data(data.nodes)
.enter()
.append("circle")
.attr("class", "node")
.attr("r", 10)
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
);
svg.append("defs")
.append("marker")
.attr("id", "arrow-out")
.attr("viewBox", "0 0 10 10")
.attr("refX", 8)
.attr("refY", 5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,0 L10,5 L0,10 Z")
.attr("fill", "#999");
// 绘制边(带箭头) Draw edges (with arrows)
const links = svg.selectAll(".link")
.data(data.edges)
.enter()
.append("line")
.attr("class", "link")
.attr("marker-end", "url(#arrow-out)"); // 始终在 target 侧绘制箭头 Always draw arrows on the target side
// 边样式配置 Edge style configuration
links
.attr("stroke", "#999")
.attr("stroke-width", 2)
.attr("stroke-opacity", 0.8);
// 绘制标签(带背景框) Draw label (with background box)
const labels = svg.selectAll(".label")
.data(data.nodes)
.enter()
.append("text")
.attr("class", "label")
.text(d => d.labels[0] || "")
.attr("text-anchor", "start")
.attr("dy", "0.3em")
.attr("fill", "#333");
// 更新位置 Update Location
simulation.on("tick", () => {
links
.attr("x1", d => {
// 计算源节点到目标节点的方向向量 Calculate the direction vector from the source node to the target node
const dx = d.target.x - d.source.x;
const dy = d.target.y - d.source.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance === 0) return d.source.x; // 避免除以零 Avoid dividing by zero
// 根据半径 10 调整起点坐标源节点边缘Adjust the starting point coordinates (source node edge) based on radius 10
return d.source.x + (dx / distance) * 10;
})
.attr("y1", d => {
const dx = d.target.x - d.source.x;
const dy = d.target.y - d.source.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance === 0) return d.source.y;
return d.source.y + (dy / distance) * 10;
})
.attr("x2", d => {
// 根据半径 10 调整终点坐标(目标节点边缘) Adjust the endpoint coordinates (target node edge) based on a radius of 10
const dx = d.target.x - d.source.x;
const dy = d.target.y - d.source.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance === 0) return d.target.x;
return d.target.x - (dx / distance) * 10;
})
.attr("y2", d => {
const dx = d.target.x - d.source.x;
const dy = d.target.y - d.source.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance === 0) return d.target.y;
return d.target.y - (dy / distance) * 10;
});
// 更新节点和标签的位置(保持不变) Update the position of nodes and labels (keep unchanged)
nodes
.attr("cx", d => d.x)
.attr("cy", d => d.y);
labels
.attr("x", d => d.x + 12)
.attr("y", d => d.y + 4);
});
// 拖拽逻辑 Drag and drop logic
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;
simulation.alpha(0.3).restart();
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}