D3力导向图通过WebSocket实时更新报错:Uncaught TypeError: Cannot read properties of undefined (reading 'length')
首先,咱们先定位你遇到的核心问题:报错出现在simulation.nodes(graph.nodes)这一行,说明graph.nodes是undefined——这大概率是WebSocket数据解析的问题,其次你的代码没处理DOM元素的实时更新逻辑,即使数据解析对了,图表也不会正确刷新。
问题1:WebSocket数据解析错误
你在onmessage里写了:
graph = JSON.parse(e.data).message;
但你给出的WebSocket推送示例数据是完整的graph对象,没有外层的message字段!如果推送的原始数据就是{"nodes": [...], "links": [...]}这种结构,那JSON.parse(e.data).message就会返回undefined,导致graph变成undefined,自然graph.nodes也是undefined,调用simulation.nodes()时就会触发"读取undefined的length"错误——因为d3的力导向模拟需要数组,它会尝试读取数组的length属性,而undefined没有这个属性。
解决方法:
检查WebSocket推送的JSON结构,如果没有外层message字段,直接解析成graph对象:
comSocket.onmessage = function (e) { // 先解析数据,根据实际结构调整是否取message字段 const receivedData = JSON.parse(e.data); // 如果推送数据外层有message就用它,否则直接用receivedData const newGraph = receivedData.message || receivedData; console.log(newGraph); // 后续调用更新逻辑 updateGraph(newGraph); };
问题2:未处理DOM元素的实时更新
你的初始代码只处理了第一次渲染的enter()阶段(创建初始节点和连线),但收到新数据时,没有处理新增元素、更新现有元素、删除多余元素这三个关键步骤,即使simulation更新了,DOM也不会同步变化,甚至可能引发其他错误。
解决方法:
把图表更新逻辑封装成一个函数,处理d3的enter/update/exit生命周期:
// 先初始化全局的link和node变量 let link; let node; const svg = d3.select("svg"), width = +svg.attr("width"), height = +svg.attr("height"); const color = d3.scaleOrdinal(d3.schemeCategory20); const simulation = d3.forceSimulation() .force("link", d3.forceLink().id(function(d) { return d.id; })) .force("charge", d3.forceManyBody().strength(-2500)) .force("center", d3.forceCenter(width / 2, height / 2)); // 定义更新图表的函数 function updateGraph(newGraph) { // 1. 更新连线 link = svg.select(".links").selectAll("line").data(newGraph.links); // 删除多余的连线 link.exit().remove(); // 添加新的连线,并合并现有连线的属性 link = link.enter().append("line") .attr("stroke-width", function(d) { return Math.sqrt(d.value); }) .merge(link); // 2. 更新节点组(g元素,包含圆圈和文字) node = svg.select(".nodes").selectAll("g").data(newGraph.nodes); // 删除多余的节点组 node.exit().remove(); // 添加新的节点组,并合并现有节点组 node = node.enter().append("g") .merge(node); // 3. 更新节点圆圈 const circles = node.selectAll("circle").data(d => [d]); // 每个g对应一个circle数据 circles.exit().remove(); circles.enter().append("circle") .attr("r", 20) .merge(circles) .attr("fill", function(d) { return color(d.group); }); // 4. 更新节点文字标签 const labels = node.selectAll("text").data(d => [d]); labels.exit().remove(); labels.enter().append("text") .attr('x', 16) .attr('y', 13) .merge(labels) .text(function(d) { return d.id; }); // 5. 更新力导向模拟 simulation.nodes(newGraph.nodes); simulation.force("link").links(newGraph.links); // 重启模拟,让图表重新布局 simulation.alpha(1).restart(); } // 初始化渲染初始图表 const initialGraph = { "nodes": [ {"id": "Agent_1", "group": 1}, {"id": "Agent_2", "group": 2}, {"id": "Agent_3", "group": 1}, {"id": "Agent_4", "group": 3} ], "links": [] }; // 先创建初始的links和nodes容器 svg.append("g").attr("class", "links"); svg.append("g").attr("class", "nodes"); updateGraph(initialGraph); // WebSocket消息处理 const comSocket = new WebSocket( 'ws://' + window.location.host + '/ws/com/' ); comSocket.onmessage = function (e) { const receivedData = JSON.parse(e.data); const newGraph = receivedData.message || receivedData; console.log(newGraph); updateGraph(newGraph); }; // 保持原来的ticked函数 function ticked() { link .attr("x1", function(d) { return d.source.x; }) .attr("y1", function(d) { return d.source.y; }) .attr("x2", function(d) { return d.target.x; }) .attr("y2", function(d) { return d.target.y; }); node .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); } // 绑定ticked事件到模拟 simulation.on("tick", ticked);
关键说明
- 数据解析:一定要和WebSocket推送的结构匹配,如果你的Django后端推送的是包含
message字段的JSON(比如{"message": {"nodes": [...], ...}}),就保留receivedData.message,否则直接用receivedData; - DOM更新:通过
data()重新绑定数据,exit().remove()删除不再需要的元素,enter()创建新元素,merge()合并enter和update阶段,确保新老元素都能正确更新; - 模拟重启:调用
simulation.alpha(1).restart()可以重置模拟的冷却系数,让图表重新进行力导向布局,适配新的节点和连线。
这样修改后,你的图表应该就能正常接收WebSocket推送的数据并实时更新了。
内容的提问来源于stack exchange,提问作者shined day

