D3 force(力导向图)研究之二:如何布局超过十万个节点的图谱

基于SVG利用力导向图对节点进行布局,容易导致布局失败的原因有两个:
1. 计算节点位置耗费大量的CPU,导致页面操作冻结;
2. 添加DOM节点到SVG元素时,渲染、重绘会耗费大量的GPU,导致页面直接崩溃;

针对以上的情况,可以采取如下策略:
1. 布局过程中,在页面只显示节点;
2. 布局过程中,在页面同时显示节点与连线;
3. 布局过程中不显示节点与连线,布局完成后显示;
4. 将布局算法放入Worker进行计算,同时显示节点与连线;

表一是各种策略的测试结果,每次布局都添加同样数量节点与连线,见下表。

表一:布局策略的完成时间(单位:毫秒)

节点数量\显示方式只显示点显示点与线布局完成后显示Worker
1000113011785463098313
200017854208866243-
300032831512628249-
400043536691351030711395
100001165121810923696648890

从上表可以看出,达到10000个节点与连线后,计算时间最短也需要37秒,基本上处于不可用状态。

在此过程中,还有如下一条数据,布局一万个节点至少需要18278 X 18336的面积,也就是说,至少需要40块1920 X 1080的显示屏,才能将图片完全显示,平均每个节点占据的面积为33514平方像素,约为183*183的大小,当然,这主要是由节点之间的连接关系决定的。

继续加大数据量进行测试,主要以Worker的方式:

节点数量\显示方式Worker布局完成后显示布局时间渲染时间画布最小尺寸SVG文件大小单位面积平方像素
100000771787765841594658118 * 5817721.2m33776=183*183
200000164361416306991291582185*8214142.5m33753=183*183
300000363951883637145323735100708*10070363.7m33805=183*183

200000个节点布局
300000个节点布局的示例如下:
300000个节点布局

示例代码

Worker计算的核心代码如下:

importScripts('d3.v4.min.js')
var sim = d3.forceSimulation();
//  每次数据发生变更时,通知布局进行DOM修正    
var tickHandler = function() {
    postMessage({
        nodes,
        links
    })
}
sim.on('tick.force', tickHandler)

计算画布尺寸的代码如下:

//  计算左上角的点与右下角的点
var max = {x : 0, y : 0},
    min = {x : 0, y : 0}
//  遍历所有的节点
nodes.forEach(function(item) {
    if(item.x > max.x) {
        max.x = item.x
    }
    if(item.y > max.y) {
        max.y = item.y
    }
    if(item.x < min.x) {
        min.x = item.x
    }
    if(item.y < min.y) {
        min.y = item.y
    }
})
//  改进SVG的视图区域
var width = max.x - min.x,
    height = max.y - min.y;
//  设置大小
svg.attr('width', width)
svg.attr('height', height)
//  设置可见区域
svg.attr('viewBox', min.x + ',' + min.y + ',' + width + ',' + height)

策略改进

通过上面的数据我们可以得出这样的结论:
1. 无论多大的显示屏都不可能把图谱显示完全;
2. 浏览器渲染节点容易导致浏览器崩溃;

于是我们也可以做出如下的改进策略:
1. 将布局结果保存到服务器;
2. 只显示可视区域之内的节点;
3. 通过可是区域的移动实现DOM节点的销毁;
4. 将SVG方式改为canvas方式,减少节点的消耗;

以Express为服务器,将布局方式放到服务器端,当节点数据生变化时,自动重排,并将结果保存到数据库,然后可视窗口滑动到哪里,就显示哪里的节点数据。

其他策略

  1. 按照连接关系对业务数据进行分片,例如分层、聚集等方法,然后依次布局;
  2. 采用多Worker方式;
  3. 采用静态布局方式,改用手动控制tick方法;
  4. 优化模拟器参数,尤其是alphaDecay、velocityDecay等核心参数。

总结

布局超大型的拓扑图或者图谱,需要多种策略的融合,无论是算法层面还是展现层面,都要经历长期的调试与优化,很难做到一蹴而就,没有最优的策略,但有最适合当前业务与环境的策略。

  • 8
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
 你手头有一些数据,想做成漂亮的表放到网站上?好主意,通过浏览器来跨平台实现数据可视化是正确的选择。什么,你还想让表能够响应用户操作?没问题,交互式表比静态片更能吸引人去探究本源。好啦,要生成通过浏览器展示的动态表,目前热门的Web数据可视化库——D3。   《灵程序设计丛书·数据可视化实战:使用D3设计交互式表》这本书很有意思,而且对读者要求不高。不需要知道什么是数据可视化,也不用有太多Web开发背景就能看懂它。不信?翻一翻就知道这是一本既好玩又实用的动手指南啦!看完这本书你会怎么样呢?   掌握必要的HTML、CSS、JavaScript和SVG基础知识;   学会基于数据在网页里生成元素和为它们设置样式的技巧;   能够生成条形、散点、饼、堆叠条形导向;   使用平滑的过渡动画来展示数据的变化;   赋予表动态交互能,响应用户从不同角度探索数据的请求;   收集数据和创建自定义的地;   另外,《灵程序设计丛书·数据可视化实战:使用D3设计交互式表》100多个代码示例都可以在线浏览! 【电子版来自互联网,仅供预览及学习交流 使用,不可用于商业用途,如有版权问题,请联系删除,支持正版,喜欢的 请购买正版书籍: https://e.jd.com/30336473.html】
以下是一个简单的D3.js导向示例,演示如何添加新节点和关系连线: ```html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>D3.js导向 - 新增节点和关系连线示例</title> <script src="https://d3js.org/d3.v6.min.js"></script> </head> <body> <svg width="800" height="600"></svg> <script> // 定义节点和关系数据 var nodes = [ { id: "node1", name: "节点1" }, { id: "node2", name: "节点2" }, { id: "node3", name: "节点3" }, { id: "node4", name: "节点4" } ]; var links = [ { source: "node1", target: "node2" }, { source: "node1", target: "node3" }, { source: "node2", target: "node4" } ]; // 创建导向对象 var simulation = d3.forceSimulation(nodes) .force("link", d3.forceLink(links).id(d => d.id)) .force("charge", d3.forceManyBody()) .force("center", d3.forceCenter(400, 300)); // 创建节点和关系连线 var svg = d3.select("svg"); var link = svg.selectAll("line") .data(links) .enter().append("line") .attr("stroke", "#999") .attr("stroke-opacity", 0.6) .attr("stroke-width", d => Math.sqrt(d.value)); var node = svg.selectAll("circle") .data(nodes) .enter().append("circle") .attr("r", 10) .attr("fill", "#ccc") .call(drag(simulation)); var label = svg.selectAll("text") .data(nodes) .enter().append("text") .text(d => d.name) .attr("font-size", "12px") .attr("dx", 15) .attr("dy", 4); // 定义拖拽行为 function drag(simulation) { 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; } return d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended); } // 新增节点和关系连线 var newNode = { id: "node5", name: "节点5" }; var newLink = { source: "node4", target: "node5" }; nodes.push(newNode); links.push(newLink); node = node.data(nodes, d => d.id); node.exit().remove(); node = node.enter().append("circle") .attr("r", 10) .attr("fill", "#ccc") .call(drag(simulation)) .merge(node); label = label.data(nodes, d => d.id); label.exit().remove(); label = label.enter().append("text") .text(d => d.name) .attr("font-size", "12px") .attr("dx", 15) .attr("dy", 4) .merge(label); link = link.data(links); link.exit().remove(); link = link.enter().append("line") .attr("stroke", "#999") .attr("stroke-opacity", 0.6) .attr("stroke-width", d => Math.sqrt(d.value)) .merge(link); // 更新导向 simulation.nodes(nodes); simulation.force("link").links(links); simulation.alpha(1).restart(); </script> </body> </html> ``` 在上面的示例中,我们首先定义了一个简单的节点和关系数据,并创建了一个导向对象。然后,我们使用D3.js创建了节点和关系连线的SVG元素,并绑定了数据。 接下来,我们定义了一个拖拽行为,以便用户可以拖动节点。然后,我们添加了一个新节点和一个新的关系连线,并使用D3.js更新了节点和关系连线的SVG元素。 最后,我们更新了导向对象,并重新启动了导向的模拟,以确保新节点和关系连线被正确地添加到导向中。 请注意,这只是一个简单的示例,实际应用中需要根据具体需求进行更复杂的处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值