Skip to content
Snippets Groups Projects
3d-force-graph.js 1.6 MiB
Newer Older
Amira Abdel-Rahman's avatar
Amira Abdel-Rahman committed
54001 54002 54003 54004 54005 54006 54007 54008 54009 54010 54011 54012 54013 54014 54015 54016 54017 54018 54019 54020 54021 54022 54023 54024 54025 54026 54027 54028 54029 54030 54031 54032 54033 54034 54035 54036 54037 54038 54039 54040 54041 54042 54043 54044 54045 54046 54047 54048 54049 54050 54051 54052 54053 54054 54055 54056 54057 54058 54059 54060 54061 54062 54063 54064 54065 54066 54067 54068 54069 54070 54071 54072 54073 54074 54075 54076 54077 54078 54079 54080 54081 54082 54083 54084 54085 54086 54087 54088 54089 54090 54091 54092 54093 54094 54095 54096 54097 54098 54099 54100 54101 54102 54103 54104 54105 54106 54107 54108 54109 54110 54111 54112 54113 54114 54115 54116 54117 54118 54119 54120 54121 54122 54123 54124 54125 54126 54127 54128 54129 54130 54131 54132 54133 54134 54135 54136 54137 54138 54139 54140 54141 54142 54143 54144 54145 54146 54147 54148 54149 54150 54151 54152 54153 54154 54155 54156 54157 54158 54159 54160 54161 54162 54163 54164 54165 54166 54167 54168 54169 54170 54171 54172 54173 54174 54175 54176 54177 54178 54179 54180 54181 54182 54183 54184 54185 54186 54187 54188 54189 54190 54191 54192 54193 54194 54195 54196 54197 54198 54199 54200 54201 54202 54203 54204 54205 54206 54207 54208 54209 54210 54211 54212 54213 54214 54215 54216 54217 54218 54219 54220 54221 54222 54223 54224 54225 54226 54227 54228 54229 54230 54231 54232 54233 54234 54235 54236 54237 54238 54239 54240 54241 54242 54243 54244 54245 54246 54247 54248 54249 54250 54251 54252 54253 54254 54255 54256 54257 54258 54259 54260 54261 54262 54263 54264 54265 54266 54267 54268 54269 54270 54271 54272 54273 54274 54275 54276 54277 54278 54279 54280 54281 54282 54283 54284 54285 54286 54287 54288 54289 54290 54291 54292 54293 54294 54295 54296 54297 54298 54299 54300 54301 54302 54303 54304 54305 54306 54307 54308 54309 54310 54311 54312 54313 54314 54315 54316 54317 54318 54319 54320 54321 54322 54323 54324 54325 54326 54327 54328 54329 54330 54331 54332 54333 54334 54335 54336 54337 54338 54339 54340 54341 54342 54343 54344 54345 54346 54347 54348 54349 54350 54351 54352 54353 54354 54355 54356 54357 54358 54359 54360 54361 54362 54363 54364 54365 54366 54367 54368 54369 54370 54371 54372 54373 54374 54375 54376 54377 54378 54379 54380 54381 54382 54383 54384 54385 54386 54387 54388 54389 54390 54391 54392 54393 54394 54395 54396 54397 54398 54399 54400 54401 54402 54403 54404 54405 54406 54407 54408 54409 54410 54411 54412 54413 54414 54415 54416 54417 54418 54419 54420 54421 54422 54423 54424 54425 54426 54427 54428 54429 54430 54431 54432 54433 54434 54435 54436 54437 54438 54439 54440 54441 54442 54443 54444 54445 54446 54447 54448 54449 54450 54451 54452 54453 54454 54455 54456 54457 54458 54459 54460 54461 54462 54463 54464 54465 54466 54467 54468 54469 54470 54471 54472 54473 54474 54475 54476 54477 54478 54479 54480 54481 54482 54483 54484 54485 54486 54487 54488 54489 54490 54491 54492 54493 54494 54495 54496 54497 54498 54499 54500 54501 54502 54503 54504 54505 54506 54507 54508 54509 54510 54511 54512 54513 54514 54515 54516 54517 54518 54519 54520 54521 54522 54523 54524 54525 54526 54527 54528 54529 54530 54531 54532 54533 54534 54535 54536 54537 54538 54539 54540 54541 54542 54543 54544 54545 54546 54547 54548 54549 54550 54551 54552 54553 54554 54555 54556 54557 54558 54559 54560 54561 54562 54563 54564 54565 54566 54567 54568 54569 54570 54571 54572 54573 54574 54575 54576 54577 54578 54579 54580 54581 54582 54583 54584 54585 54586 54587 54588 54589 54590 54591 54592 54593 54594 54595 54596 54597 54598 54599 54600 54601 54602 54603 54604 54605 54606 54607 54608 54609 54610 54611 54612 54613 54614 54615 54616 54617 54618 54619 54620 54621 54622 54623 54624 54625 54626 54627 54628 54629 54630 54631 54632 54633 54634 54635 54636 54637 54638 54639 54640 54641 54642 54643 54644 54645 54646 54647 54648 54649 54650 54651 54652 54653 54654 54655 54656 54657 54658 54659 54660 54661 54662 54663 54664 54665 54666 54667 54668 54669 54670 54671 54672 54673 54674 54675 54676 54677 54678 54679 54680 54681 54682 54683 54684 54685 54686 54687 54688 54689 54690 54691 54692 54693 54694 54695 54696 54697 54698 54699 54700 54701 54702 54703 54704 54705 54706 54707 54708 54709 54710 54711 54712 54713 54714 54715 54716 54717 54718 54719 54720 54721 54722 54723 54724 54725 54726 54727 54728 54729 54730 54731 54732 54733 54734 54735 54736 54737 54738 54739 54740 54741 54742 54743 54744 54745 54746 54747 54748 54749 54750 54751 54752 54753 54754 54755 54756 54757 54758 54759 54760 54761 54762 54763 54764 54765 54766 54767 54768 54769 54770 54771 54772 54773 54774 54775 54776 54777 54778 54779 54780 54781 54782 54783 54784 54785 54786 54787 54788 54789 54790 54791 54792 54793 54794 54795 54796 54797 54798 54799 54800 54801 54802 54803 54804 54805 54806 54807 54808 54809 54810 54811 54812 54813 54814 54815 54816 54817 54818 54819 54820 54821 54822 54823 54824 54825 54826 54827 54828 54829 54830 54831 54832 54833 54834 54835 54836 54837 54838 54839 54840 54841 54842 54843 54844 54845 54846 54847 54848 54849 54850 54851 54852 54853 54854 54855 54856 54857 54858 54859 54860 54861 54862 54863 54864 54865 54866 54867 54868 54869 54870 54871 54872 54873 54874 54875 54876 54877 54878 54879 54880 54881 54882 54883 54884 54885 54886 54887 54888 54889 54890 54891 54892 54893 54894 54895 54896 54897 54898 54899 54900 54901 54902 54903 54904 54905 54906 54907 54908 54909 54910 54911 54912 54913 54914 54915 54916 54917 54918 54919 54920 54921 54922 54923 54924 54925 54926 54927 54928 54929 54930 54931 54932 54933 54934 54935 54936 54937 54938 54939 54940 54941 54942 54943 54944 54945 54946 54947 54948 54949 54950 54951 54952 54953 54954 54955 54956 54957 54958 54959 54960 54961 54962 54963 54964 54965 54966 54967 54968 54969 54970 54971 54972 54973 54974 54975 54976 54977 54978 54979 54980 54981 54982 54983 54984 54985 54986 54987 54988 54989 54990 54991 54992 54993 54994 54995 54996 54997 54998 54999 55000
          // and the node's center-of-mass
          dx = node.massX / node.mass - sourceBody.pos.x;
          dy = node.massY / node.mass - sourceBody.pos.y;
          r = Math.sqrt(dx * dx + dy * dy);

          if (r === 0) {
            // Sorry about code duplucation. I don't want to create many functions
            // right away. Just want to see performance first.
            dx = (random.nextDouble() - 0.5) / 50;
            dy = (random.nextDouble() - 0.5) / 50;
            r = Math.sqrt(dx * dx + dy * dy);
          }
          // If s / r < θ, treat this internal node as a single body, and calculate the
          // force it exerts on sourceBody, and add this amount to sourceBody's net force.
          if ((node.right - node.left) / r < theta) {
            // in the if statement above we consider node's width only
            // because the region was squarified during tree creation.
            // Thus there is no difference between using width or height.
            v = gravity * node.mass * sourceBody.mass / (r * r * r);
            fx += v * dx;
            fy += v * dy;
          } else {
            // Otherwise, run the procedure recursively on each of the current node's children.

            // I intentionally unfolded this loop, to save several CPU cycles.
            if (node.quad0) {
              queue[pushIdx] = node.quad0;
              queueLength += 1;
              pushIdx += 1;
            }
            if (node.quad1) {
              queue[pushIdx] = node.quad1;
              queueLength += 1;
              pushIdx += 1;
            }
            if (node.quad2) {
              queue[pushIdx] = node.quad2;
              queueLength += 1;
              pushIdx += 1;
            }
            if (node.quad3) {
              queue[pushIdx] = node.quad3;
              queueLength += 1;
              pushIdx += 1;
            }
          }
        }
      }

      sourceBody.force.x += fx;
      sourceBody.force.y += fy;
    }

    function insertBodies(bodies) {
      var x1 = Number.MAX_VALUE,
        y1 = Number.MAX_VALUE,
        x2 = Number.MIN_VALUE,
        y2 = Number.MIN_VALUE,
        i,
        max = bodies.length;

      // To reduce quad tree depth we are looking for exact bounding box of all particles.
      i = max;
      while (i--) {
        var x = bodies[i].pos.x;
        var y = bodies[i].pos.y;
        if (x < x1) {
          x1 = x;
        }
        if (x > x2) {
          x2 = x;
        }
        if (y < y1) {
          y1 = y;
        }
        if (y > y2) {
          y2 = y;
        }
      }

      // Squarify the bounds.
      var dx = x2 - x1,
        dy = y2 - y1;
      if (dx > dy) {
        y2 = y1 + dx;
      } else {
        x2 = x1 + dy;
      }

      currentInCache = 0;
      root = newNode();
      root.left = x1;
      root.right = x2;
      root.top = y1;
      root.bottom = y2;

      i = max - 1;
      if (i >= 0) {
        root.body = bodies[i];
      }
      while (i--) {
        insert(bodies[i]);
      }
    }

    function insert(newBody) {
      insertStack$1.reset();
      insertStack$1.push(root, newBody);

      while (!insertStack$1.isEmpty()) {
        var stackItem = insertStack$1.pop(),
          node = stackItem.node,
          body = stackItem.body;

        if (!node.body) {
          // This is internal node. Update the total mass of the node and center-of-mass.
          var x = body.pos.x;
          var y = body.pos.y;
          node.mass = node.mass + body.mass;
          node.massX = node.massX + body.mass * x;
          node.massY = node.massY + body.mass * y;

          // Recursively insert the body in the appropriate quadrant.
          // But first find the appropriate quadrant.
          var quadIdx = 0, // Assume we are in the 0's quad.
            left = node.left,
            right = (node.right + left) / 2,
            top = node.top,
            bottom = (node.bottom + top) / 2;

          if (x > right) { // somewhere in the eastern part.
            quadIdx = quadIdx + 1;
            left = right;
            right = node.right;
          }
          if (y > bottom) { // and in south.
            quadIdx = quadIdx + 2;
            top = bottom;
            bottom = node.bottom;
          }

          var child = getChild(node, quadIdx);
          if (!child) {
            // The node is internal but this quadrant is not taken. Add
            // subnode to it.
            child = newNode();
            child.left = left;
            child.top = top;
            child.right = right;
            child.bottom = bottom;
            child.body = body;

            setChild(node, quadIdx, child);
          } else {
            // continue searching in this quadrant.
            insertStack$1.push(child, body);
          }
        } else {
          // We are trying to add to the leaf node.
          // We have to convert current leaf into internal node
          // and continue adding two nodes.
          var oldBody = node.body;
          node.body = null; // internal nodes do not cary bodies

          if (isSamePosition$1(oldBody.pos, body.pos)) {
            // Prevent infinite subdivision by bumping one node
            // anywhere in this quadrant
            var retriesCount = 3;
            do {
              var offset = random.nextDouble();
              var dx = (node.right - node.left) * offset;
              var dy = (node.bottom - node.top) * offset;

              oldBody.pos.x = node.left + dx;
              oldBody.pos.y = node.top + dy;
              retriesCount -= 1;
              // Make sure we don't bump it out of the box. If we do, next iteration should fix it
            } while (retriesCount > 0 && isSamePosition$1(oldBody.pos, body.pos));

            if (retriesCount === 0 && isSamePosition$1(oldBody.pos, body.pos)) {
              // This is very bad, we ran out of precision.
              // if we do not return from the method we'll get into
              // infinite loop here. So we sacrifice correctness of layout, and keep the app running
              // Next layout iteration should get larger bounding box in the first step and fix this
              return;
            }
          }
          // Next iteration should subdivide node further.
          insertStack$1.push(node, oldBody);
          insertStack$1.push(node, body);
        }
      }
    }
  };

  function getChild(node, idx) {
    if (idx === 0) return node.quad0;
    if (idx === 1) return node.quad1;
    if (idx === 2) return node.quad2;
    if (idx === 3) return node.quad3;
    return null;
  }

  function setChild(node, idx, child) {
    if (idx === 0) node.quad0 = child;
    else if (idx === 1) node.quad1 = child;
    else if (idx === 2) node.quad2 = child;
    else if (idx === 3) node.quad3 = child;
  }

  var bounds = function (bodies, settings) {
    var random = ngraph_random.random(42);
    var boundingBox =  { x1: 0, y1: 0, x2: 0, y2: 0 };

    return {
      box: boundingBox,

      update: updateBoundingBox,

      reset : function () {
        boundingBox.x1 = boundingBox.y1 = 0;
        boundingBox.x2 = boundingBox.y2 = 0;
      },

      getBestNewPosition: function (neighbors) {
        var graphRect = boundingBox;

        var baseX = 0, baseY = 0;

        if (neighbors.length) {
          for (var i = 0; i < neighbors.length; ++i) {
            baseX += neighbors[i].pos.x;
            baseY += neighbors[i].pos.y;
          }

          baseX /= neighbors.length;
          baseY /= neighbors.length;
        } else {
          baseX = (graphRect.x1 + graphRect.x2) / 2;
          baseY = (graphRect.y1 + graphRect.y2) / 2;
        }

        var springLength = settings.springLength;
        return {
          x: baseX + random.next(springLength) - springLength / 2,
          y: baseY + random.next(springLength) - springLength / 2
        };
      }
    };

    function updateBoundingBox() {
      var i = bodies.length;
      if (i === 0) { return; } // don't have to wory here.

      var x1 = Number.MAX_VALUE,
          y1 = Number.MAX_VALUE,
          x2 = Number.MIN_VALUE,
          y2 = Number.MIN_VALUE;

      while(i--) {
        // this is O(n), could it be done faster with quadtree?
        // how about pinned nodes?
        var body = bodies[i];
        if (body.isPinned) {
          body.pos.x = body.prevPos.x;
          body.pos.y = body.prevPos.y;
        } else {
          body.prevPos.x = body.pos.x;
          body.prevPos.y = body.pos.y;
        }
        if (body.pos.x < x1) {
          x1 = body.pos.x;
        }
        if (body.pos.x > x2) {
          x2 = body.pos.x;
        }
        if (body.pos.y < y1) {
          y1 = body.pos.y;
        }
        if (body.pos.y > y2) {
          y2 = body.pos.y;
        }
      }

      boundingBox.x1 = x1;
      boundingBox.x2 = x2;
      boundingBox.y1 = y1;
      boundingBox.y2 = y2;
    }
  };

  /**
   * Represents drag force, which reduces force value on each step by given
   * coefficient.
   *
   * @param {Object} options for the drag force
   * @param {Number=} options.dragCoeff drag force coefficient. 0.1 by default
   */
  var dragForce = function (options) {
    var merge = ngraph_merge,
        expose = ngraph_expose;

    options = merge(options, {
      dragCoeff: 0.02
    });

    var api = {
      update : function (body) {
        body.force.x -= options.dragCoeff * body.velocity.x;
        body.force.y -= options.dragCoeff * body.velocity.y;
      }
    };

    // let easy access to dragCoeff:
    expose(options, api, ['dragCoeff']);

    return api;
  };

  /**
   * Represents spring force, which updates forces acting on two bodies, connected
   * by a spring.
   *
   * @param {Object} options for the spring force
   * @param {Number=} options.springCoeff spring force coefficient.
   * @param {Number=} options.springLength desired length of a spring at rest.
   */
  var springForce = function (options) {
    var merge = ngraph_merge;
    var random = ngraph_random.random(42);
    var expose = ngraph_expose;

    options = merge(options, {
      springCoeff: 0.0002,
      springLength: 80
    });

    var api = {
      /**
       * Upsates forces acting on a spring
       */
      update : function (spring) {
        var body1 = spring.from,
            body2 = spring.to,
            length = spring.length < 0 ? options.springLength : spring.length,
            dx = body2.pos.x - body1.pos.x,
            dy = body2.pos.y - body1.pos.y,
            r = Math.sqrt(dx * dx + dy * dy);

        if (r === 0) {
            dx = (random.nextDouble() - 0.5) / 50;
            dy = (random.nextDouble() - 0.5) / 50;
            r = Math.sqrt(dx * dx + dy * dy);
        }

        var d = r - length;
        var coeff = ((!spring.coeff || spring.coeff < 0) ? options.springCoeff : spring.coeff) * d / r;

        body1.force.x += coeff * dx;
        body1.force.y += coeff * dy;

        body2.force.x -= coeff * dx;
        body2.force.y -= coeff * dy;
      }
    };

    expose(options, api, ['springCoeff', 'springLength']);
    return api;
  };

  /**
   * Performs forces integration, using given time step. Uses Euler method to solve
   * differential equation (http://en.wikipedia.org/wiki/Euler_method ).
   *
   * @returns {Number} squared distance of total position updates.
   */

  var eulerIntegrator = integrate;

  function integrate(bodies, timeStep) {
    var dx = 0, tx = 0,
        dy = 0, ty = 0,
        i,
        max = bodies.length;

    if (max === 0) {
      return 0;
    }

    for (i = 0; i < max; ++i) {
      var body = bodies[i],
          coeff = timeStep / body.mass;

      body.velocity.x += coeff * body.force.x;
      body.velocity.y += coeff * body.force.y;
      var vx = body.velocity.x,
          vy = body.velocity.y,
          v = Math.sqrt(vx * vx + vy * vy);

      if (v > 1) {
        // We normalize it so that we move within timeStep range. 
        // for the case when v <= 1 - we let velocity to fade out.
        body.velocity.x = vx / v;
        body.velocity.y = vy / v;
      }

      dx = timeStep * body.velocity.x;
      dy = timeStep * body.velocity.y;

      body.pos.x += dx;
      body.pos.y += dy;

      tx += Math.abs(dx); ty += Math.abs(dy);
    }

    return (tx * tx + ty * ty)/max;
  }

  var ngraph_physics_primitives = {
    Body: Body,
    Vector2d: Vector2d,
    Body3d: Body3d,
    Vector3d: Vector3d
  };

  function Body(x, y) {
    this.pos = new Vector2d(x, y);
    this.prevPos = new Vector2d(x, y);
    this.force = new Vector2d();
    this.velocity = new Vector2d();
    this.mass = 1;
  }

  Body.prototype.setPosition = function (x, y) {
    this.prevPos.x = this.pos.x = x;
    this.prevPos.y = this.pos.y = y;
  };

  function Vector2d(x, y) {
    if (x && typeof x !== 'number') {
      // could be another vector
      this.x = typeof x.x === 'number' ? x.x : 0;
      this.y = typeof x.y === 'number' ? x.y : 0;
    } else {
      this.x = typeof x === 'number' ? x : 0;
      this.y = typeof y === 'number' ? y : 0;
    }
  }

  Vector2d.prototype.reset = function () {
    this.x = this.y = 0;
  };

  function Body3d(x, y, z) {
    this.pos = new Vector3d(x, y, z);
    this.prevPos = new Vector3d(x, y, z);
    this.force = new Vector3d();
    this.velocity = new Vector3d();
    this.mass = 1;
  }

  Body3d.prototype.setPosition = function (x, y, z) {
    this.prevPos.x = this.pos.x = x;
    this.prevPos.y = this.pos.y = y;
    this.prevPos.z = this.pos.z = z;
  };

  function Vector3d(x, y, z) {
    if (x && typeof x !== 'number') {
      // could be another vector
      this.x = typeof x.x === 'number' ? x.x : 0;
      this.y = typeof x.y === 'number' ? x.y : 0;
      this.z = typeof x.z === 'number' ? x.z : 0;
    } else {
      this.x = typeof x === 'number' ? x : 0;
      this.y = typeof y === 'number' ? y : 0;
      this.z = typeof z === 'number' ? z : 0;
    }
  }
  Vector3d.prototype.reset = function () {
    this.x = this.y = this.z = 0;
  };

  var createBody = function(pos) {
    return new ngraph_physics_primitives.Body(pos);
  };

  /**
   * Manages a simulation of physical forces acting on bodies and springs.
   */
  var ngraph_physics_simulator = physicsSimulator;

  function physicsSimulator(settings) {
    var Spring = spring;
    var expose = ngraph_expose;
    var merge = ngraph_merge;
    var eventify = ngraph_events;

    settings = merge(settings, {
        /**
         * Ideal length for links (springs in physical model).
         */
        springLength: 30,

        /**
         * Hook's law coefficient. 1 - solid spring.
         */
        springCoeff: 0.0008,

        /**
         * Coulomb's law coefficient. It's used to repel nodes thus should be negative
         * if you make it positive nodes start attract each other :).
         */
        gravity: -1.2,

        /**
         * Theta coefficient from Barnes Hut simulation. Ranged between (0, 1).
         * The closer it's to 1 the more nodes algorithm will have to go through.
         * Setting it to one makes Barnes Hut simulation no different from
         * brute-force forces calculation (each node is considered).
         */
        theta: 0.8,

        /**
         * Drag force coefficient. Used to slow down system, thus should be less than 1.
         * The closer it is to 0 the less tight system will be.
         */
        dragCoeff: 0.02,

        /**
         * Default time step (dt) for forces integration
         */
        timeStep : 20,
    });

    // We allow clients to override basic factory methods:
    var createQuadTree = settings.createQuadTree || ngraph_quadtreebh;
    var createBounds = settings.createBounds || bounds;
    var createDragForce = settings.createDragForce || dragForce;
    var createSpringForce = settings.createSpringForce || springForce;
    var integrate = settings.integrator || eulerIntegrator;
    var createBody$1 = settings.createBody || createBody;

    var bodies = [], // Bodies in this simulation.
        springs = [], // Springs in this simulation.
        quadTree =  createQuadTree(settings),
        bounds$1 = createBounds(bodies, settings),
        springForce$1 = createSpringForce(settings),
        dragForce$1 = createDragForce(settings);

    var bboxNeedsUpdate = true;
    var totalMovement = 0; // how much movement we made on last step

    var publicApi = {
      /**
       * Array of bodies, registered with current simulator
       *
       * Note: To add new body, use addBody() method. This property is only
       * exposed for testing/performance purposes.
       */
      bodies: bodies,

      quadTree: quadTree,

      /**
       * Array of springs, registered with current simulator
       *
       * Note: To add new spring, use addSpring() method. This property is only
       * exposed for testing/performance purposes.
       */
      springs: springs,

      /**
       * Returns settings with which current simulator was initialized
       */
      settings: settings,

      /**
       * Performs one step of force simulation.
       *
       * @returns {boolean} true if system is considered stable; False otherwise.
       */
      step: function () {
        accumulateForces();

        var movement = integrate(bodies, settings.timeStep);
        bounds$1.update();

        return movement;
      },

      /**
       * Adds body to the system
       *
       * @param {ngraph.physics.primitives.Body} body physical body
       *
       * @returns {ngraph.physics.primitives.Body} added body
       */
      addBody: function (body) {
        if (!body) {
          throw new Error('Body is required');
        }
        bodies.push(body);

        return body;
      },

      /**
       * Adds body to the system at given position
       *
       * @param {Object} pos position of a body
       *
       * @returns {ngraph.physics.primitives.Body} added body
       */
      addBodyAt: function (pos) {
        if (!pos) {
          throw new Error('Body position is required');
        }
        var body = createBody$1(pos);
        bodies.push(body);

        return body;
      },

      /**
       * Removes body from the system
       *
       * @param {ngraph.physics.primitives.Body} body to remove
       *
       * @returns {Boolean} true if body found and removed. falsy otherwise;
       */
      removeBody: function (body) {
        if (!body) { return; }

        var idx = bodies.indexOf(body);
        if (idx < 0) { return; }

        bodies.splice(idx, 1);
        if (bodies.length === 0) {
          bounds$1.reset();
        }
        return true;
      },

      /**
       * Adds a spring to this simulation.
       *
       * @returns {Object} - a handle for a spring. If you want to later remove
       * spring pass it to removeSpring() method.
       */
      addSpring: function (body1, body2, springLength, springCoefficient) {
        if (!body1 || !body2) {
          throw new Error('Cannot add null spring to force simulator');
        }

        if (typeof springLength !== 'number') {
          springLength = -1; // assume global configuration
        }

        var spring = new Spring(body1, body2, springLength, springCoefficient >= 0 ? springCoefficient : -1);
        springs.push(spring);

        // TODO: could mark simulator as dirty.
        return spring;
      },

      /**
       * Returns amount of movement performed on last step() call
       */
      getTotalMovement: function () {
        return totalMovement;
      },

      /**
       * Removes spring from the system
       *
       * @param {Object} spring to remove. Spring is an object returned by addSpring
       *
       * @returns {Boolean} true if spring found and removed. falsy otherwise;
       */
      removeSpring: function (spring) {
        if (!spring) { return; }
        var idx = springs.indexOf(spring);
        if (idx > -1) {
          springs.splice(idx, 1);
          return true;
        }
      },

      getBestNewBodyPosition: function (neighbors) {
        return bounds$1.getBestNewPosition(neighbors);
      },

      /**
       * Returns bounding box which covers all bodies
       */
      getBBox: function () {
        if (bboxNeedsUpdate) {
          bounds$1.update();
          bboxNeedsUpdate = false;
        }
        return bounds$1.box;
      },

      invalidateBBox: function () {
        bboxNeedsUpdate = true;
      },

      gravity: function (value) {
        if (value !== undefined) {
          settings.gravity = value;
          quadTree.options({gravity: value});
          return this;
        } else {
          return settings.gravity;
        }
      },

      theta: function (value) {
        if (value !== undefined) {
          settings.theta = value;
          quadTree.options({theta: value});
          return this;
        } else {
          return settings.theta;
        }
      }
    };

    // allow settings modification via public API:
    expose(settings, publicApi);

    eventify(publicApi);

    return publicApi;

    function accumulateForces() {
      // Accumulate forces acting on bodies.
      var body,
          i = bodies.length;

      if (i) {
        // only add bodies if there the array is not empty:
        quadTree.insertBodies(bodies); // performance: O(n * log n)
        while (i--) {
          body = bodies[i];
          // If body is pinned there is no point updating its forces - it should
          // never move:
          if (!body.isPinned) {
            body.force.reset();

            quadTree.updateBodyForce(body);
            dragForce$1.update(body);
          }
        }
      }

      i = springs.length;
      while(i--) {
        springForce$1.update(springs[i]);
      }
    }
  }

  var ngraph_forcelayout = createLayout;
  var simulator = ngraph_physics_simulator;



  /**
   * Creates force based layout for a given graph.
   *
   * @param {ngraph.graph} graph which needs to be laid out
   * @param {object} physicsSettings if you need custom settings
   * for physics simulator you can pass your own settings here. If it's not passed
   * a default one will be created.
   */
  function createLayout(graph, physicsSettings) {
    if (!graph) {
      throw new Error('Graph structure cannot be undefined');
    }

    var createSimulator = ngraph_physics_simulator;
    var physicsSimulator = createSimulator(physicsSettings);

    var nodeMass = defaultNodeMass;
    if (physicsSettings && typeof physicsSettings.nodeMass === 'function') {
      nodeMass = physicsSettings.nodeMass;
    }

    var nodeBodies = new Map();
    var springs = {};
    var bodiesCount = 0;

    var springTransform = physicsSimulator.settings.springTransform || noop$1;

    // Initialize physics with what we have in the graph:
    initPhysics();
    listenToEvents();

    var wasStable = false;

    var api = {
      /**
       * Performs one step of iterative layout algorithm
       *
       * @returns {boolean} true if the system should be considered stable; False otherwise.
       * The system is stable if no further call to `step()` can improve the layout.
       */
      step: function() {
        if (bodiesCount === 0) return true; // TODO: This will never fire 'stable'

        var lastMove = physicsSimulator.step();

        // Save the movement in case if someone wants to query it in the step
        // callback.
        api.lastMove = lastMove;

        // Allow listeners to perform low-level actions after nodes are updated.
        api.fire('step');

        var ratio = lastMove/bodiesCount;
        var isStableNow = ratio <= 0.01; // TODO: The number is somewhat arbitrary...

        if (wasStable !== isStableNow) {
          wasStable = isStableNow;
          onStableChanged(isStableNow);
        }

        return isStableNow;
      },

      /**
       * For a given `nodeId` returns position
       */
      getNodePosition: function (nodeId) {
        return getInitializedBody(nodeId).pos;
      },

      /**
       * Sets position of a node to a given coordinates
       * @param {string} nodeId node identifier
       * @param {number} x position of a node
       * @param {number} y position of a node
       * @param {number=} z position of node (only if applicable to body)
       */
      setNodePosition: function (nodeId) {
        var body = getInitializedBody(nodeId);
        body.setPosition.apply(body, Array.prototype.slice.call(arguments, 1));
        physicsSimulator.invalidateBBox();
      },

      /**
       * @returns {Object} Link position by link id
       * @returns {Object.from} {x, y} coordinates of link start
       * @returns {Object.to} {x, y} coordinates of link end
       */
      getLinkPosition: function (linkId) {
        var spring = springs[linkId];
        if (spring) {
          return {
            from: spring.from.pos,
            to: spring.to.pos
          };
        }
      },

      /**
       * @returns {Object} area required to fit in the graph. Object contains
       * `x1`, `y1` - top left coordinates
       * `x2`, `y2` - bottom right coordinates
       */
      getGraphRect: function () {
        return physicsSimulator.getBBox();
      },

      /**
       * Iterates over each body in the layout simulator and performs a callback(body, nodeId)
       */
      forEachBody: forEachBody,

      /*
       * Requests layout algorithm to pin/unpin node to its current position
       * Pinned nodes should not be affected by layout algorithm and always
       * remain at their position
       */
      pinNode: function (node, isPinned) {
        var body = getInitializedBody(node.id);
         body.isPinned = !!isPinned;
      },

      /**
       * Checks whether given graph's node is currently pinned
       */
      isNodePinned: function (node) {
        return getInitializedBody(node.id).isPinned;
      },

      /**
       * Request to release all resources
       */
      dispose: function() {
        graph.off('changed', onGraphChanged);
        api.fire('disposed');
      },

      /**
       * Gets physical body for a given node id. If node is not found undefined
       * value is returned.
       */
      getBody: getBody,

      /**
       * Gets spring for a given edge.
       *
       * @param {string} linkId link identifer. If two arguments are passed then
       * this argument is treated as formNodeId
       * @param {string=} toId when defined this parameter denotes head of the link
       * and first argument is treated as tail of the link (fromId)
       */
      getSpring: getSpring,

      /**
       * [Read only] Gets current physics simulator
       */
      simulator: physicsSimulator,

      /**
       * Gets the graph that was used for layout
       */
      graph: graph,

      /**
       * Gets amount of movement performed during last step operation
       */
      lastMove: 0
    };

    ngraph_events(api);

    return api;

    function forEachBody(cb) {
      nodeBodies.forEach(function(body, bodyId) {
        cb(body, bodyId);
      });
    }

    function getSpring(fromId, toId) {
      var linkId;
      if (toId === undefined) {
        if (typeof fromId !== 'object') {
          // assume fromId as a linkId:
          linkId = fromId;
        } else {
          // assume fromId to be a link object:
          linkId = fromId.id;
        }
      } else {
        // toId is defined, should grab link:
        var link = graph.hasLink(fromId, toId);
        if (!link) return;
        linkId = link.id;
      }

      return springs[linkId];
    }

    function getBody(nodeId) {
      return nodeBodies.get(nodeId);
    }

    function listenToEvents() {
      graph.on('changed', onGraphChanged);
    }

    function onStableChanged(isStable) {
      api.fire('stable', isStable);
    }

    function onGraphChanged(changes) {
      for (var i = 0; i < changes.length; ++i) {
        var change = changes[i];
        if (change.changeType === 'add') {
          if (change.node) {
            initBody(change.node.id);
          }
          if (change.link) {
            initLink(change.link);
          }
        } else if (change.changeType === 'remove') {
          if (change.node) {
            releaseNode(change.node);
          }
          if (change.link) {
            releaseLink(change.link);
          }
        }
      }
      bodiesCount = graph.getNodesCount();