D3原始碼解讀系列之Force

arlendp2012發表於2019-11-01

Force模組用於模擬在粒子上的物理作用力,常用於網路圖和層級圖。

Simulation

d3.forceSimulation

根據指定的nodes值建立一個新的simulation,此時還沒有設定force函式。

//d3.forceSimulation用於設定節點和相關引數
function simulation(nodes) {
    var simulation,
        //alpha表示simulation當前的狀態
        alpha = 1,
        alphaMin = 0.001,
        //alphaDecay表示alpha每次的衰減率
        alphaDecay = 1 - Math.pow(alphaMin, 1 / 300),
        //alphaTarget表示最終要穩定時的狀態
        alphaTarget = 0,
        //velocityDecay表示速度的衰退率
        velocityDecay = 0.6,
        //用於儲存force函式
        forces = map$1(),
        stepper = timer(step),
        //simulation包含以下兩種型別的事件
        event = dispatch("tick", "end");

    if (nodes == null) nodes = [];

    function step() {
      tick();
      //自定義的tick函式,在這裡被呼叫
      event.call("tick", simulation);
      //當alpha小於臨界值即alphaMin時,停止計時
      if (alpha < alphaMin) {
        stepper.stop();
        //自定義的end函式在這裡呼叫
        event.call("end", simulation);
      }
    }

    function tick() {
      var i, n = nodes.length, node;

      alpha += (alphaTarget - alpha) * alphaDecay;
      //alpha用於force中對速度vx和vy進行設定
      forces.each(function(force) {
        force(alpha);
      });

      for (i = 0; i < n; ++i) {
        node = nodes[i];
        //fx和fy是node的固定點,如果設定了該屬性則node會固定在該位置
        //這裡簡化了物理作用力,將當前位置座標加上當前速度得到下一步的位置座標
        if (node.fx == null) node.x += node.vx *= velocityDecay;
        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;
      }
    }
    //對nodes進行處理
    function initializeNodes() {
      for (var i = 0, n = nodes.length, node; i < n; ++i) {
        node = nodes[i], node.index = i;
        //如果node中不含x、 y值,則按預設方法計算。
        if (isNaN(node.x) || isNaN(node.y)) {
          var radius = initialRadius * Math.sqrt(i), angle = i * initialAngle;
          node.x = radius * Math.cos(angle);
          node.y = radius * Math.sin(angle);
        }
        //如果不含vx、vy值,則預設為0。
        if (isNaN(node.vx) || isNaN(node.vy)) {
          node.vx = node.vy = 0;
        }
      }
    }

    function initializeForce(force) {
      if (force.initialize) force.initialize(nodes);
      return force;
    }

    initializeNodes();

    return simulation = {
      tick: tick,

      restart: function() {
        return stepper.restart(step), simulation;
      },

      stop: function() {
        return stepper.stop(), simulation;
      },
      //設定nodes時會對所有的force進行初始化
      nodes: function(_) {
        return arguments.length ? (nodes = _, initializeNodes(), forces.each(initializeForce), simulation) : nodes;
      },

      alpha: function(_) {
        return arguments.length ? (alpha = +_, simulation) : alpha;
      },

      alphaMin: function(_) {
        return arguments.length ? (alphaMin = +_, simulation) : alphaMin;
      },

      alphaDecay: function(_) {
        return arguments.length ? (alphaDecay = +_, simulation) : +alphaDecay;
      },

      alphaTarget: function(_) {
        return arguments.length ? (alphaTarget = +_, simulation) : alphaTarget;
      },

      velocityDecay: function(_) {
        return arguments.length ? (velocityDecay = 1 - _, simulation) : 1 - velocityDecay;
      },

      force: function(name, _) {
        return arguments.length > 1 ? ((_ == null ? forces.remove(name) : forces.set(name, initializeForce(_))), simulation) : forces.get(name);
      },

      find: function(x, y, radius) {
        var i = 0,
            n = nodes.length,
            dx,
            dy,
            d2,
            node,
            closest;

        if (radius == null) radius = Infinity;
        else radius *= radius;

        for (i = 0; i < n; ++i) {
          node = nodes[i];
          dx = x - node.x;
          dy = y - node.y;
          d2 = dx * dx + dy * dy;
          if (d2 < radius) closest = node, radius = d2;
        }

        return closest;
      },

      on: function(name, _) {
        return arguments.length > 1 ? (event.on(name, _), simulation) : event.on(name);
      }
    };
}
複製程式碼

上述方法對nodes進行處理,計算其x和y值以及初始化vx、vy值,其中很重要的一部分是force函式,該函式用來模擬物理作用力來改變nodes的位置和速度。

Force

force函式在定時器執行過程中會重複呼叫,用於控制nodes的座標和速度。

d3.forceLink

forceLink主要用於nodes之間的聯絡即links,每個link會將兩個不同的node以source和target的方式進行連線,同時內部會對vx、vy進行調整。

/* d3.forceLink
   * force用於控制節點之間的聯絡
   * !將於節點相連的link的數量記作該節點的權值
   */
function link(links) {
    var id = index$2,
        strength = defaultStrength,
        strengths,
        //預設link的長度都為30
        distance = constant$6(30),
        distances,
        nodes,
        //count記錄跟每個節點有關聯的節點數量,即該節點的權值
        count,
        //bias儲存每條link對應的source的權值與source和target權值和的比值
        bias,
        iterations = 1;

    if (links == null) links = [];
    //預設計算link的強度的方法
    function defaultStrength(link) {
      return 1 / Math.min(count[link.source.index], count[link.target.index]);
    }

    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();
          y = target.y + target.vy - source.y - source.vy || jiggle();
          //target和source的距離為l
          l = Math.sqrt(x * x + y * y);
          l = (l - distances[i]) / l * alpha * strengths[i];
          x *= l, y *= l;
          //對target和source的速度進行調整
          target.vx -= x * (b = bias[i]);
          target.vy -= y * b;
          source.vx += x * (b = 1 - b);
          source.vy += y * b;
        }
      }
    }

    function initialize() {
      if (!nodes) return;

      var i,
          n = nodes.length,
          m = links.length,
          //對nodes中每個值設定id作為鍵值
          nodeById = map$1(nodes, id),
          link;

      for (i = 0, count = new Array(n); i < n; ++i) {
        count[i] = 0;
      }

      for (i = 0; i < m; ++i) {
        link = links[i], link.index = i;
        //將link中的source和target值作為id來查詢node
        if (typeof link.source !== "object") link.source = nodeById.get(link.source);
        if (typeof link.target !== "object") link.target = nodeById.get(link.target);
        ++count[link.source.index], ++count[link.target.index];
      }

      for (i = 0, bias = new Array(m); i < m; ++i) {
        link = links[i], bias[i] = count[link.source.index] / (count[link.source.index] + count[link.target.index]);
      }

      strengths = new Array(m), initializeStrength();
      distances = new Array(m), initializeDistance();
    }

    function initializeStrength() {
      if (!nodes) return;

      for (var i = 0, n = links.length; i < n; ++i) {
        strengths[i] = +strength(links[i], i, links);
      }
    }

    function initializeDistance() {
      if (!nodes) return;

      for (var i = 0, n = links.length; i < n; ++i) {
        distances[i] = +distance(links[i], i, links);
      }
    }

    force.initialize = function(_) {
      nodes = _;
      initialize();
    };

    force.links = function(_) {
      return arguments.length ? (links = _, initialize(), force) : links;
    };

    force.id = function(_) {
      return arguments.length ? (id = _, force) : id;
    };

    force.iterations = function(_) {
      return arguments.length ? (iterations = +_, force) : iterations;
    };

    force.strength = function(_) {
      return arguments.length ? (strength = typeof _ === "function" ? _ : constant$6(+_), initializeStrength(), force) : strength;
    };

    force.distance = function(_) {
      return arguments.length ? (distance = typeof _ === "function" ? _ : constant$6(+_), initializeDistance(), force) : distance;
    };

    return force;
}
複製程式碼

d3.forceCenter

forceCenter根據設定的(x, y)座標而將node的座標向其移動。

//d3.forceCenter
function center$1(x, y) {
    var nodes;

    if (x == null) x = 0;
    if (y == null) y = 0;

    function force() {
      var i,
          n = nodes.length,
          node,
          sx = 0,
          sy = 0;

      for (i = 0; i < n; ++i) {
        node = nodes[i], sx += node.x, sy += node.y;
      }
      //這裡簡化了粒子的質量,認為都相等,通過sx / n和sy / n得到所有粒子的重心
      for (sx = sx / n - x, sy = sy / n - y, i = 0; i < n; ++i) {
        //將所有粒子的座標向中心點靠近
        node = nodes[i], node.x -= sx, node.y -= sy;
      }
    }

    force.initialize = function(_) {
      nodes = _;
    };

    force.x = function(_) {
      return arguments.length ? (x = +_, force) : x;
    };

    force.y = function(_) {
      return arguments.length ? (y = +_, force) : y;
    };

    return force;
}
複製程式碼

d3.forceCollide

forceCollide方法將nodes不再看做一個點而是一個指定半徑的圓形,防止不同的節點發生碰撞即要滿足兩個圓心的距離大於兩個圓形半徑之和。

//d3.forceCollide
function collide(radius) {
    var nodes,
        radii,
        strength = 1,
        iterations = 1;
    //如果沒有設定radius,則預設為1
    if (typeof radius !== "function") radius = constant$6(radius == null ? 1 : +radius);

    function force() {
      var i, n = nodes.length,
          tree,
          node,
          xi,
          yi,
          ri,
          ri2;

      for (var k = 0; k < iterations; ++k) {
        //visitAfter函式使得對每個node都執行prepare。這裡採用後續遍歷的方法,因為只有知道了孩子節點的半徑才能確定根節點半徑
        tree = quadtree(nodes, x$1, y$1).visitAfter(prepare);
        //依次訪問所有的node節點,判斷其他節點是否可能與其重疊
        for (i = 0; i < n; ++i) {
          node = nodes[i];
          ri = radii[i], ri2 = ri * ri;
          xi = node.x + node.vx;
          yi = node.y + node.vy;
          tree.visit(apply);
        }
      }
      //這裡用於對重疊的節點進行處理,如果當前節點為根節點則判斷node是否與該根節點的範圍有重疊,如果沒有則返回true,不再訪問其子節點;否則繼續訪問其子節點。
      //如果當前節點為葉子節點,
      function apply(quad, x0, y0, x1, y1) {
        var data = quad.data, rj = quad.r, r = ri + rj;
        if (data) {
          // 只比較index大於i的,可防止重複比較
          if (data.index > i) {
            var x = xi - data.x - data.vx,
                y = yi - data.y - data.vy,
                l = x * x + y * y;
            if (l < r * r) {
              if (x === 0) x = jiggle(), l += x * x;
              if (y === 0) y = jiggle(), l += y * y;
              l = (r - (l = Math.sqrt(l))) / l * strength;
              //根據兩個節點間的距離和兩個節點的半徑對node和data的速度進行調整
              //TODO: 為什麼這樣調整???
              node.vx += (x *= l) * (r = (rj *= rj) / (ri2 + rj));
              node.vy += (y *= l) * r;
              data.vx -= x * (r = 1 - r);
              data.vy -= y * r;
            }
          }
          return;
        }
        return x0 > xi + r || x1 < xi - r || y0 > yi + r || y1 < yi - r;
      }
    }
    //為所有節點設定半徑
    function prepare(quad) {
      if (quad.data) return quad.r = radii[quad.data.index];
      //quad是一個陣列,即quad不是葉子節點時,將其所有子節點的最大半徑複製給quad.r
      for (var i = quad.r = 0; i < 4; ++i) {
        if (quad[i] && quad[i].r > quad.r) {
          quad.r = quad[i].r;
        }
      }
    }
    //初始化時通過radius函式處理nodes得到每個node的半徑
    force.initialize = function(_) {
      var i, n = (nodes = _).length; radii = new Array(n);
      for (i = 0; i < n; ++i) radii[i] = +radius(nodes[i], i, nodes);
    };
    //iteration值越大,node節點重疊情況就會越小
    force.iterations = function(_) {
      return arguments.length ? (iterations = +_, force) : iterations;
    };
    //strength用於在兩個節點重疊時調整節點的速度
    force.strength = function(_) {
      return arguments.length ? (strength = +_, force) : strength;
    };
    //設定節點的獲取半徑的函式
    force.radius = function(_) {
      return arguments.length ? (radius = typeof _ === "function" ? _ : constant$6(+_), force) : radius;
    };

    return force;
}
複製程式碼

d3.forceManyBody

用於模擬所有粒子間的作用力,例如模擬重力或者靜電力。

//d3.forceManyBody
function manyBody() {
    var nodes,
        node,
        alpha,
        //當strength為正值時粒子間會互相吸引,當為負值時粒子間會互相排斥
        //在這裡表現為當strength為正值時,兩個互相作用的粒子速度會增加,互相靠近;為負值時,兩個粒子速度減小,互相遠離。
        strength = constant$6(-30),
        strengths,
        distanceMin2 = 1,
        distanceMax2 = Infinity,
        //theta用於判斷距離遠近而採取不同的方法對粒子的速度進行處理
        theta2 = 0.81;

    function force(_) {
      var i, n = nodes.length, tree = quadtree(nodes, x$2, y$2).visitAfter(accumulate);
      for (alpha = _, i = 0; i < n; ++i) node = nodes[i], tree.visit(apply);
    }

    function initialize() {
      if (!nodes) return;
      var i, n = nodes.length;
      strengths = new Array(n);
      for (i = 0; i < n; ++i) strengths[i] = +strength(nodes[i], i, nodes);
    }

    function accumulate(quad) {
      var strength = 0, q, c, x, y, i;

      // 對於根節點,根據其子節點來計算
      if (quad.length) {
        for (x = y = i = 0; i < 4; ++i) {
          if ((q = quad[i]) && (c = q.value)) {
            strength += c, x += c * q.x, y += c * q.y;
          }
        }
        quad.x = x / strength;
        quad.y = y / strength;
      }

      // 對於葉子節點,根據其是否有相同節點來計算strength值
      else {
        q = quad;
        q.x = q.data.x;
        q.y = q.data.y;
        do strength += strengths[q.data.index];
        while (q = q.next);
      }

      quad.value = strength;
    }

    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;

      // 如果quad和node間的距離較遠則根據value、alpha和l來調整node的速度
      if (w * w / theta2 < l) {
        if (l < distanceMax2) {
          if (x === 0) x = jiggle(), l += x * x;
          if (y === 0) y = jiggle(), 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;
      }

      // 如果quad為根節點則返回去訪問其子節點
      else if (quad.length || l >= distanceMax2) return;
      //quad和node相同時不會執行以下過程
      //當quad和node間距離較近時,同時要考慮strength來調整node的速度
      
      if (quad.data !== node || quad.next) {
        if (x === 0) x = jiggle(), l += x * x;
        if (y === 0) y = jiggle(), 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);
    }

    force.initialize = function(_) {
      nodes = _;
      initialize();
    };

    force.strength = function(_) {
      return arguments.length ? (strength = typeof _ === "function" ? _ : constant$6(+_), initialize(), force) : strength;
    };

    force.distanceMin = function(_) {
      return arguments.length ? (distanceMin2 = _ * _, force) : Math.sqrt(distanceMin2);
    };

    force.distanceMax = function(_) {
      return arguments.length ? (distanceMax2 = _ * _, force) : Math.sqrt(distanceMax2);
    };

    force.theta = function(_) {
      return arguments.length ? (theta2 = _ * _, force) : Math.sqrt(theta2);
    };

    return force;
}
複製程式碼

d3.forceX

使所有的節點向指定的x座標處靠近。

//d3.forceX
function x$3(x) {
    var strength = constant$6(0.1),
        nodes,
        strengths,
        xz;

    if (typeof x !== "function") x = constant$6(x == null ? 0 : +x);

    function force(alpha) {
      for (var i = 0, n = nodes.length, node; i < n; ++i) {
        //通過xz和strength來改變node的x軸方向的速度,使得節點像xz處靠近。strength的值越大,node的速度改變的越快,即會更快的到達指定座標位置而趨於穩定
        node = nodes[i], node.vx += (xz[i] - node.x) * strengths[i] * alpha;
      }
    }

    function initialize() {
      if (!nodes) return;
      var i, n = nodes.length;
      strengths = new Array(n);
      xz = new Array(n);
      for (i = 0; i < n; ++i) {
        //對每個node分別計算x座標存入xz陣列中,同時計算strength值
        strengths[i] = isNaN(xz[i] = +x(nodes[i], i, nodes)) ? 0 : +strength(nodes[i], i, nodes);
      }
    }

    force.initialize = function(_) {
      nodes = _;
      initialize();
    };

    force.strength = function(_) {
      return arguments.length ? (strength = typeof _ === "function" ? _ : constant$6(+_), initialize(), force) : strength;
    };

    force.x = function(_) {
      return arguments.length ? (x = typeof _ === "function" ? _ : constant$6(+_), initialize(), force) : x;
    };

    return force;
}
複製程式碼

相關文章