function createLayout(graph, physicsSettings) { var merge = require('ngraph.merge'); physicsSettings = merge(physicsSettings, { dimension: 2, createQuadTree: require('./lib/orthantTree/index.js'), createBounds: require('./lib/bounds'), createDragForce: require('./lib/dragForce'), createSpringForce: require('./lib/springForce'), integrator: require('./lib/eulerIntegrator'), createBody: require('./lib/createBody') }); return createLayout.get2dLayout(graph, physicsSettings); }
function save(graph, options) { options = merge(options, { outDir: '.', labels: 'labels.json', meta: 'meta.json', links: 'links.bin' }); fixPaths(); var labels = require('./lib/getLabels.js')(graph); saveLabels(labels); var linksBuffer = require('./lib/getLinksBuffer.js')(graph, labels); fs.writeFileSync(options.links, linksBuffer); console.log(graph.getLinksCount() + ' links saved to ' + options.links); saveMeta(); function fixPaths() { if (!fs.existsSync(options.outDir)) { mkdirp.sync(options.outDir); } options.labels = path.join(options.outDir, options.labels); options.meta = path.join(options.outDir, options.meta); options.links = path.join(options.outDir, options.links); } function saveMeta() { var meta = getMetaInfo(); fs.writeFileSync(options.meta, JSON.stringify(meta), 'utf8'); console.log('Meta information saved to ' + options.meta); } function getMetaInfo() { return { date: +new Date(), nodeCount: graph.getNodesCount(), linkCount: graph.getLinksCount(), nodeFile: options.labels, linkFile: options.links, version: require(path.join(__dirname, 'package.json')).version }; } function saveLabels(labels) { fs.writeFileSync(options.labels, JSON.stringify(labels), 'utf8'); console.log(labels.length + ' ids saved to ' + options.labels); } }
function createLayout(graph, physicsSettings) { var createSimulator = require('ngraph.physics.simulator'); var createForceLayout = require('ngraph.forcelayout'); var merge = require('ngraph.merge'); var physicsSettings = merge(physicsSettings, { createQuadTree: require('ngraph.quadtreebh3d'), createBounds: require('./lib/bounds'), createDragForce: require('./lib/dragForce'), createSpringForce: require('./lib/springForce'), integrator: require('./lib/eulerIntegrator'), createBody: require('./lib/createBody') }); var simulator3d = createSimulator(physicsSettings); var layout = createForceLayout(graph, simulator3d); return layout; }
module.exports = function (options) { var merge = require('ngraph.merge'), expose = require('ngraph.expose'); options = merge(options, { dragCoeff: 0.02 }); var api = { update : function (body) { body.force.moveByScale(-options.dragCoeff, body.velocity); } }; // let easy access to dragCoeff: expose(options, api, ['dragCoeff']); return api; };
module.exports = function (graph, settings) { var merge = require('ngraph.merge'); // Initialize default settings: settings = merge(settings, { // What is the background color of a graph? background: 0x000000, // Default physics engine settings physics: { springLength: 30, springCoeff: 0.0008, dragCoeff: 0.01, gravity: -1.2, theta: 1 } }); // Where do we render our graph? if (typeof settings.container === 'undefined') { settings.container = document.body } // If client does not need custom layout algorithm, let's create default one: var layout = settings.layout; if (!layout) { var createLayout = require('ngraph.forcelayout'), physics = require('ngraph.physics.simulator'); layout = createLayout(graph, physics(settings.physics)); } var width = settings.container.clientWidth, height = settings.container.clientHeight; var stage = new PIXI.Stage(settings.background, true); var renderer = PIXI.autoDetectRenderer(width, height, null, false, true); settings.container.appendChild(renderer.view); var graphics = new PIXI.Graphics(); graphics.position.x = width/2; graphics.position.y = height/2; graphics.scale.x = 1; graphics.scale.y = 1; stage.addChild(graphics); // Default callbacks to build/render nodes var nodeUIBuilder = defaultCreateNodeUI, nodeRenderer = defaultNodeRenderer, linkUIBuilder = defaultCreateLinkUI, linkRenderer = defaultLinkRenderer; // Storage for UI of nodes/links: var nodeUI = {}, linkUI = {}; graph.forEachNode(initNode); graph.forEachLink(initLink); listenToGraphEvents(); var pixiGraphics = { /** * Allows client to start animation loop, without worrying about RAF stuff. */ run: animationLoop, /** * For more sophisticated clients we expose one frame rendering as part of * API. This may be useful for clients who have their own RAF pipeline. */ renderOneFrame: renderOneFrame, /** * This callback creates new UI for a graph node. This becomes helpful * when you want to precalculate some properties, which otherwise could be * expensive during rendering frame. * * @callback createNodeUICallback * @param {object} node - graph node for which UI is required. * @returns {object} arbitrary object which will be later passed to renderNode */ /** * This function allows clients to pass custon node UI creation callback * * @param {createNodeUICallback} createNodeUICallback - The callback that * creates new node UI * @returns {object} this for chaining. */ createNodeUI : function (createNodeUICallback) { nodeUI = {}; nodeUIBuilder = createNodeUICallback; graph.forEachNode(initNode); return this; }, /** * This callback is called by pixiGraphics when it wants to render node on * a screen. * * @callback renderNodeCallback * @param {object} node - result of createNodeUICallback(). It contains anything * you'd need to render a node * @param {PIXI.Graphics} ctx - PIXI's rendering context. */ /** * Allows clients to pass custom node rendering callback * * @param {renderNodeCallback} renderNodeCallback - Callback which renders * node. * * @returns {object} this for chaining. */ renderNode: function (renderNodeCallback) { nodeRenderer = renderNodeCallback; return this; }, /** * This callback creates new UI for a graph link. This becomes helpful * when you want to precalculate some properties, which otherwise could be * expensive during rendering frame. * * @callback createLinkUICallback * @param {object} link - graph link for which UI is required. * @returns {object} arbitrary object which will be later passed to renderNode */ /** * This function allows clients to pass custon node UI creation callback * * @param {createLinkUICallback} createLinkUICallback - The callback that * creates new link UI * @returns {object} this for chaining. */ createLinkUI : function (createLinkUICallback) { linkUI = {}; linkUIBuilder = createLinkUICallback; graph.forEachLink(initLink); return this; }, /** * This callback is called by pixiGraphics when it wants to render link on * a screen. * * @callback renderLinkCallback * @param {object} link - result of createLinkUICallback(). It contains anything * you'd need to render a link * @param {PIXI.Graphics} ctx - PIXI's rendering context. */ /** * Allows clients to pass custom link rendering callback * * @param {renderLinkCallback} renderLinkCallback - Callback which renders * link. * * @returns {object} this for chaining. */ renderLink: function (renderLinkCallback) { linkRenderer = renderLinkCallback; return this; }, /** * Tries to get node at (x, y) graph coordinates. By default renderer assumes * width and height of the node is 10 pixels. But if your createNodeUICallback * returns object with `width` and `height` attributes, they are considered * as actual dimensions of a node * * @param {Number} x - x coordinate of a node in layout's coordinates * @param {Number} y - y coordinate of a node in layout's coordinates * @returns {Object} - acutal graph node located at (x, y) coordinates. * If there is no node in that are `undefined` is returned. * * TODO: This should be part of layout itself */ getNodeAt: getNodeAt, /** * [Read only] Current layout algorithm. If you want to pass custom layout * algorithm, do it via `settings` argument of ngraph.pixi. */ layout: layout, // TODO: These properties seem to only be required fo graph input. I'd really // like to hide them, but not sure how to do it nicely domContainer: renderer.view, graphGraphics: graphics, stage: stage }; // listen to mouse events var graphInput = require('./lib/graphInput'); graphInput(pixiGraphics, layout); return pixiGraphics; /////////////////////////////////////////////////////////////////////////////// // Public API is over /////////////////////////////////////////////////////////////////////////////// function animationLoop() { layout.step(); renderOneFrame(); requestAnimFrame(animationLoop); } function renderOneFrame() { graphics.clear(); Object.keys(linkUI).forEach(renderLink); Object.keys(nodeUI).forEach(renderNode); renderer.render(stage); } function renderLink(linkId) { linkRenderer(linkUI[linkId], graphics); } function renderNode(nodeId) { nodeRenderer(nodeUI[nodeId], graphics); } function initNode(node) { var ui = nodeUIBuilder(node); // augment it with position data: ui.pos = layout.getNodePosition(node.id); // and store for subsequent use: nodeUI[node.id] = ui; } function initLink(link) { var ui = linkUIBuilder(link); ui.from = layout.getNodePosition(link.fromId); ui.to = layout.getNodePosition(link.toId); linkUI[link.id] = ui; } function defaultCreateNodeUI(node) { return {}; } function defaultCreateLinkUI(link) { return {}; } function defaultNodeRenderer(node) { var x = node.pos.x - NODE_WIDTH/2, y = node.pos.y - NODE_WIDTH/2; graphics.beginFill(0xFF3300); graphics.drawRect(x, y, NODE_WIDTH, NODE_WIDTH); } function defaultLinkRenderer(link) { graphics.lineStyle(1, 0xcccccc, 1); graphics.moveTo(link.from.x, link.from.y); graphics.lineTo(link.to.x, link.to.y); } function getNodeAt(x, y) { var half = NODE_WIDTH/2; // currently it's a linear search, but nothing stops us from refactoring // this into spatial lookup data structure in future: for (var nodeId in nodeUI) { if (nodeUI.hasOwnProperty(nodeId)) { var node = nodeUI[nodeId]; var pos = node.pos; var width = node.width || NODE_WIDTH; var half = width/2; var insideNode = pos.x - half < x && x < pos.x + half && pos.y - half < y && y < pos.y + half; if (insideNode) { return graph.getNode(nodeId); } } } } function listenToGraphEvents() { graph.on('changed', onGraphChanged); } function onGraphChanged(changes) { for (var i = 0; i < changes.length; ++i) { var change = changes[i]; if (change.changeType === 'add') { if (change.node) { initNode(change.node); } if (change.link) { initLink(change.link); } } else if (change.changeType === 'remove') { if (change.node) { delete nodeUI[change.node.id]; } if (change.link) { delete linkUI[change.link.id]; } } } } }
function webglGraphics(options) { options = merge(options, { enableBlending : true, preserveDrawingBuffer : false, clearColor: false, clearColorValue : { r : 1, g : 1, b : 1, a : 1 } }); var container, graphicsRoot, gl, width, height, nodesCount = 0, linksCount = 0, transform = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ], userPlaceNodeCallback, userPlaceLinkCallback, nodes = [], links = [], initCallback, allNodes = {}, allLinks = {}, linkProgram = webglLinkProgram(), nodeProgram = webglNodeProgram(), /*jshint unused: false */ nodeUIBuilder = function (node) { return webglSquare(); // Just make a square, using provided gl context (a nodeProgram); }, linkUIBuilder = function (link) { return webglLine(0xb3b3b3ff); }, /*jshint unused: true */ updateTransformUniform = function () { linkProgram.updateTransform(transform); nodeProgram.updateTransform(transform); }, resetScaleInternal = function () { transform = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; }, updateSize = function () { if (container && graphicsRoot) { width = graphicsRoot.width = Math.max(container.offsetWidth, 1); height = graphicsRoot.height = Math.max(container.offsetHeight, 1); if (gl) { gl.viewport(0, 0, width, height); } if (linkProgram) { linkProgram.updateSize(width / 2, height / 2); } if (nodeProgram) { nodeProgram.updateSize(width / 2, height / 2); } } }, fireRescaled = function (graphics) { graphics.fire("rescaled"); }; graphicsRoot = window.document.createElement("canvas"); var graphics = { getLinkUI: function (linkId) { return allLinks[linkId]; }, getNodeUI: function (nodeId) { return allNodes[nodeId]; }, /** * Sets the callback that creates node representation. * * @param builderCallback a callback function that accepts graph node * as a parameter and must return an element representing this node. * * @returns If builderCallbackOrNode is a valid callback function, instance of this is returned; * Otherwise undefined value is returned */ node : function (builderCallback) { if (typeof builderCallback !== "function") { return; // todo: throw? This is not compatible with old versions } nodeUIBuilder = builderCallback; return this; }, /** * Sets the callback that creates link representation * * @param builderCallback a callback function that accepts graph link * as a parameter and must return an element representing this link. * * @returns If builderCallback is a valid callback function, instance of this is returned; * Otherwise undefined value is returned. */ link : function (builderCallback) { if (typeof builderCallback !== "function") { return; // todo: throw? This is not compatible with old versions } linkUIBuilder = builderCallback; return this; }, /** * Allows to override default position setter for the node with a new * function. newPlaceCallback(nodeUI, position) is function which * is used by updateNodePosition(). */ placeNode : function (newPlaceCallback) { userPlaceNodeCallback = newPlaceCallback; return this; }, placeLink : function (newPlaceLinkCallback) { userPlaceLinkCallback = newPlaceLinkCallback; return this; }, /** * Custom input manager listens to mouse events to process nodes drag-n-drop inside WebGL canvas */ inputManager : webglInputManager, /** * Called every time before renderer starts rendering. */ beginRender : function () { // this function could be replaced by this.init, // based on user options. }, /** * Called every time when renderer finishes one step of rendering. */ endRender : function () { if (linksCount > 0) { linkProgram.render(); } if (nodesCount > 0) { nodeProgram.render(); } }, bringLinkToFront : function (linkUI) { var frontLinkId = linkProgram.getFrontLinkId(), srcLinkId, temp; linkProgram.bringToFront(linkUI); if (frontLinkId > linkUI.id) { srcLinkId = linkUI.id; temp = links[frontLinkId]; links[frontLinkId] = links[srcLinkId]; links[frontLinkId].id = frontLinkId; links[srcLinkId] = temp; links[srcLinkId].id = srcLinkId; } }, /** * Sets translate operation that should be applied to all nodes and links. */ graphCenterChanged : function (x, y) { transform[12] = (2 * x / width) - 1; transform[13] = 1 - (2 * y / height); updateTransformUniform(); }, /** * Called by Viva.Graph.View.renderer to let concrete graphic output * provider prepare to render given link of the graph * * @param link - model of a link */ addLink: function (link, boundPosition) { var uiid = linksCount++, ui = linkUIBuilder(link); ui.id = uiid; ui.pos = boundPosition; linkProgram.createLink(ui); links[uiid] = ui; allLinks[link.id] = ui; return ui; }, /** * Called by Viva.Graph.View.renderer to let concrete graphic output * provider prepare to render given node of the graph. * * @param nodeUI visual representation of the node created by node() execution. **/ addNode : function (node, boundPosition) { var uiid = nodesCount++, ui = nodeUIBuilder(node); ui.id = uiid; ui.position = boundPosition; ui.node = node; nodeProgram.createNode(ui); nodes[uiid] = ui; allNodes[node.id] = ui; return ui; }, translateRel : function (dx, dy) { transform[12] += (2 * transform[0] * dx / width) / transform[0]; transform[13] -= (2 * transform[5] * dy / height) / transform[5]; updateTransformUniform(); }, scale : function (scaleFactor, scrollPoint) { // Transform scroll point to clip-space coordinates: var cx = 2 * scrollPoint.x / width - 1, cy = 1 - (2 * scrollPoint.y) / height; cx -= transform[12]; cy -= transform[13]; transform[12] += cx * (1 - scaleFactor); transform[13] += cy * (1 - scaleFactor); transform[0] *= scaleFactor; transform[5] *= scaleFactor; updateTransformUniform(); fireRescaled(this); return transform[0]; }, resetScale : function () { resetScaleInternal(); if (gl) { updateSize(); // TODO: what is this? // gl.useProgram(linksProgram); // gl.uniform2f(linksProgram.screenSize, width, height); updateTransformUniform(); } return this; }, /** * Called by Viva.Graph.View.renderer to let concrete graphic output * provider prepare to render. */ init : function (c) { var contextParameters = {}; if (options.preserveDrawingBuffer) { contextParameters.preserveDrawingBuffer = true; } container = c; updateSize(); resetScaleInternal(); container.appendChild(graphicsRoot); gl = graphicsRoot.getContext("experimental-webgl", contextParameters); if (!gl) { var msg = "Could not initialize WebGL. Seems like the browser doesn't support it."; window.alert(msg); throw msg; } if (options.enableBlending) { gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); gl.enable(gl.BLEND); } if (options.clearColor) { var color = options.clearColorValue; gl.clearColor(color.r, color.g, color.b, color.a); // TODO: not the best way, really. Should come up with something better // what if we need more updates inside beginRender, like depth buffer? this.beginRender = function () { gl.clear(gl.COLOR_BUFFER_BIT); }; } linkProgram.load(gl); linkProgram.updateSize(width / 2, height / 2); nodeProgram.load(gl); nodeProgram.updateSize(width / 2, height / 2); updateTransformUniform(); // Notify the world if someone waited for update. TODO: should send an event if (typeof initCallback === "function") { initCallback(graphicsRoot); } }, /** * Called by Viva.Graph.View.renderer to let concrete graphic output * provider release occupied resources. */ release : function (container) { if (graphicsRoot && container) { container.removeChild(graphicsRoot); // TODO: anything else? } }, /** * Checks whether webgl is supported by this browser. */ isSupported : function () { var c = window.document.createElement("canvas"), gl = c && c.getContext && c.getContext("experimental-webgl"); return gl; }, /** * Called by Viva.Graph.View.renderer to let concrete graphic output * provider remove link from rendering surface. * * @param linkUI visual representation of the link created by link() execution. **/ releaseLink : function (link) { if (linksCount > 0) { linksCount -= 1; } var linkUI = allLinks[link.id]; delete allLinks[link.id]; linkProgram.removeLink(linkUI); var linkIdToRemove = linkUI.id; if (linkIdToRemove < linksCount) { if (linksCount === 0 || linksCount === linkIdToRemove) { return; // no more links or removed link is the last one. } var lastLinkUI = links[linksCount]; links[linkIdToRemove] = lastLinkUI; lastLinkUI.id = linkIdToRemove; } }, /** * Called by Viva.Graph.View.renderer to let concrete graphic output * provider remove node from rendering surface. * * @param nodeUI visual representation of the node created by node() execution. **/ releaseNode : function (node) { if (nodesCount > 0) { nodesCount -= 1; } var nodeUI = allNodes[node.id]; delete allNodes[node.id]; nodeProgram.removeNode(nodeUI); var nodeIdToRemove = nodeUI.id; if (nodeIdToRemove < nodesCount) { if (nodesCount === 0 || nodesCount === nodeIdToRemove) { return; // no more nodes or removed node is the last in the list. } var lastNodeUI = nodes[nodesCount]; nodes[nodeIdToRemove] = lastNodeUI; lastNodeUI.id = nodeIdToRemove; // Since concrete shaders may cache properties in the UI element // we are letting them to make this swap (e.g. image node shader // uses this approach to update node's offset in the atlas) nodeProgram.replaceProperties(nodeUI, lastNodeUI); } }, renderNodes: function () { var pos = {x : 0, y : 0}; // WebGL coordinate system is different. Would be better // to have this transform in the shader code, but it would // require every shader to be updated.. for (var i = 0; i < nodesCount; ++i) { var ui = nodes[i]; pos.x = ui.position.x; pos.y = ui.position.y; if (userPlaceNodeCallback) { userPlaceNodeCallback(ui, pos); } nodeProgram.position(ui, pos); } }, renderLinks: function () { if (this.omitLinksRendering) { return; } var toPos = {x : 0, y : 0}; var fromPos = {x : 0, y : 0}; for (var i = 0; i < linksCount; ++i) { var ui = links[i]; var pos = ui.pos.from; fromPos.x = pos.x; fromPos.y = -pos.y; pos = ui.pos.to; toPos.x = pos.x; toPos.y = -pos.y; if (userPlaceLinkCallback) { userPlaceLinkCallback(ui, fromPos, toPos); } linkProgram.position(ui, fromPos, toPos); } }, /** * Returns root element which hosts graphics. */ getGraphicsRoot : function (callbackWhenReady) { // todo: should fire an event, instead of having this context. if (typeof callbackWhenReady === "function") { if (graphicsRoot) { callbackWhenReady(graphicsRoot); } else { initCallback = callbackWhenReady; } } return graphicsRoot; }, /** * Updates default shader which renders nodes * * @param newProgram to use for nodes. */ setNodeProgram : function (newProgram) { if (!gl && newProgram) { // Nothing created yet. Just set shader to the new one // and let initialization logic take care about the rest. nodeProgram = newProgram; } else if (newProgram) { throw "Not implemented. Cannot swap shader on the fly... Yet."; // TODO: unload old shader and reinit. } }, /** * Updates default shader which renders links * * @param newProgram to use for links. */ setLinkProgram : function (newProgram) { if (!gl && newProgram) { // Nothing created yet. Just set shader to the new one // and let initialization logic take care about the rest. linkProgram = newProgram; } else if (newProgram) { throw "Not implemented. Cannot swap shader on the fly... Yet."; // TODO: unload old shader and reinit. } }, /** * Transforms client coordinates into layout coordinates. Client coordinates * are DOM coordinates relative to the rendering container. Layout * coordinates are those assigned by by layout algorithm to each node. * * @param {Object} p - a point object with `x` and `y` attributes. * This method mutates p. */ transformClientToGraphCoordinates: function (p) { // TODO: could be a problem when container has margins? // normalize p.x = ((2 * p.x) / width) - 1; p.y = 1 - ((2 * p.y) / height); // apply transform p.x = (p.x - transform[12]) / transform[0]; p.y = (p.y - transform[13]) / transform[5]; // transform to graph coordinates p.x = p.x * (width / 2); p.y = p.y * (-height / 2); return p; }, /** * Transforms WebGL coordinates into client coordinates. Reverse of * `transformClientToGraphCoordinates()` * * @param {Object} p - a point object with `x` and `y` attributes, which * represents a layout coordinate. This method mutates p. */ transformGraphToClientCoordinates: function (p) { // TODO: could be a problem when container has margins? // transform from graph coordinates p.x = p.x / (width / 2); p.y = p.y / (-height / 2); // apply transform p.x = (p.x * transform[0]) + transform[12]; p.y = (p.y * transform[5]) + transform[13]; // denormalize p.x = ((p.x + 1) * width) / 2; p.y = ((1 - p.y) * height) / 2; return p; }, getNodeAtClientPos: function (clientPos, preciseCheck) { if (typeof preciseCheck !== "function") { // we don't know anything about your node structure here :( // potentially this could be delegated to node program, but for // right now, we are giving up if you don't pass boundary check // callback. It answers to a question is nodeUI covers (x, y) return null; } // first transform to graph coordinates: this.transformClientToGraphCoordinates(clientPos); // now using precise check iterate over each node and find one within box: // TODO: This is poor O(N) performance. for (var i = 0; i < nodesCount; ++i) { if (preciseCheck(nodes[i], clientPos.x, clientPos.y)) { return nodes[i].node; } } return null; } }; // Let graphics fire events before we return it to the caller. eventify(graphics); return graphics; }
/** * Does not really perform any layouting algorithm but is compliant * with renderer interface. Allowing clients to provide specific positioning * callback and get static layout of the graph * * @param {Viva.Graph.graph} graph to layout * @param {Object} userSettings */ function constant(graph, userSettings) { userSettings = merge(userSettings, { maxX : 1024, maxY : 1024, seed : 'Deterministic randomness made me do this' }); // This class simply follows API, it does not use some of the arguments: /*jshint unused: false */ var rand = random(userSettings.seed), graphRect = new Rect(Number.MAX_VALUE, Number.MAX_VALUE, Number.MIN_VALUE, Number.MIN_VALUE), layoutLinks = {}, placeNodeCallback = function (node) { return { x: rand.next(userSettings.maxX), y: rand.next(userSettings.maxY) }; }, updateGraphRect = function (position, graphRect) { if (position.x < graphRect.x1) { graphRect.x1 = position.x; } if (position.x > graphRect.x2) { graphRect.x2 = position.x; } if (position.y < graphRect.y1) { graphRect.y1 = position.y; } if (position.y > graphRect.y2) { graphRect.y2 = position.y; } }, layoutNodes = typeof Object.create === 'function' ? Object.create(null) : {}, ensureNodeInitialized = function (node) { layoutNodes[node.id] = placeNodeCallback(node); updateGraphRect(layoutNodes[node.id], graphRect); }, updateNodePositions = function () { if (graph.getNodesCount() === 0) { return; } graphRect.x1 = Number.MAX_VALUE; graphRect.y1 = Number.MAX_VALUE; graphRect.x2 = Number.MIN_VALUE; graphRect.y2 = Number.MIN_VALUE; graph.forEachNode(ensureNodeInitialized); }, ensureLinkInitialized = function (link) { layoutLinks[link.id] = link; }, onGraphChanged = function(changes) { for (var i = 0; i < changes.length; ++i) { var change = changes[i]; if (change.node) { if (change.changeType === 'add') { ensureNodeInitialized(change.node); } else { delete layoutNodes[change.node.id]; } } if (change.link) { if (change.changeType === 'add') { ensureLinkInitialized(change.link); } else { delete layoutLinks[change.link.id]; } } } }; graph.forEachNode(ensureNodeInitialized); graph.forEachLink(ensureLinkInitialized); graph.on('changed', onGraphChanged); return { /** * Attempts to layout graph within given number of iterations. * * @param {integer} [iterationsCount] number of algorithm's iterations. * The constant layout ignores this parameter. */ run : function (iterationsCount) { this.step(); }, /** * One step of layout algorithm. */ step : function () { updateNodePositions(); return true; // no need to continue. }, /** * Returns rectangle structure {x1, y1, x2, y2}, which represents * current space occupied by graph. */ getGraphRect : function () { return graphRect; }, /** * Request to release all resources */ dispose : function () { graph.off('change', onGraphChanged); }, /* * Checks whether given node is pinned; all nodes in this layout are pinned. */ isNodePinned: function (node) { return true; }, /* * 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) { // noop }, /* * Gets position of a node by its id. If node was not seen by this * layout algorithm undefined value is returned; */ getNodePosition: getNodePosition, /** * Returns {from, to} position of a link. */ getLinkPosition: function (linkId) { var link = layoutLinks[linkId]; return { from : getNodePosition(link.fromId), to : getNodePosition(link.toId) }; }, /** * Sets position of a node to a given coordinates */ setNodePosition: function (nodeId, x, y) { var pos = layoutNodes[nodeId]; if (pos) { pos.x = x; pos.y = y; } }, // Layout specific methods: /** * Based on argument either update default node placement callback or * attempts to place given node using current placement callback. * Setting new node callback triggers position update for all nodes. * * @param {Object} newPlaceNodeCallbackOrNode - if it is a function then * default node placement callback is replaced with new one. Node placement * callback has a form of function (node) {}, and is expected to return an * object with x and y properties set to numbers. * * Otherwise if it's not a function the argument is treated as graph node * and current node placement callback will be used to place it. */ placeNode : function (newPlaceNodeCallbackOrNode) { if (typeof newPlaceNodeCallbackOrNode === 'function') { placeNodeCallback = newPlaceNodeCallbackOrNode; updateNodePositions(); return this; } // it is not a request to update placeNodeCallback, trying to place // a node using current callback: return placeNodeCallback(newPlaceNodeCallbackOrNode); } }; function getNodePosition(nodeId) { return layoutNodes[nodeId]; } }
function physicsSimulator(settings) { var Spring = require('./lib/spring'); var expose = require('ngraph.expose'); var merge = require('ngraph.merge'); var eventify = require('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 || require('ngraph.quadtreebh'); var createBounds = settings.createBounds || require('./lib/bounds'); var createDragForce = settings.createDragForce || require('./lib/dragForce'); var createSpringForce = settings.createSpringForce || require('./lib/springForce'); var integrate = settings.integrator || require('./lib/eulerIntegrator'); var createBody = settings.createBody || require('./lib/createBody'); var bodies = [], // Bodies in this simulation. springs = [], // Springs in this simulation. quadTree = createQuadTree(settings), bounds = createBounds(bodies, settings), springForce = createSpringForce(settings), dragForce = createDragForce(settings); 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.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(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.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, springWeight, 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, springWeight); 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.getBestNewPosition(neighbors); }, /** * Returns bounding box which covers all bodies */ getBBox: function () { return bounds.box; }, 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.update(body); } } } i = springs.length; while(i--) { springForce.update(springs[i]); } } };
module.exports = function (graph, settings) { var merge = require('ngraph.merge'); settings = merge(settings, { // how often do we want to render frames (in ms)? frameInterval: 24, // how we render characters? screen: require('./terminal')() }); // If client does not need custom layout algorithm, let's create default one: var layout = settings.layout || require('ngraph.forcelayout')(graph); var nodeUIBuilder = defaultCreateNodeUI, screen = settings.screen, sx, sy, // screen scale, updated per frame; // min coordinates of graph nodes. Updated per frame, used for centering minX, minY; return { /** * Renders just one frame of animation */ renderOneFrame: renderOneFrame, /** * Runs animation loop */ run : run, /** * This callback creates new text UI for a graph node. * * @callback createNodeUICallback * @param {object} node - graph node for which UI is required. * @returns {String} a character, which should be rendered as a node */ /** * This function allows clients to pass custom node UI creation callback * * @param {createNodeUICallback} createNodeUICallback - The callback that * creates new node UI * @returns {object} this for chaining. */ createNodeUI: function (cb) { nodeUIBuilder = cb; return this; } }; function run() { renderOneFrame(); setTimeout(run, settings.frameInterval); } function renderOneFrame() { layout.step(); updateTransform(); screen.clear(); graph.forEachNode(renderNode); screen.flush(); } function renderNode(node) { var pos = layout.getNodePosition(node.id); // since graph is centered at (0, 0) we want to move its (minx, miny) to // (0,0) of a screen: var x = (pos.x - minx) * sx; var y = (pos.y - miny) * sy; screen.put(x, y, nodeUIBuilder(node)); } function defaultCreateNodeUI(node) { return '*'; } function updateTransform() { // try to fit graph into what we have in terminal: var rect = layout.getGraphRect(); sx = screen.width()/(rect.x2 - rect.x1); sy = screen.height()/(rect.y2 - rect.y1); minx = rect.x1; miny = rect.y1; } }