大厂技术  坚持周更  精选好文

简介

D3.js[1] 是一个基于 web 标准的 JS 可视化库,它借助 SVG、Canvas 和 HTML 进行数据可视化。在数据可视化中,我们很多时候会使用图来表达数据中所蕴含的信息,图方便让我们清晰的理清各个节点之间的联系,快速提取有用信息。而图布局算法可以使散乱的信息 (信息多以点线的关系承载) 通过一种清晰的方式呈现出来,符合相应的美学标准。

不同的图布局[2]也有不同的应用场景,例如树形布局 / Dagre布局,它是一个有向无环图,具有的拓扑性质,可以用作流程表达的场景。

同心圆布局,图分析的场景中通常是将节点先按照度数排序,度数最大的节点会排列在最中心的位置,越往外度数越小,整体成同心圆形式排列。

力导布局,通过节点之间的互斥力,减少重叠,但是在有限的空间内避免所有节点不重叠目前还是无解的。我们今天讲一下,简单的力导布局的实现。建立在物理理论基础上,将节点模拟为原子,通过原子之间作用力的相互影响,最终达到平衡状态,这就是力导向图[3]

思路

整体可以拆解为 2 个实体和 1 个作用因子:节点、线、力。d3-force的实现与传统的 FR 算法思路一样,可以分成三个部分组成,先计算节点间的互斥力,再计算连接点的吸引力,得出最终的作用力,得到每个节点的速度。使用模拟退火的衰减方案,达到稳定。

https://jsfiddle.net/smallstars/h8y6e031/51/

d3-force原理

节点处理

初始化导入节点

将节点导入 d3 中,需要对节点进行预处理,按照一定的半径和角度进行环绕。

// d3-force/simulation.js
var initialRadius = 10,initialAngle = Math.PI * (3 - Math.sqrt(5));function initializeNodes() {for (var i = 0, n = nodes.length, node; i < n; ++i) {node = nodes[i], node.index = i;if (node.fx != null) node.x = node.fx;if (node.fy != null) node.y = node.fy;// 初始位置if (isNaN(node.x) || isNaN(node.y)) {var radius = initialRadius * Math.sqrt(0.5 + i), angle = i * initialAngle;node.x = radius * Math.cos(angle);node.y = radius * Math.sin(angle);}// vx, vy为 x, y 的速度if (isNaN(node.vx) || isNaN(node.vy)) {node.vx = node.vy = 0;}}
}

建立节点四叉树

四叉树(quad-tree)

这里采取四叉树的结构原因是方便做碰撞检测, 四叉树是从根节点开始,每一个节点下面最多有四个子树的数据结构。通常我们把一部分二维空间细分为四象限,每一个节点存储相应的象限信息。

遍历导入所有节点数据,求出 最小值 ,最大值 , 最小值 ,最大值 ,将 与 进行 增幅,满足 ,。2的倍数满足四叉树在一维上对半分的特征。我们在添加节点的时候,总共会有下面 4 种情况:

  1. 四叉树为空,节点添加为根节点。

  2. 当前查询节点为索引节点且添加处范围矩阵为空,直接添加。

  3. 当前查询节点为真实节点且添加节点坐标相等且无数组索引,建立数组索引,再次划分该矩阵,直到查询节点与添加节点处于不同矩阵。

  4. 当前查询节点为真实节点且添加节点坐标相等且有数组索引,直接下挂。

// d3-force/collide.js
for (var k = 0; k < iterations; ++k) {// 调用 d3-quadtree 进行addtree = quadtree(nodes, x, y).visitAfter(prepare);for (i = 0; i < n; ++i) {node = nodes[i];ri = radii[node.index], ri2 = ri * ri;xi = node.x + node.vx;yi = node.y + node.vy;tree.visit(apply);}
}// d3-quadtree/add.js
function add(tree, x, y, d) {if (isNaN(x) || isNaN(y)) return tree; // ignore invalid pointsvar parent,node = tree._root,leaf = {data: d},// 象限坐标x0 = tree._x0,y0 = tree._y0,x1 = tree._x1,y1 = tree._y1,xm,ym,xp,yp,right,bottom,i,j;// case1: If the tree is empty, initialize the root as a leaf.if (!node) return tree._root = leaf, tree;// 类似二分,自上向下搜索// Find the existing leaf for the new point, or add it.while (node.length) {if (right = x >= (xm = (x0 + x1) / 2)) x0 = xm; else x1 = xm;if (bottom = y >= (ym = (y0 + y1) / 2)) y0 = ym; else y1 = ym;// case2: 判断当前添加节点所在象限是否为空if (parent = node, !(node = node[i = bottom << 1 | right])) return parent[i] = leaf, tree;}// case4: 添加节点是否与父节点重合// Is the new point is exactly coincident with the existing point?xp = +tree._x.call(null, node.data);yp = +tree._y.call(null, node.data);if (x === xp && y === yp) return leaf.next = node, parent ? parent[i] = leaf : tree._root = leaf, tree;// case3: 不停分割,直到处于不同象限// Otherwise, split the leaf node until the old and new point are separated.do {parent = parent ? parent[i] = new Array(4) : tree._root = new Array(4);if (right = x >= (xm = (x0 + x1) / 2)) x0 = xm; else x1 = xm;if (bottom = y >= (ym = (y0 + y1) / 2)) y0 = ym; else y1 = ym;} while ((i = bottom << 1 | right) === (j = (yp >= ym) << 1 | (xp >= xm)));return parent[j] = node, parent[i] = leaf, tree;
}

斥力的优化求解

节点间的斥力优化关键为电荷斥力[4]求解优化,最基本的一个节点所受的力,需要与其他所有节点进行计算求和,复杂度为 。

整合受力

而四叉树结构与 Barnes-Hut[5] 近似,复杂度可降为 。当前节点所受周围点的斥力进行整合处理,大小由 Barnes-Hut 近似精度 (默认值为 ) 决定,最后根据 Velocity Verlet[6] 对速度进行求解。

象限面积 ,节点(node)与象限点(quad)形成的面积

// d3-force/manyBody.jsvar distanceMin2 = 1,distanceMax2 = Infinity,theta2 = 0.81;function apply(quad, x1, _, x2) {if (!quad.value) return true;var x = quad.x - node.x,y = quad.y - node.y,w = x2 - x1,l = x * x + y * y;// Barnes-Hut成立// 如何点非常近,冲突的时候随机方向if (w * w / theta2 < l) {if (l < distanceMax2) {if (x === 0) x = jiggle(random), l += x * x;if (y === 0) y = jiggle(random), l += y * y;if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);node.vx += x * quad.value * alpha / l;node.vy += y * quad.value * alpha / l;}return true;}// Barnes-Hut不成立且 quad 有节点else if (quad.length || l >= distanceMax2) return;// 排除自身对自身影响,还可以继续向下遍历if (quad.data !== node || quad.next) {if (x === 0) x = jiggle(random), l += x * x;if (y === 0) y = jiggle(random), l += y * y;if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);}do if (quad.data !== node) {w = strengths[quad.data.index] * alpha / l;node.vx += x * w;node.vy += y * w;} while (quad = quad.next);}

节点连线的处理

先初始化连线,计算节点的度和每一条边对起始节点度(source/target degree)的占比,使 ,alpha为阻尼系数,默认边长度(distance)为30,默认弹簧劲度(strength)为 ,减少度大节点的引力,提高稳定性。计算连线两边的引力,最终推导出速度的变化。

同理可得出、、

// d3-force/link.js
function force(alpha) {for (var k = 0, n = links.length; k < iterations; ++k) {for (var i = 0, link, source, target, x, y, l, b; i < n; ++i) {link = links[i], source = link.source, target = link.target;x = target.x + target.vx - source.x - source.vx || jiggle(random);y = target.y + target.vy - source.y - source.vy || jiggle(random);l = Math.sqrt(x * x + y * y);l = (l - distances[i]) / l * alpha * strengths[i];x *= l, y *= l;target.vx -= x * (b = bias[i]);target.vy -= y * b;source.vx += x * (b = 1 - b);source.vy += y * b;}}
}

布局的形成

通过不断的迭代运算,每次运算都可以看做一步,通过模拟退火的衰减方案最后达到稳定状态。

// d3-force/simulation.jsvar simulation,alpha = 1,alphaMin = 0.001,// alpha衰减率alphaDecay = 1 - Math.pow(alphaMin, 1 / 300),alphaTarget = 0,// 速度衰减velocityDecay = 0.6,stepper = timer(step),// tick事件与end事件event = dispatch("tick", "end");function step() {tick();event.call("tick", simulation);if (alpha < alphaMin) {stepper.stop();event.call("end", simulation);}
}function tick() {// alpha不断衰减alpha += (alphaTarget - alpha) * alphaDecay;// 不停迭代forces.each(function(force) {force(alpha);});// 速度转化为距离改变for (i = 0; i < n; ++i) {node = nodes[i];if (node.fx == null) node.x += node.vx *= velocityDecay;// 具有fx,说明当前节点被控制,不需要受到力的影响,速度置为0else node.x = node.fx, node.vx = 0;if (node.fy == null) node.y += node.vy *= velocityDecay;else node.y = node.fy, node.vy = 0;}
}

实战示例

Demo:

React-d3js[7]

代码:

https://github.com/SmaIIstars/react-demo/blob/master/src/pages/d3-force/index.tsx

遇见的问题

  1. Svg 中绘制复杂内容

可以使用 foreignObject 节点进行绘制,在 foreignObject 中可以编写 XML 命名空间的元素。

  1. 节点迭代次数过多,导致页面卡顿

通常初次生成图布局的时候,需要一个过渡动画,在模拟退火的过程中,节点、线会不断的移动。监听数据的变化,使用 React.memo 和 useCallback 减少不必要的运算。

  1. 节点间重叠,相互遮盖

增大排斥力,增常线段长度,增加碰撞体积。但是在有限的空间内,仍然无法完全避免重叠的问题。

  1. 节点过多,超出画布

使用设备的宽高作为画布,对内容进行缩放和拖拽,让用户可以查看到所有内容。

参考

  1. GitHub - d3/d3-force: Force-directed graph layout using velocity Verlet integration.[8]

  2. D3.js(Data-Driver Documents)力导向图理论知识[9]

参考资料

[1]

D3.js: https://github.com/d3/d3

[2]

图布局: https://segmentfault.com/a/1190000039054038

[3]

力导向图: https://observablehq.com/@sandravizmad/force-directed-layout

[4]

电荷斥力: https://bytedance.feishu.cn/docs/doccnyBnKUxyMSdw74Tws6lavfd#8M5j10

[5]

Barnes-Hut: https://bytedance.feishu.cn/docs/doccnyBnKUxyMSdw74Tws6lavfd#yeVWli

[6]

Velocity Verlet: https://bytedance.feishu.cn/docs/doccnyBnKUxyMSdw74Tws6lavfd#lfo9UK

[7]

React-d3js: http://demo.smallstars.top/demo/d3-force

[8]

GitHub - d3/d3-force: Force-directed graph layout using velocity Verlet integration.: https://github.com/d3/d3-force

[9]

D3.js(Data-Driver Documents)力导向图理论知识: https://bytedance.feishu.cn/docs/doccnyBnKUxyMSdw74Tws6lavfd#WmTLGZ

最后

如果想学习更多H5游戏webpacknodegulpcss3javascriptnodeJScanvas数据可视化等前端知识和实战,欢迎在《趣谈前端》加入我们的技术群一起学习讨论,共同探索前端的边界。

从零搭建全栈可视化大屏制作平台V6.Dooring

从零设计可视化大屏搭建引擎

Dooring可视化搭建平台数据源设计剖析

可视化搭建的一些思考和实践

基于Koa + React + TS从零开发全栈文档编辑器(进阶实战

点个在看你最好看

深入理解 D3.js 可视化库之力导向图原理与实现相关推荐

  1. D3.js可视化库入门视频教程

    2019独角兽企业重金招聘Python工程师标准>>> D3 是最流行的可视化库之一,它被很多其他的表格插件所使用.它允许绑定任意数据到DOM,然后将数据驱动转换应用到Documen ...

  2. 利用D3.js快速绘制力导向图

    碎碎念: 最近课题需要基于图论和力导向图可视化每个脑电通道之间的连接性,MATLAB画的效果差强人意,于是上网搜寻到JavaScript 的一个函数库--D3.js,想快速得到数据的可视化结果 简介D ...

  3. 图形化开发(一)——D3.js的基本介绍、技术原理

    图形化开发(一)--D3.js的基本介绍.技术原理 D3.js 为什么学习D3 中文官网--https://www.d3js.org.cn/ github--https://github.com/d3 ...

  4. Vue+D3 绘制条形图和力导向图

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 Vue+D3 绘制条形图和力导向图 准备:D3代码 1.条形图 2.力导向图 本篇blog在于把已经编写好的D3代码从一般HTML文件 ...

  5. D3 笔记十:力导向图

    本文目前来说,是学完极客学院的<D3.js 入门教程>之后的整理出来的精简知识版,仅仅是为了知识整理.后期我会随着学习的深入,在这个基础上,进行维护与更改. 一.力导向图 我们先来了解一下 ...

  6. D3 v3版本绘制力导向图更新节点时JS控制台报错 :Error: circle attribute cx: Expected length, NaN解决办法

    D3的v3版本,很古老了. 拿来绘制力导向图,画布初始化的代码如下: //代码不完整,仅作演示以说明问题 //节点数组var nodes = [ { name: "sssss" , ...

  7. python绘制不带颜色曲线图_Python数据可视化库-Matplotlib——折线图,子图绘制

    # coding:utf-8 import pandas as pd import numpy as np from matplotlib import pylab as plt # 导入数据可视化库 ...

  8. d3.js 教程 模仿echarts折线图

    今天我们来仿echarts折线图,这个图在echarts是折线图堆叠,但是我用d3改造成了普通的折线图,只为了大家学习(其实在简单的写一个布局就可以).废话不多说商行代码. 1 制作 Line 类 c ...

  9. 深入理解Three.js(WebGL)贴图(纹理映射)和UV映射

    本文将详细描述如何使用Three.js给3D对象添加贴图(Texture Map,也译作纹理映射,"贴图"的翻译要更直观,而"纹理映射"更准确.).为了能够查看 ...

最新文章

  1. 有向图的强连通分量——Tarjan
  2. 观点 | 有区块链就一定能创新吗?区块链改变了什么、改变不了什么?
  3. Node版本管理nvm, npm
  4. synchronized的理解
  5. Robot Framework操作mongodb数据库
  6. 云科技网络验证源码_面向虚拟化架构和容器云的开源安全工具
  7. 日本研发高精度诊疗感应器,或颠覆疾病诊疗
  8. CAS单点登出实现案例
  9. 185.部门工资前三高的员工
  10. 大电流场效应管500a_不可不知的电子工程常用的6大电子元器件,了解一下!
  11. VS中卸载当前的EF版本命令
  12. {windows}之向 暴风一号(病毒) 学习一个注册表小知识
  13. c语言表白用代码(1)
  14. xp计算机u盘重装系统,如何用u盘装xp系统_纯净版xp系统_xp系统安装教程 - u大师...
  15. adb 查看屏幕大小_Android adb获取屏幕分辨率
  16. 查看页面滚动条滚动距离,可视区窗口尺寸
  17. 单独使用bable插件
  18. python pyecharts生成图表
  19. 《Python基础知识-4判断和循环语句》
  20. 基于ubuntu系统的HEVC视频编码与解码

热门文章

  1. 华为交换机——光电复用口如何切换光口模式
  2. 费洪荒之力解决部署Java项目遇到的Hibernate奇葩状
  3. 牛客网刷题-概率统计
  4. 青年教师大讲堂 计算机,课堂展风采交流促提升 ───数学与统计学院开展青年教师教学能力提升大讲堂活动...
  5. 《zhaoolee的宝藏周刊》第2期: 用星际远航拯救生育率问题
  6. 想做游戏3D建模师,该如何学习,学习哪些方面?
  7. 稀疏结构模型——套索模型、组套索、重叠组套索模型
  8. MATLAB plot函数
  9. C#反射类读取配置表 PropertyInfo
  10. 送给高三毕业学生:如何选专业