123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- module.exports = createLayout;
- module.exports.simulator = require('./lib/createPhysicsSimulator');
- var eventify = require('ngraph.events');
- /**
- * 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 = (physicsSettings && physicsSettings.createSimulator) || require('./lib/createPhysicsSimulator');
- var physicsSimulator = createSimulator(physicsSettings);
- if (Array.isArray(physicsSettings)) throw new Error('Physics settings is expected to be an object');
- var nodeMass = graph.version > 19 ? defaultSetNodeMass : defaultArrayNodeMass;
- if (physicsSettings && typeof physicsSettings.nodeMass === 'function') {
- nodeMass = physicsSettings.nodeMass;
- }
- var nodeBodies = new Map();
- var springs = {};
- var bodiesCount = 0;
- var springTransform = physicsSimulator.settings.springTransform || noop;
- // 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) {
- updateStableStatus(true);
- return true;
- }
- 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...
- updateStableStatus(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));
- },
- /**
- * @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,
- /**
- * Returns length of cumulative force vector. The closer this to zero - the more stable the system is
- */
- getForceVectorLength: getForceVectorLength,
- /**
- * [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
- };
- eventify(api);
- return api;
- function updateStableStatus(isStableNow) {
- if (wasStable !== isStableNow) {
- wasStable = isStableNow;
- onStableChanged(isStableNow);
- }
- }
- function forEachBody(cb) {
- nodeBodies.forEach(cb);
- }
- function getForceVectorLength() {
- var fx = 0, fy = 0;
- forEachBody(function(body) {
- fx += Math.abs(body.force.x);
- fy += Math.abs(body.force.y);
- });
- return Math.sqrt(fx * fx + fy * fy);
- }
- 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();
- }
- function initPhysics() {
- bodiesCount = 0;
- graph.forEachNode(function (node) {
- initBody(node.id);
- bodiesCount += 1;
- });
- graph.forEachLink(initLink);
- }
- function initBody(nodeId) {
- var body = nodeBodies.get(nodeId);
- if (!body) {
- var node = graph.getNode(nodeId);
- if (!node) {
- throw new Error('initBody() was called with unknown node id');
- }
- var pos = node.position;
- if (!pos) {
- var neighbors = getNeighborBodies(node);
- pos = physicsSimulator.getBestNewBodyPosition(neighbors);
- }
- body = physicsSimulator.addBodyAt(pos);
- body.id = nodeId;
- nodeBodies.set(nodeId, body);
- updateBodyMass(nodeId);
- if (isNodeOriginallyPinned(node)) {
- body.isPinned = true;
- }
- }
- }
- function releaseNode(node) {
- var nodeId = node.id;
- var body = nodeBodies.get(nodeId);
- if (body) {
- nodeBodies.delete(nodeId);
- physicsSimulator.removeBody(body);
- }
- }
- function initLink(link) {
- updateBodyMass(link.fromId);
- updateBodyMass(link.toId);
- var fromBody = nodeBodies.get(link.fromId),
- toBody = nodeBodies.get(link.toId),
- spring = physicsSimulator.addSpring(fromBody, toBody, link.length);
- springTransform(link, spring);
- springs[link.id] = spring;
- }
- function releaseLink(link) {
- var spring = springs[link.id];
- if (spring) {
- var from = graph.getNode(link.fromId),
- to = graph.getNode(link.toId);
- if (from) updateBodyMass(from.id);
- if (to) updateBodyMass(to.id);
- delete springs[link.id];
- physicsSimulator.removeSpring(spring);
- }
- }
- function getNeighborBodies(node) {
- // TODO: Could probably be done better on memory
- var neighbors = [];
- if (!node.links) {
- return neighbors;
- }
- var maxNeighbors = Math.min(node.links.length, 2);
- for (var i = 0; i < maxNeighbors; ++i) {
- var link = node.links[i];
- var otherBody = link.fromId !== node.id ? nodeBodies.get(link.fromId) : nodeBodies.get(link.toId);
- if (otherBody && otherBody.pos) {
- neighbors.push(otherBody);
- }
- }
- return neighbors;
- }
- function updateBodyMass(nodeId) {
- var body = nodeBodies.get(nodeId);
- body.mass = nodeMass(nodeId);
- if (Number.isNaN(body.mass)) {
- throw new Error('Node mass should be a number');
- }
- }
- /**
- * Checks whether graph node has in its settings pinned attribute,
- * which means layout algorithm cannot move it. Node can be marked
- * as pinned, if it has "isPinned" attribute, or when node.data has it.
- *
- * @param {Object} node a graph node to check
- * @return {Boolean} true if node should be treated as pinned; false otherwise.
- */
- function isNodeOriginallyPinned(node) {
- return (node && (node.isPinned || (node.data && node.data.isPinned)));
- }
- function getInitializedBody(nodeId) {
- var body = nodeBodies.get(nodeId);
- if (!body) {
- initBody(nodeId);
- body = nodeBodies.get(nodeId);
- }
- return body;
- }
- /**
- * Calculates mass of a body, which corresponds to node with given id.
- *
- * @param {String|Number} nodeId identifier of a node, for which body mass needs to be calculated
- * @returns {Number} recommended mass of the body;
- */
- function defaultArrayNodeMass(nodeId) {
- // This function is for older versions of ngraph.graph.
- var links = graph.getLinks(nodeId);
- if (!links) return 1;
- return 1 + links.length / 3.0;
- }
- function defaultSetNodeMass(nodeId) {
- var links = graph.getLinks(nodeId);
- if (!links) return 1;
- return 1 + links.size / 3.0;
- }
- }
- function noop() { }
|