资讯专栏INFORMATION COLUMN

d3-force 力导图 源码解读与原理分析【一】

luckyyulin / 2550人阅读

摘要:设置力导图点阵中心碰撞其他引用模块构造常量函数微小晃动随机数四叉树模块设置力导图点阵中心此处代码使用的是单例对象模式,读者要注意,切勿与类对象理解混了。

首先先推荐一下某呆翻译的d3-force的中文文档:https://github.com/xswei/D3-V... 。
在我们解读源码前还请读者先熟悉一下force相关的API,以及es6语法 .

如有分析不当之处还请留言指出,谢谢~

那我们进入正题吧

D3-force 有什么

我们来看一下index.js 这个文件大家可以理解为force的对外统一出口。当然你也可以自定义使用这些模块。

// index.js
export {default as forceCenter} from "./src/center"; // 设置力导图点阵中心
export {default as forceCollide} from "./src/collide"; // 碰撞
export {default as forceLink} from "./src/link";
export {default as forceManyBody} from "./src/manyBody";
export {default as forceSimulation} from "./src/simulation";
export {default as forceX} from "./src/x";
export {default as forceY} from "./src/y";

其他引用模块

//collide.js
import constant from "./constant";    // 构造常量函数
import jiggle from "./jiggle";        // 微小晃动随机数
import {quadtree} from "d3-quadtree"; // 四叉树
模块1:center.js 设置力导图点阵中心

此处代码使用的是单例对象模式,读者要注意,切勿与类对象理解混了。

export default function(x, y) {
  var nodes; // 使用闭包构建私有变量,存储nodes。

  if (x == null) x = 0; // 力导图中心位置 x 默认值为0
  if (y == null) y = 0; // 力导图中心位置 y 默认值为0
   
  // force 单例对象
  function force() {
    var i,
        n = nodes.length,
        node,   // 临时变量用于循环
        sx = 0, // 临时变量用于计算
        sy = 0; // 临时变量用于计算
    
    for (i = 0; i < n; ++i) {
      // sx = sum(node.x);  节点x之和
      // sy = sum(node.y);  节点y之和
      node = nodes[i], sx += node.x, sy += node.y;
    }

    for (sx = sx / n - x, sy = sy / n - y, i = 0; i < n; ++i) {
      // sx / n 是点阵的中心x坐标;sy / n 是点阵的中心y坐标。
      // node.x = node.x + (x - (sx / n)); 该计算与此表达式等价,这样读者应该更好理解;
      // 坐标加减即平移坐标,即将整个点阵中心平移到坐标(x,y)
      node = nodes[i], node.x -= sx, node.y -= sy;
    }
  }
  // 初始化,为nodes私有变量赋值
  force.initialize = function(_) {
    nodes = _;
  };
  // 如果传入参数x则设置x,否则返回当前力导图中心位置 x
  force.x = function(_) {
    return arguments.length ? (x = +_, force) : x; 
  };
  // 如果传入参数y则设置y,否则返回当前力导图中心位置 y
  force.y = function(_) {
    return arguments.length ? (y = +_, force) : y;  
  };

  return force; // 返回 force对象
}
模块2:constant.js 创建一个常量函数
// 构造一个返回参数值的常量函数
// let a = constant(123); a() 输出: 123
export default function(x) {
  return function() {
    return x;
  };
}
模块3:jiggle.js 微小晃动随机数
// jiggle.js
// 微小晃动随机数
export default function() {
  return (Math.random() - 0.5) * 1e-6; // 1e-6 ==> 1*10的-6次方
}
模块4:collide.js 碰撞
import constant from "./constant";    // 构造常量函数
import jiggle from "./jiggle";        // 微小晃动随机数
import {quadtree} from "d3-quadtree"; // 四叉树

// vx vy 是指当前节点的运动速度
function x(d) {
  return d.x + d.vx; // 运动一步 x + vx 
}

function y(d) {
  return d.y + d.vy; // 运动一步 y + vy
}

export default function(radius) {
  var nodes,
      radii,
      strength = 1, // 力度
      iterations = 1;
      
  // radius 设置默认值,值类型为常量函数;
  if (typeof radius !== "function") radius = constant(radius == null ? 1 : +radius);

  // 单例对象模式
  function force() {
    var i, n = nodes.length,
        tree,
        node,
        xi,
        yi,
        ri,  // 半径
        ri2; // 半径平方
// -------------- 四叉树相关,**后文有详细分析**----------
    for (var k = 0; k < iterations; ++k) {
      // 以x,y访问器构建一个四叉树,即节点运动到下一步位置为坐标(就像我们走夜路,探出一步试试看)
      // visitAfter是后序遍历树的节点,执行prepare为每个节点求半径r,参数为各个节点,
      // 返回树的跟节点root。
      tree = quadtree(nodes, x, y).visitAfter(prepare); 
      // for循环普通遍历节点
      for (i = 0; i < n; ++i) {
        node = nodes[i];
        ri = radii[node.index], ri2 = ri * ri; // r平方(勾股定理用)
        xi = node.x + node.vx;// 运动一步 x + vx 
        yi = node.y + node.vy;// 运动一步 y + vy 
        // 前序遍历所有节点,apply返回true则不访问其子节点
        tree.visit(apply); 
      }
    }

    function apply(quad, x0, y0, x1, y1) {
      var data = quad.data, rj = quad.r, r = ri + rj;// 两个点与其作用域构成两个圆,请参考之前的文章,圆与圆的碰撞测验。
      if (data) { // 存在data即叶子节点,每个叶子节点为一个坐标点
        if (data.index > node.index) {
          // 因为这是二重循环,所有index小于自身的点坐标已经与自身判断过了,此处是为了避免重复测验
          // 设第一重循环Node[i]为节点A(xi,yi) 第二重循环为节点B(data.x,data.y)下一步运动(+=vx,+=vy)
          var x = xi - data.x - data.vx, // Ax - Bx
              y = yi - data.y - data.vy, // Ay - By
              l = x * x + y * y;  // 勾股定理 d^2 = x^2 +y^2
          if (l < r * r) { // 判断是否碰撞,如果碰撞执行以下,l:实际距离平方,r:半径之和
            if (x === 0) x = jiggle(), l += x * x; // 避免x值为0
            if (y === 0) y = jiggle(), l += y * y; // 避免y值为0
            // strength:碰撞力的强度,可以理解为两点之间的斥力系数
            // 见后文碰撞测验的图
            // l = 重叠长度/实际距离 * 碰撞力度
            // 重叠约多,斥力越大。斥力影响点的运动速度
            l = (r - (l = Math.sqrt(l))) / l * strength; 
            // 根据求出的斥力计算AB点新的运动速度与方向
            // A点x方向的运动速度 
            // A速度 += B速度 -= 使得AB两点往相反方向运动。注意,这里的x是B到A的距离,所有是A+= ,B-=
            // 但斥力的原因会使得节点的vx ,vy 趋近于0.
            // node.vx = B-A点x方向距离 *= 斥力 * B半径平方(rj = B半径平方)/( A半径平方+B半径平方);r = B半径平方/( A半径平方+B半径平方)
            node.vx += (x *= l) * (r = (rj *= rj) / (ri2 + rj));
            // 同x方向
            node.vy += (y *= l) * r;
            data.vx -= x * (r = 1 - r);
            data.vy -= y * r;
          }
        }
        return;
      }
      // 如果是父节点,这里需要读者理解四叉树【后面一篇文章会讲解】
      // 节点坐标为中心的正方形,如果没有覆盖到该父节点的正方形区域,这改点与此父节点的任何子节点都不会发生碰撞,则无需遍历其子节点校验。
      // 返回true 不遍历子节点
      // 这也是v4 相比v3对性能优化最重要的一个步骤,成倍的减少计算量
      return x0 > xi + r || x1 < xi - r || y0 > yi + r || y1 < yi - r;
    }
  }
  // 遍历树节点过滤器,返回true节点不可见
  function prepare(quad) {
    // quad.data是叶子节点才有的,所以这里是判断是否是叶子节点
    if (quad.data) return quad.r = radii[quad.data.index];
    for (var i = quad.r = 0; i < 4; ++i) {
      // 因为是后序遍历,所以节点的叶子节点一定在之前已经遍历过。
      // 取叶子节点四个象限最大的r
      if (quad[i] && quad[i].r > quad.r) {
        quad.r = quad[i].r;
      }
    }
  }
//---------------------------------------------------------------------------------------
  function initialize() {
    if (!nodes) return; // 判断是否有节点
    var i, n = nodes.length, node;
    radii = new Array(n);
    // 按照node.index索引排序nodes 并又 radius【后文解析】 计算出半径 后 存储在 radii 
    for (i = 0; i < n; ++i) node = nodes[i], radii[node.index] = +radius(node, i, nodes);
  }

  force.initialize = function(_) {
    nodes = _; // 赋值节点
    initialize(); // 初始化
  };

  force.iterations = function(_) {
    // get or set iterations (迭代次数)
    return arguments.length ? (iterations = +_, force) : iterations;
  };
  
  force.strength = function(_) {
    // get or set strength(力度)
    return arguments.length ? (strength = +_, force) : strength;
  };

  force.radius = function(_) {
    // 前端加+号 将字符串转为number  +"123" === 123
    // 有参数:
    // 执行1:(radius = typeof _ === "function" ? _ : constant(+_)
     //radius 值是一个返回自身的函数
    // 执行2:initialize()
    // 执行3:return force
    // 无参数:
    // 执行:return radius
    return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), initialize(), force) : radius;
  };

  return force;
}


碰撞测验

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81936.html

相关文章

  • d3-force 导图 源码解读原理分析【二 : 四叉树()】

    摘要:我们在上文源码解析发现版的节点碰撞采用四叉树进行了优化。那么版本的力导图具体和版的有何不同点呢,四叉树又如何优化碰撞校验的呢原文链接被重命名为。性能的提高归功于的新的四叉树。 我们在上文源码解析发现v4版的节点碰撞采用四叉树进行了优化。那么V4版本的力导图具体和v3版的有何不同点呢,四叉树又如何优化碰撞校验的呢? v3-force VS v4-force https://github...

    pepperwang 评论0 收藏0
  • 高并发

    摘要:表示的是两个,当其中任意一个计算完并发编程之是线程安全并且高效的,在并发编程中经常可见它的使用,在开始分析它的高并发实现机制前,先讲讲废话,看看它是如何被引入的。电商秒杀和抢购,是两个比较典型的互联网高并发场景。 干货:深度剖析分布式搜索引擎设计 分布式,高可用,和机器学习一样,最近几年被提及得最多的名词,听名字多牛逼,来,我们一步一步来击破前两个名词,今天我们首先来说说分布式。 探究...

    supernavy 评论0 收藏0
  • 高并发

    摘要:表示的是两个,当其中任意一个计算完并发编程之是线程安全并且高效的,在并发编程中经常可见它的使用,在开始分析它的高并发实现机制前,先讲讲废话,看看它是如何被引入的。电商秒杀和抢购,是两个比较典型的互联网高并发场景。 干货:深度剖析分布式搜索引擎设计 分布式,高可用,和机器学习一样,最近几年被提及得最多的名词,听名字多牛逼,来,我们一步一步来击破前两个名词,今天我们首先来说说分布式。 探究...

    ddongjian0000 评论0 收藏0
  • 高并发

    摘要:表示的是两个,当其中任意一个计算完并发编程之是线程安全并且高效的,在并发编程中经常可见它的使用,在开始分析它的高并发实现机制前,先讲讲废话,看看它是如何被引入的。电商秒杀和抢购,是两个比较典型的互联网高并发场景。 干货:深度剖析分布式搜索引擎设计 分布式,高可用,和机器学习一样,最近几年被提及得最多的名词,听名字多牛逼,来,我们一步一步来击破前两个名词,今天我们首先来说说分布式。 探究...

    wangdai 评论0 收藏0
  • ❤️思维导图整理大厂面试高频数组10: 3种方法彻底解决中位数问题, 力扣4❤️

    此专栏文章是对力扣上算法题目各种方法的总结和归纳, 整理出最重要的思路和知识重点并以思维导图形式呈现, 当然也会加上我对导图的详解. 目的是为了更方便快捷的记忆和回忆算法重点(不用每次都重复看题解), 毕竟算法不是做了一遍就能完全记住的. 所以本文适合已经知道解题思路和方法, 想进一步加强理解和记忆的朋友, 并不适合第一次接触此题的朋友(可以根据题号先去力扣看看官方题解, 然后再看本文内容). 关...

    XanaHopper 评论0 收藏0

发表评论

0条评论

luckyyulin

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<