d3-force 力導圖 源碼解讀與原理分析【一】

首先先推薦一下某呆翻譯的d3-force的中文文檔:https://github.com/xswei/D3-V...
在咱們解讀源碼前還請讀者先熟悉一下force相關的API,以及es6語法 .前端

若有分析不當之處還請留言指出,謝謝~node

那咱們進入正題吧git

D3-force 有什麼

咱們來看一下index.js 這個文件你們能夠理解爲force的對外統一出口。固然你也能夠自定義使用這些模塊。es6

// 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";

其餘引用模塊github

//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;
}

clipboard.png
碰撞測驗閉包

相關文章
相關標籤/搜索