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

发表于 2年以前  | 总阅读数:436 次

简介

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 进行add
  tree = 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 points

  var 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.js
  var 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.js
 var 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,说明当前节点被控制,不需要受到力的影响,速度置为0
    else 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 命名空间的元素。

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

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

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

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

4 . 节点过多,超出画布

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

参考

  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

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/yLSaWn32T7FQ66fiDNJfBg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录