60 d3.js 不能正确展示节点连线, 以及一个基础的demo
前言
这个问题是来自于 一位朋友的需求
主要就是使用 d3.js 来渲染 neo4j 的响应的数据, 做一个可视化的展示
但是 碰到了一些问题, 比如 需要再连线之间增加 关系的标注
另外就是 初始化 发过来的代码, 实际上 展示是存在问题的, 这里的主题 就是这几个部分

d3.js demo 中 节点连线不能正确展示
demo 代码如下
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>查询景点并可视化</title><style>#graph {width: 800px;height: 600px;margin: 20px auto;border: 1px solid #ccc;}</style>
</head>
<body>
<h1 style="color:black;font-size:32px;margin-bottom:0px;text-align:center;margin-left:40px;">西双版纳旅游知识</h1>
<input type="text" id="query-input" placeholder="请输入您想查询的知识">
<button onclick="search()">查询</button>
<div id="graph"></div><script src="js/d3.v4.min.js"></script>
<script>function search() {const queryInput = document.getElementById('query-input');const query = queryInput.value;fetch('data/records5.json').then(response => response.json()).then(data => {renderGraph(data, query);}).catch(error => {console.error('Error fetching data:', error);resultContainer.innerHTML = '查询出错,请检查网络连接或数据格式';});}function renderGraph(data, query) {const graphContainer = document.getElementById('graph');graphContainer.innerHTML = '';const svg = d3.select("#graph").append("svg").attr("width", 800).attr("height", 600).style("display", "block").style("margin", "auto");const width = 800; // SVG 容器的宽度const height = 600; // SVG 容器的高度const filteredData = data.filter(item => {const startNodeName = item.p.start.properties.name;const endNodeName = item.p.end.properties.name;return true;});const nodes = {};const links = [];filteredData.forEach(item => {const startNodeName = item.p.start.properties.name;const endNodeName = item.p.end.properties.name;const relationshipType = item.p.segments[0].relationship.type;const relationshipDescription = item.p.segments[0].relationship.properties.description;if (!nodes[startNodeName]) {nodes[startNodeName] = { id: startNodeName, name: startNodeName, description: startNodeName }; // 将节点名称作为描述}if (!nodes[endNodeName]) {nodes[endNodeName] = { id: endNodeName, name: endNodeName, description: endNodeName }; // 将节点名称作为描述}links.push({source: nodes[startNodeName],target: nodes[endNodeName],type: relationshipType,description: relationshipDescription});});const simulation = d3.forceSimulation(Object.values(nodes)).force("link", d3.forceLink(links).id(d => d.id)).force("charge", d3.forceManyBody().strength(-200)) // 添加电荷力,使节点相互排斥.force("center", d3.forceCenter(width / 2, height / 2)) // 添加中心引力,吸引所有节点到中心.force("collision", d3.forceCollide(18)); // 添加碰撞检测力,确保节点之间保持一定的距离// 绘制节点const node = svg.selectAll("circle").data(Object.values(nodes)).enter().append("circle").attr("r", 10).attr("cx", width/2) // 设置x坐标为中心.attr("cy", height/2) // 设置y坐标为中心.attr("fill", "#964c5d").append("title") // 添加 title 元素以显示节点描述.text(d => d.description);const link = svg.selectAll("line").data(links).enter().append("line").attr("stroke", "#964c5d").attr("stroke-opacity", 0.6).attr("stroke-width", 1).append("title") // 添加 title 元素以显示关系描述.text(d => d.description);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);});}
</script>
</body>
</html>
然后 record.json 的测试数据如下, 可以直接使用
[{"p": {"start": {"identity": 7823,"labels": ["景点名称"],"properties": {"name": "曼听公园"},"elementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7823"},"end": {"identity": 7972,"labels": ["景点属性值"],"properties": {"name": "4A"},"elementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7972"},"segments": [{"start": {"identity": 7823,"labels": ["景点名称"],"properties": {"name": "曼听公园"},"elementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7823"},"relationship": {"identity": 0,"start": 7823,"end": 7972,"type": "景点关系","properties": {"景点关系": "星级"},"elementId": "5:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:0","startNodeElementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7823","endNodeElementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7972"},"end": {"identity": 7972,"labels": ["景点属性值"],"properties": {"name": "4A"},"elementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7972"}}],"length": 1.0}},{"p": {"start": {"identity": 7823,"labels": ["景点名称"],"properties": {"name": "曼听公园"},"elementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7823"},"end": {"identity": 7973,"labels": ["景点属性值"],"properties": {"name": "4.5"},"elementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7973"},"segments": [{"start": {"identity": 7823,"labels": ["景点名称"],"properties": {"name": "曼听公园"},"elementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7823"},"relationship": {"identity": 1,"start": 7823,"end": 7973,"type": "景点关系","properties": {"景点关系": "评分"},"elementId": "5:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:1","startNodeElementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7823","endNodeElementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7973"},"end": {"identity": 7973,"labels": ["景点属性值"],"properties": {"name": "4.5"},"elementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7973"}}],"length": 1.0}},{"p": {"start": {"identity": 7823,"labels": ["景点名称"],"properties": {"name": "曼听公园"},"elementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7823"},"end": {"identity": 7974,"labels": ["景点属性值"],"properties": {"name": "云南省西双版纳傣族自治州景洪市曼听路35号"},"elementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7974"},"segments": [{"start": {"identity": 7823,"labels": ["景点名称"],"properties": {"name": "曼听公园"},"elementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7823"},"relationship": {"identity": 2,"start": 7823,"end": 7974,"type": "景点关系","properties": {"景点关系": "地址"},"elementId": "5:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:2","startNodeElementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7823","endNodeElementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7974"},"end": {"identity": 7974,"labels": ["景点属性值"],"properties": {"name": "云南省西双版纳傣族自治州景洪市曼听路35号"},"elementId": "4:fea7ca27-49b0-48cd-b5eb-17bbeb53c807:7974"}}],"length": 1.0}}
]然后 页面展示如下, 可以看到 页面绘制是存在问题的, 四个点的坐标 一样
然后 也就没有了 点和点之间的连线, 然后 出现的现象就是这样

d3.js demo 中 节点连线展示问题解决
注释掉这里两个 append("title"), 然后 就可以正常展示了

展示结果如下

一个完整的 d3.js demo
这个demo包含了基础的 点线的展示
节点的拖动, 关联线的标注信息 等等
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>查询景点并可视化</title><style>#graph {width: 800px;height: 600px;margin: 20px auto;border: 1px solid #ccc;}.link-annotation {font-size: 10px;dominant-baseline: middle;text-anchor: middle;}</style>
</head>
<body>
<h1 style="color:black;font-size:32px;margin-bottom:0px;text-align:center;margin-left:40px;">西双版纳旅游知识</h1>
<input type="text" id="query-input" placeholder="请输入您想查询的知识">
<button onclick="search()">查询</button>
<div id="graph"></div><script src="js/d3.v4.min.js"></script>
<script>let simulation = null;function search() {const queryInput = document.getElementById('query-input');const query = queryInput.value;fetch('data/records7.json').then(response => response.json()).then(data => {renderGraph(data, query);}).catch(error => {console.error('Error fetching data:', error);resultContainer.innerHTML = '查询出错,请检查网络连接或数据格式';});}function renderGraph(data, query) {const graphContainer = document.getElementById('graph');graphContainer.innerHTML = '';const svg = d3.select(graphContainer).append("svg").attr("width", 800).attr("height", 600).style("display", "block").style("margin", "auto");const width = 800; // SVG 容器的宽度const height = 600; // SVG 容器的高度const filteredData = data.filter(item => {const startNodeName = item.p.start.properties.name;const endNodeName = item.p.end.properties.name;return true;});const nodes = {};const links = [];filteredData.forEach(item => {const startNodeName = item.p.start.properties.name;const endNodeName = item.p.end.properties.name;const relationshipType = item.p.segments[0].relationship.type;const relationshipDescription = item.p.segments[0].relationship.properties[item.p.segments[0].relationship.type];if (!nodes[startNodeName]) {nodes[startNodeName] = { id: startNodeName, name: startNodeName, description: startNodeName, type: 'start' };}if (!nodes[endNodeName]) {nodes[endNodeName] = { id: endNodeName, name: endNodeName, description: endNodeName, type: 'end' };}links.push({source: nodes[startNodeName],target: nodes[endNodeName],type: relationshipType,description: relationshipDescription});});simulation = d3.forceSimulation(Object.values(nodes)).force("link", d3.forceLink(links).id(d => d.id)).force("charge", d3.forceManyBody().strength(-400)).force("center", d3.forceCenter(width / 2, height / 2)).force("collision", d3.forceCollide(50));const node = svg.selectAll("circle").data(Object.values(nodes)).enter().append("circle").attr("r", 20).attr("fill", d => d.type === 'start' ? "blue" : "pink") // 根据节点类型设置颜色.text(d => d.description).attr("id", d => d.id).call(d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended));const nodeLabel = svg.selectAll("text").data(Object.values(nodes)).enter().append("text").attr("x", d => d.x).attr("y", d => d.y).attr("dy", "0.35em").attr("text-anchor", "middle").style("font-size", "12px").style("fill", "green").text(d => d.name);const link = svg.selectAll(".link").data(links).enter().append("line").attr("class", "link").attr("stroke", "blue").attr("stroke-opacity", 0.6).attr("stroke-width", 1);let annoEleList = null;function ticked() {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);nodeLabel.attr("x", d => d.x).attr("y", d => d.y);// 在 link 上面添加标注if(annoEleList) {for(let i in annoEleList) {annoEleList[i].remove()}}annoEleList = []links.forEach(function(linkData) {let annoEle = svg.append("text").attr("class", "link-annotation").text(linkData.description)annoEleList.push(annoEle)let midX = linkData.source.x + ((linkData.target.x - linkData.source.x) / 2)let midY = linkData.source.y + ((linkData.target.y - linkData.source.y) / 2)annoEle.attr("x", midX).attr("y", midY);});}simulation.on("tick", ticked);}// 拖动函数代码var dragging = false;// 开始拖动并更新相应的点function dragstarted(d) {if (!d3.event.active) simulation.alphaTarget(0.3).restart();d.fx = d.x;d.fy = d.y;dragging = true;}// 拖动进行中function dragged(d) {d.fx = d3.event.x;d.fy = d3.event.y;}// 拖动结束function dragended(d) {if (!d3.event.active) simulation.alphaTarget(0);d.fx = null;d.fy = null;dragging = false;}</script>
</body>
</html>
展示效果如下

完
